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   die "illegal trans_id passed for DATEV export: " . $self->{trans_id} . "\n" unless $self->{trans_id} =~ m/^\d+$/;
 
 204   return $self->{trans_id};
 
 211    $self->{accnofrom} = $_[0];
 
 214  return $self->{accnofrom};
 
 221    $self->{accnoto} = $_[0];
 
 224  return $self->{accnoto};
 
 232     $self->{dbh} = $_[0];
 
 233     $self->{provided_dbh} = 1;
 
 236   $self->{dbh} ||= $::form->get_standard_dbh;
 
 243 sub clean_temporary_directories {
 
 244   $::lxdebug->enter_sub;
 
 246   foreach my $path (glob($::lx_office_conf{paths}->{userspath} . "/datev-export-*")) {
 
 247     next unless -d $path;
 
 249     my $mtime = (stat($path))[9];
 
 250     next if ((time() - $mtime) < 8 * 60 * 60);
 
 255   $::lxdebug->leave_sub;
 
 259   $main::lxdebug->enter_sub();
 
 261   my $text      = shift // '';
 
 262   my $field_len = shift;
 
 263   my $fill_char = shift;
 
 264   my $alignment = shift || 'right';
 
 266   my $text_len  = length $text;
 
 268   if ($field_len < $text_len) {
 
 269     $text = substr $text, 0, $field_len;
 
 271   } elsif ($field_len > $text_len) {
 
 272     my $filler = ($fill_char) x ($field_len - $text_len);
 
 273     $text      = $alignment eq 'right' ? $filler . $text : $text . $filler;
 
 276   $main::lxdebug->leave_sub();
 
 281 sub get_datev_stamm {
 
 282   return $_[0]{stamm} ||= selectfirst_hashref_query($::form, $_[0]->dbh, 'SELECT * FROM datev');
 
 285 sub save_datev_stamm {
 
 286   my ($self, $data) = @_;
 
 288   do_query($::form, $self->dbh, 'DELETE FROM datev');
 
 290   my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
 
 292   my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
 
 293   do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
 
 295   $self->dbh->commit unless $self->provided_dbh;
 
 302   die 'no format set!' unless $self->has_format;
 
 304   if ($self->format == DATEV_FORMAT_KNE) {
 
 305     $result = $self->kne_export;
 
 306   } elsif ($self->format == DATEV_FORMAT_OBE) {
 
 307     $result = $self->obe_export;
 
 309     die 'unrecognized export format';
 
 319   die 'no exporttype set!' unless $self->has_exporttype;
 
 321   if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
 
 322     $result = $self->kne_buchungsexport;
 
 323   } elsif ($self->exporttype == DATEV_ET_STAMM) {
 
 324     $result = $self->kne_stammdatenexport;
 
 326     die 'unrecognized exporttype';
 
 333   die 'not yet implemented';
 
 339   return unless $self->from && $self->to;
 
 341   return "transdate >= '" . $self->from->to_lxoffice . "' and transdate <= '" . $self->to->to_lxoffice . "'";
 
 348 sub _get_transactions {
 
 349   $main::lxdebug->enter_sub();
 
 352   my $progress_callback = shift || sub {};
 
 354   my $form     =  $main::form;
 
 356   my $trans_id_filter = '';
 
 358   if ( $self->{trans_id} ) {
 
 359     # ignore dates when trans_id is passed so that the entire transaction is
 
 360     # checked, not just either the initial bookings or the subsequent payments
 
 361     # (the transdates will likely differ)
 
 363     $trans_id_filter = 'ac.trans_id = ' . $self->trans_id;
 
 365     $fromto      =~ s/transdate/ac\.transdate/g;
 
 370   my $filter   = '';            # Useful for debugging purposes
 
 372   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');
 
 375     qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey,
 
 376          ar.invnumber, ar.duedate, ar.amount as umsatz, ar.deliverydate,
 
 378          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
 
 382        LEFT JOIN ar          ON (ac.trans_id    = ar.id)
 
 383        LEFT JOIN customer ct ON (ar.customer_id = ct.id)
 
 384        LEFT JOIN chart c     ON (ac.chart_id    = c.id)
 
 385        LEFT JOIN tax t       ON (ac.tax_id      = t.id)
 
 386        WHERE (ar.id IS NOT NULL)
 
 393        SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey,
 
 394          ap.invnumber, ap.duedate, ap.amount as umsatz, ap.deliverydate,
 
 396          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
 
 400        LEFT JOIN ap        ON (ac.trans_id  = ap.id)
 
 401        LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
 
 402        LEFT JOIN chart c   ON (ac.chart_id  = c.id)
 
 403        LEFT JOIN tax t     ON (ac.tax_id    = t.id)
 
 404        WHERE (ap.id IS NOT NULL)
 
 411        SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey,
 
 412          gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz, NULL as deliverydate,
 
 413          gl.description AS name, NULL as ustid,
 
 414          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
 
 418        LEFT JOIN gl      ON (ac.trans_id  = gl.id)
 
 419        LEFT JOIN chart c ON (ac.chart_id  = c.id)
 
 420        LEFT JOIN tax t   ON (ac.tax_id    = t.id)
 
 421        WHERE (gl.id IS NOT NULL)
 
 426        ORDER BY trans_id, acc_trans_id|;
 
 428   my $sth = prepare_execute_query($form, $self->dbh, $query);
 
 434   while ( $continue && (my $ref = $sth->fetchrow_hashref("NAME_lc")) ) {
 
 435     last unless $ref;  # for single transactions
 
 437     if (($counter % 500) == 0) {
 
 438       $progress_callback->($counter);
 
 441     my $trans    = [ $ref ];
 
 443     my $count    = $ref->{amount};
 
 446     # if the amount of a booking in a group is smaller than 0.02, any tax
 
 447     # amounts will likely be smaller than 1 cent, so go into subcent mode
 
 448     my $subcent  = abs($count) < 0.02;
 
 450     # records from acc_trans are ordered by trans_id and acc_trans_id
 
 451     # first check for unbalanced ledger inside one trans_id
 
 452     # there may be several groups inside a trans_id, e.g. the original booking and the payment
 
 453     # each group individually should be exactly balanced and each group
 
 454     # individually needs its own datev lines
 
 456     # keep fetching new acc_trans lines until the end of a balanced group is reached
 
 457     while (abs($count) > 0.01 || $firstrun || ($subcent && abs($count) > 0.005)) {
 
 458       my $ref2 = $sth->fetchrow_hashref("NAME_lc");
 
 464       # check if trans_id of current acc_trans line is still the same as the
 
 465       # trans_id of the first line in group, i.e. we haven't finished a 0-group
 
 466       # before moving on to the next trans_id, error will likely be in the old
 
 469       if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
 
 470         require SL::DB::Manager::AccTransaction;
 
 471         if ( $trans->[0]->{trans_id} ) {
 
 472           my $acc_trans_old_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
 
 473           $self->add_error("Unbalanced ledger! Old: " . $acc_trans_old_obj->transaction_name) if ref($acc_trans_old_obj);
 
 475         if ( $ref2->{trans_id} ) {
 
 476           my $acc_trans_curr_obj = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $ref2->{trans_id} ]);
 
 477           $self->add_error("Unbalanced ledger! New:" . $acc_trans_curr_obj->transaction_name) if ref($acc_trans_curr_obj);
 
 479         $self->add_error("count: $count");
 
 483       push @{ $trans }, $ref2;
 
 485       $count    += $ref2->{amount};
 
 489     foreach my $i (0 .. scalar(@{ $trans }) - 1) {
 
 490       my $ref        = $trans->[$i];
 
 491       my $prev_ref   = 0 < $i ? $trans->[$i - 1] : undef;
 
 492       if (   $all_taxchart_ids{$ref->{id}}
 
 493           && ($ref->{link} =~ m/(?:AP_tax|AR_tax)/)
 
 494           && (   ($prev_ref && $prev_ref->{taxkey} && (_sign($ref->{amount}) == _sign($prev_ref->{amount})))
 
 495               || $ref->{invoice})) {
 
 499       if (   !$ref->{invoice}   # we have a non-invoice booking (=gl)
 
 500           &&  $ref->{is_tax}    # that has "is_tax" set
 
 501           && !($prev_ref->{is_tax})  # previous line wasn't is_tax
 
 502           &&  (_sign($ref->{amount}) == _sign($prev_ref->{amount}))) {  # and sign same as previous sign
 
 503         $trans->[$i - 1]->{tax_amount} = $ref->{amount};
 
 508     if (scalar(@{$trans}) <= 2) {
 
 509       push @{ $self->{DATEV} }, $trans;
 
 513     # determine at which array position the reference value (called absumsatz) is
 
 514     # and which amount it has
 
 516     for my $j (0 .. (scalar(@{$trans}) - 1)) {
 
 519       # 1: gl transaction (Dialogbuchung), invoice is false, no double split booking allowed
 
 521       # 2: sales or vendor invoice (Verkaufs- und Einkaufsrechnung): invoice is
 
 522       # true, instead of absumsatz use link AR/AP (there should only be one
 
 525       # 3. AR/AP transaction (Kreditoren- und Debitorenbuchung): invoice is false,
 
 526       # instead of absumsatz use link AR/AP (there should only be one, so jump
 
 527       # out of search as soon as you find it )
 
 530       # for gl-bookings no split is allowed and there is no AR/AP account, so we always use the maximum value as a reference
 
 531       # for ap/ar bookings we can always search for AR/AP in link and use that
 
 532       if ( ( not $trans->[$j]->{'invoice'} and abs($trans->[$j]->{'amount'}) > abs($absumsatz) )
 
 533          or ($trans->[$j]->{'invoice'} and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP'))) {
 
 534         $absumsatz     = $trans->[$j]->{'amount'};
 
 539       # Problem: we can't distinguish between AR and AP and normal invoices via boolean "invoice"
 
 540       # for AR and AP transaction exit the loop as soon as an AR or AP account is found
 
 541       # there must be only one AR or AP chart in the booking
 
 542       if ( $trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP') {
 
 543         $notsplitindex = $j;   # position in booking with highest amount
 
 544         $absumsatz     = $trans->[$j]->{'amount'};
 
 549     my $ml             = ($trans->[0]->{'umsatz'} > 0) ? 1 : -1;
 
 550     my $rounding_error = 0;
 
 553     # go through each line and determine if it is a tax booking or not
 
 554     # skip all tax lines and notsplitindex line
 
 555     # push all other accounts (e.g. income or expense) with corresponding taxkey
 
 557     for my $j (0 .. (scalar(@{$trans}) - 1)) {
 
 558       if (   ($j != $notsplitindex)
 
 559           && !$trans->[$j]->{is_tax}
 
 560           && (   $trans->[$j]->{'taxkey'} eq ""
 
 561               || $trans->[$j]->{'taxkey'} eq "0"
 
 562               || $trans->[$j]->{'taxkey'} eq "1"
 
 563               || $trans->[$j]->{'taxkey'} eq "10"
 
 564               || $trans->[$j]->{'taxkey'} eq "11")) {
 
 566         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
 
 568         $absumsatz               += $trans->[$j]->{'amount'};
 
 569         $new_trans{'amount'}      = $trans->[$j]->{'amount'} * (-1);
 
 570         $new_trans{'umsatz'}      = abs($trans->[$j]->{'amount'}) * $ml;
 
 571         $trans->[$j]->{'umsatz'}  = abs($trans->[$j]->{'amount'}) * $ml;
 
 573         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
 
 575       } elsif (($j != $notsplitindex) && !$trans->[$j]->{is_tax}) {
 
 578         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
 
 580         my $tax_rate              = $trans->[$j]->{'taxrate'};
 
 581         $new_trans{'net_amount'}  = $trans->[$j]->{'amount'} * -1;
 
 582         $new_trans{'tax_rate'}    = 1 + $tax_rate;
 
 584         if (!$trans->[$j]->{'invoice'}) {
 
 585           $new_trans{'amount'}      = $form->round_amount(-1 * ($trans->[$j]->{amount} + $trans->[$j]->{tax_amount}), 2);
 
 586           $new_trans{'umsatz'}      = abs($new_trans{'amount'}) * $ml;
 
 587           $trans->[$j]->{'umsatz'}  = $new_trans{'umsatz'};
 
 588           $absumsatz               += -1 * $new_trans{'amount'};
 
 591           my $unrounded             = $trans->[$j]->{'amount'} * (1 + $tax_rate) * -1 + $rounding_error;
 
 592           my $rounded               = $form->round_amount($unrounded, 2);
 
 594           $rounding_error           = $unrounded - $rounded;
 
 595           $new_trans{'amount'}      = $rounded;
 
 596           $new_trans{'umsatz'}      = abs($rounded) * $ml;
 
 597           $trans->[$j]->{'umsatz'}  = $new_trans{umsatz};
 
 598           $absumsatz               -= $rounded;
 
 601         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
 
 602         push @taxed, $self->{DATEV}->[-1];
 
 608     while ((abs($absumsatz) >= 0.01) && (abs($absumsatz) < 1.00)) {
 
 609       if ($idx >= scalar @taxed) {
 
 610         last if (!$correction);
 
 616       my $transaction = $taxed[$idx]->[0];
 
 618       my $old_amount     = $transaction->{amount};
 
 619       my $old_correction = $correction;
 
 622       if (!$transaction->{diff}) {
 
 623         @possible_diffs = (0.01, -0.01);
 
 625         @possible_diffs = ($transaction->{diff});
 
 628       foreach my $diff (@possible_diffs) {
 
 629         my $net_amount = $form->round_amount(($transaction->{amount} + $diff) / $transaction->{tax_rate}, 2);
 
 630         next if ($net_amount != $transaction->{net_amount});
 
 632         $transaction->{diff}    = $diff;
 
 633         $transaction->{amount} += $diff;
 
 634         $transaction->{umsatz} += $diff;
 
 644     $absumsatz = $form->round_amount($absumsatz, 2);
 
 645     if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
 
 646       require SL::DB::Manager::AccTransaction;
 
 647       my $acc_trans_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
 
 648       $self->add_error("Datev-Export fehlgeschlagen! Bei Transaktion " . $acc_trans_obj->transaction_name . " ($absumsatz)");
 
 650     } elsif (abs($absumsatz) >= 0.01) {
 
 651       $self->add_net_gross_differences($absumsatz);
 
 657   $::lxdebug->leave_sub;
 
 660 sub make_kne_data_header {
 
 661   $main::lxdebug->enter_sub();
 
 663   my ($self, $form) = @_;
 
 666   my $stamm = $self->get_datev_stamm;
 
 668   my $jahr = $self->from ? $self->from->year : DateTime->today->year;
 
 671   my $header  = "\x1D\x181";
 
 672   $header    .= _fill($stamm->{datentraegernr}, 3, ' ', 'left');
 
 673   $header    .= ($self->fromto) ? "11" : "13"; # Anwendungsnummer
 
 674   $header    .= _fill($stamm->{dfvkz}, 2, '0');
 
 675   $header    .= _fill($stamm->{beraternr}, 7, '0');
 
 676   $header    .= _fill($stamm->{mandantennr}, 5, '0');
 
 677   $header    .= _fill(($stamm->{abrechnungsnr} // '') . $jahr, 6, '0');
 
 679   $header .= $self->from ? $self->from->strftime('%d%m%y') : '';
 
 680   $header .= $self->to   ? $self->to->strftime('%d%m%y')   : '';
 
 684     $header .= $primanota;
 
 687   $header .= _fill($stamm->{passwort}, 4, '0');
 
 688   $header .= " " x 16;       # Anwendungsinfo
 
 689   $header .= " " x 16;       # Inputinfo
 
 693   my $versionssatz  = $self->exporttype == DATEV_ET_BUCHUNGEN ? "\xB5" . "1," : "\xB6" . "1,";
 
 695   my $query         = qq|SELECT accno FROM chart LIMIT 1|;
 
 696   my $ref           = selectfirst_hashref_query($form, $self->dbh, $query);
 
 698   $versionssatz    .= length $ref->{accno};
 
 699   $versionssatz    .= ",";
 
 700   $versionssatz    .= length $ref->{accno};
 
 701   $versionssatz    .= ",SELF" . "\x1C\x79";
 
 703   $header          .= $versionssatz;
 
 705   $main::lxdebug->leave_sub();
 
 711   $main::lxdebug->enter_sub();
 
 713   my ($date, $six) = @_;
 
 715   my ($day, $month, $year) = split(/\./, $date);
 
 718     $day = substr($day, 1, 1);
 
 720   if (length($month) < 2) {
 
 721     $month = "0" . $month;
 
 723   if (length($year) > 2) {
 
 724     $year = substr($year, -2, 2);
 
 728     $date = $day . $month . $year;
 
 730     $date = $day . $month;
 
 733   $main::lxdebug->leave_sub();
 
 738 sub trim_leading_zeroes {
 
 746 sub make_ed_versionset {
 
 747   $main::lxdebug->enter_sub();
 
 749   my ($self, $header, $filename, $blockcount) = @_;
 
 751   my $versionset  = "V" . substr($filename, 2, 5);
 
 752   $versionset    .= substr($header, 6, 22);
 
 755     $versionset .= "0000" . substr($header, 28, 19);
 
 757     my $datum = " " x 16;
 
 758     $versionset .= $datum . "001" . substr($header, 28, 4);
 
 761   $versionset .= _fill($blockcount, 5, '0');
 
 762   $versionset .= "001";
 
 764   $versionset .= substr($header, -12, 10) . "    ";
 
 765   $versionset .= " " x 53;
 
 767   $main::lxdebug->leave_sub();
 
 773   $main::lxdebug->enter_sub();
 
 775   my ($self, $form, $fileno) = @_;
 
 777   my $stamm = $self->get_datev_stamm;
 
 779   my $ev_header  = _fill($stamm->{datentraegernr}, 3, ' ', 'left');
 
 781   $ev_header    .= _fill($stamm->{beraternr}, 7, ' ', 'left');
 
 782   $ev_header    .= _fill($stamm->{beratername}, 9, ' ', 'left');
 
 784   $ev_header    .= (_fill($fileno, 5, '0')) x 2;
 
 785   $ev_header    .= " " x 95;
 
 787   $main::lxdebug->leave_sub();
 
 792 sub kne_buchungsexport {
 
 793   $main::lxdebug->enter_sub();
 
 801   my $filename    = "ED00000";
 
 806   my $fromto = $self->fromto;
 
 808   $self->_get_transactions($fromto);
 
 810   return if $self->errors;
 
 814   while (scalar(@{ $self->{DATEV} || [] })) {
 
 817     my $ed_filename = $self->export_path . $filename;
 
 818     push(@filenames, $filename);
 
 819     my $header = $self->make_kne_data_header($form);
 
 821     my $kne_file = SL::DATEV::KNEFile->new();
 
 822     $kne_file->add_block($header);
 
 824     while (scalar(@{ $self->{DATEV} }) > 0) {
 
 825       my $transaction = shift @{ $self->{DATEV} };
 
 826       my $trans_lines = scalar(@{$transaction});
 
 835       my $buchungstext   = "";
 
 837       my $datevautomatik = 0;
 
 842       my $iconv          = $::locale->{iconv_utf8};
 
 843       my %umlaute = ($iconv->convert('ä') => 'ae',
 
 844                      $iconv->convert('ö') => 'oe',
 
 845                      $iconv->convert('ü') => 'ue',
 
 846                      $iconv->convert('Ä') => 'Ae',
 
 847                      $iconv->convert('Ö') => 'Oe',
 
 848                      $iconv->convert('Ü') => 'Ue',
 
 849                      $iconv->convert('ß') => 'sz');
 
 850       for (my $i = 0; $i < $trans_lines; $i++) {
 
 851         if ($trans_lines == 2) {
 
 852           if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
 
 853             $umsatz = $transaction->[$i]->{'amount'};
 
 856           if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
 
 857             $umsatz = $transaction->[$i]->{'umsatz'};
 
 860         if ($transaction->[$i]->{'datevautomatik'}) {
 
 863         if ($transaction->[$i]->{'taxkey'}) {
 
 864           $taxkey = $transaction->[$i]->{'taxkey'};
 
 866         if ($transaction->[$i]->{'charttax'}) {
 
 867           $charttax = $transaction->[$i]->{'charttax'};
 
 869         if ($transaction->[$i]->{'amount'} > 0) {
 
 875       # Umwandlung von Umlauten und Sonderzeichen in erlaubte Zeichen bei Textfeldern
 
 876       foreach my $umlaut (keys(%umlaute)) {
 
 877         $transaction->[$haben]->{'invnumber'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
 
 878         $transaction->[$haben]->{'name'}      =~ s/${umlaut}/${umlaute{$umlaut}}/g;
 
 881       $transaction->[$haben]->{'invnumber'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
 
 882       $transaction->[$haben]->{'name'}      =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
 
 884       $transaction->[$haben]->{'invnumber'} =  substr($transaction->[$haben]->{'invnumber'}, 0, 12);
 
 885       $transaction->[$haben]->{'name'}      =  substr($transaction->[$haben]->{'name'}, 0, 30);
 
 886       $transaction->[$haben]->{'invnumber'} =~ s/\ *$//;
 
 887       $transaction->[$haben]->{'name'}      =~ s/\ *$//;
 
 889       if ($trans_lines >= 2) {
 
 891         $gegenkonto = "a" . trim_leading_zeroes($transaction->[$haben]->{'accno'});
 
 892         $konto      = "e" . trim_leading_zeroes($transaction->[$soll]->{'accno'});
 
 893         if ($transaction->[$haben]->{'invnumber'} ne "") {
 
 894           $belegfeld1 = "\xBD" . $transaction->[$haben]->{'invnumber'} . "\x1C";
 
 897         $datum .= &datetofour($transaction->[$haben]->{'transdate'}, 0);
 
 898         $waehrung = "\xB3" . "EUR" . "\x1C";
 
 899         if ($transaction->[$haben]->{'name'} ne "") {
 
 900           $buchungstext = "\x1E" . $transaction->[$haben]->{'name'} . "\x1C";
 
 902         if (($transaction->[$haben]->{'ustid'} // '') ne "") {
 
 903           $ustid = "\xBA" . $transaction->[$haben]->{'ustid'} . "\x1C";
 
 905         if (($transaction->[$haben]->{'duedate'} // '') ne "") {
 
 906           $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
 
 910       $umsatz       = $kne_file->format_amount(abs($umsatz), 0);
 
 911       $umsatzsumme += $umsatz;
 
 912       $kne_file->add_block("+" . $umsatz);
 
 914       # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
 
 915       # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
 
 916       # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
 
 917       # Skript angelegt werden.
 
 918       # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
 
 919       # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
 
 920       # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
 
 921       # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
 
 922       # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
 
 924       if (   ( $datevautomatik || $taxkey)
 
 925           && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
 
 926 #         $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
 
 927         $kne_file->add_block("\x6C${taxkey}");
 
 930       $kne_file->add_block($gegenkonto);
 
 931       $kne_file->add_block($belegfeld1);
 
 932       $kne_file->add_block($belegfeld2);
 
 933       $kne_file->add_block($datum);
 
 934       $kne_file->add_block($konto);
 
 935       $kne_file->add_block($buchungstext);
 
 936       $kne_file->add_block($ustid);
 
 937       $kne_file->add_block($waehrung . "\x79");
 
 940     my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
 
 942     $kne_file->add_block($mandantenendsumme);
 
 945     open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
 
 946     print(ED $kne_file->get_data());
 
 949     $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
 
 953   #Make EV Verwaltungsdatei
 
 954   my $ev_header = $self->make_ev_header($form, $fileno);
 
 955   my $ev_filename = $self->export_path . $evfile;
 
 956   push(@filenames, $evfile);
 
 957   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
 
 958   print(EV $ev_header);
 
 960   foreach my $file (@ed_versionset) {
 
 966   $self->add_filenames(@filenames);
 
 968   $main::lxdebug->leave_sub();
 
 970   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 
 973 sub kne_stammdatenexport {
 
 974   $main::lxdebug->enter_sub();
 
 979   $self->get_datev_stamm->{abrechnungsnr} = "99";
 
 983   my $filename    = "ED00000";
 
 989   my $remaining_bytes = 256;
 
 990   my $total_bytes     = 256;
 
 991   my $buchungssatz    = "";
 
 993   my $ed_filename = $self->export_path . $filename;
 
 994   push(@filenames, $filename);
 
 995   open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
 
 996   my $header = $self->make_kne_data_header($form);
 
 997   $remaining_bytes -= length($header);
 
1001   my (@where, @values) = ((), ());
 
1002   if ($self->accnofrom) {
 
1003     push @where, 'c.accno >= ?';
 
1004     push @values, $self->accnofrom;
 
1006   if ($self->accnoto) {
 
1007     push @where, 'c.accno <= ?';
 
1008     push @values, $self->accnoto;
 
1011   my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
 
1013   my $query     = qq|SELECT c.accno, c.description
 
1018   my $sth = $self->dbh->prepare($query);
 
1019   $sth->execute(@values) || $form->dberror($query);
 
1021   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
 
1022     if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
 
1023       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
 
1024       $buchungssatz .= "\x00" x $fuellzeichen;
 
1026       $total_bytes = ($blockcount) * 256;
 
1028     $buchungssatz .= "t" . $ref->{'accno'};
 
1029     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
 
1030     $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
 
1031     $ref->{'description'} = substr($ref->{'description'}, 0, 40);
 
1032     $ref->{'description'} =~ s/\ *$//;
 
1035         ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
 
1038       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
 
1039       $buchungssatz .= "\x00" x $fuellzeichen;
 
1041       $total_bytes = ($blockcount) * 256;
 
1043     $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
 
1044     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
 
1049   print(ED $buchungssatz);
 
1050   $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
 
1051   my $dateiende = "\x00" x $fuellzeichen;
 
1053   print(ED $dateiende);
 
1056   #Make EV Verwaltungsdatei
 
1058     $self->make_ed_versionset($header, $filename, $blockcount);
 
1060   my $ev_header = $self->make_ev_header($form, $fileno);
 
1061   my $ev_filename = $self->export_path . $evfile;
 
1062   push(@filenames, $evfile);
 
1063   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
 
1064   print(EV $ev_header);
 
1066   foreach my $file (@ed_versionset) {
 
1067     print(EV $ed_versionset[$file]);
 
1071   $self->add_filenames(@filenames);
 
1073   $main::lxdebug->leave_sub();
 
1075   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 
1079   clean_temporary_directories();
 
1090 SL::DATEV - kivitendo DATEV Export module
 
1094   use SL::DATEV qw(:CONSTANTS);
 
1096   my $startdate = DateTime->new(year => 2014, month => 9, day => 1);
 
1097   my $enddate   = DateTime->new(year => 2014, month => 9, day => 31);
 
1098   my $datev = SL::DATEV->new(
 
1099     exporttype => DATEV_ET_BUCHUNGEN,
 
1100     format     => DATEV_FORMAT_KNE,
 
1105   # To only export transactions from a specific trans_id: (from and to are ignored)
 
1106   my $invoice = SL::DB::Manager::Invoice->find_by( invnumber => '216' );
 
1107   my $datev = SL::DATEV->new(
 
1108     exporttype => DATEV_ET_BUCHUNGEN,
 
1109     format     => DATEV_FORMAT_KNE,
 
1110     trans_id   => $invoice->trans_id,
 
1113   my $datev = SL::DATEV->new(
 
1114     exporttype => DATEV_ET_STAMM,
 
1115     format     => DATEV_FORMAT_KNE,
 
1116     accnofrom  => $start_account_number,
 
1117     accnoto    => $end_account_number,
 
1120   # get or set datev stamm
 
1121   my $hashref = $datev->get_datev_stamm;
 
1122   $datev->save_datev_stamm($hashref);
 
1124   # manually clean up temporary directories older than 8 hours
 
1125   $datev->clean_temporary_directories;
 
1130   if ($datev->errors) {
 
1131     die join "\n", $datev->error;
 
1134   # get relevant data for saving the export:
 
1135   my $dl_token = $datev->download_token;
 
1136   my $path     = $datev->export_path;
 
1137   my @files    = $datev->filenames;
 
1139   # retrieving an export at a later time
 
1140   my $datev = SL::DATEV->new(
 
1141     download_token => $dl_token_from_user,
 
1144   my $path     = $datev->export_path;
 
1145   my @files    = glob("$path/*");
 
1149 This module implements the DATEV export standard. For usage see above.
 
1157 Generic constructor. See section attributes for information about what to pass.
 
1159 =item get_datev_stamm
 
1161 Loads DATEV Stammdaten and returns as hashref.
 
1163 =item save_datev_stamm HASHREF
 
1165 Saves DATEV Stammdaten from provided hashref.
 
1169 See L<CONSTANTS> for possible values
 
1171 =item has_exporttype
 
1173 Returns true if an exporttype has been set. Without exporttype most report functions won't work.
 
1177 Specifies the designated format of the export. Currently only KNE export is implemented.
 
1179 See L<CONSTANTS> for possible values
 
1183 Returns true if a format has been set. Without format most report functions won't work.
 
1185 =item download_token
 
1187 Returns a download token for this DATEV object.
 
1189 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
 
1193 Returns an export_path for this DATEV object.
 
1195 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
 
1199 Returns a list of filenames generated by this DATEV object. This only works if the files were generated during its lifetime, not if the object was created from a download_token.
 
1201 =item net_gross_differences
 
1203 If there were any net gross differences during calculation they will be collected here.
 
1205 =item sum_net_gross_differences
 
1207 Sum of all differences.
 
1209 =item clean_temporary_directories
 
1211 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.
 
1215 Returns a list of errors that occured. If no errors occured, the export was a success.
 
1219 Exports data. You have to have set L<exporttype> and L<format> or an error will
 
1220 occur. OBE exports are currently not implemented.
 
1226 This is a list of attributes set in either the C<new> or a method of the same name.
 
1232 Set a database handle to use in the process. This allows for an export to be
 
1233 done on a transaction in progress without committing first.
 
1237 See L<CONSTANTS> for possible values. This MUST be set before export is called.
 
1241 See L<CONSTANTS> for possible values. This MUST be set before export is called.
 
1243 =item download_token
 
1245 Can be set on creation to retrieve a prior export for download.
 
1251 Set boundary dates for the export. Unless a trans_id is passed these MUST be
 
1252 set for the export to work.
 
1256 To check only one gl/ar/ap transaction, pass the trans_id. The attributes
 
1257 L<from> and L<to> are currently still needed for the query to be assembled
 
1264 Set boundary account numbers for the export. Only useful for a stammdaten export.
 
1270 =head2 Supplied to L<exporttype>
 
1274 =item DATEV_ET_BUCHUNGEN
 
1276 =item DATEV_ET_STAMM
 
1280 =head2 Supplied to L<format>.
 
1284 =item DATEV_FORMAT_KNE
 
1286 =item DATEV_FORMAT_OBE
 
1290 =head1 ERROR HANDLING
 
1292 This module will die in the following cases:
 
1298 No or unrecognized exporttype or format was provided for an export
 
1302 OBE export was called, which is not yet implemented.
 
1310 Errors that occur during th actual export will be collected in L<errors>. The following types can occur at the moment:
 
1316 C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occur.
 
1320 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).>  This error occurs if a
 
1321 transaction could not be reliably sorted out, or had rounding errors above the acceptable threshold.
 
1325 =head1 BUGS AND CAVEATS
 
1331 Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to in order to get a working export.
 
1335 OBE export is currently not implemented.
 
1341 - handling of export_path and download token is a bit dodgy, clean that up.
 
1345 L<SL::DATEV::KNEFile>
 
1349 Philip Reetz E<lt>p.reetz@linet-services.deE<gt>,
 
1351 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
 
1353 Jan Büren E<lt>jan@lx-office-hosting.deE<gt>,
 
1355 Geoffrey Richardson E<lt>information@lx-office-hosting.deE<gt>,
 
1357 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,