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, NULL as ustid,
 
 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;
 
 815       my $iconv          = $::locale->{iconv_utf8};
 
 816       my %umlaute = ($iconv->convert('ä') => 'ae',
 
 817                      $iconv->convert('ö') => 'oe',
 
 818                      $iconv->convert('ü') => 'ue',
 
 819                      $iconv->convert('Ä') => 'Ae',
 
 820                      $iconv->convert('Ö') => 'Oe',
 
 821                      $iconv->convert('Ü') => 'Ue',
 
 822                      $iconv->convert('ß') => 'sz');
 
 823       for (my $i = 0; $i < $trans_lines; $i++) {
 
 824         if ($trans_lines == 2) {
 
 825           if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
 
 826             $umsatz = $transaction->[$i]->{'amount'};
 
 829           if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
 
 830             $umsatz = $transaction->[$i]->{'umsatz'};
 
 833         if ($transaction->[$i]->{'datevautomatik'}) {
 
 836         if ($transaction->[$i]->{'taxkey'}) {
 
 837           $taxkey = $transaction->[$i]->{'taxkey'};
 
 839         if ($transaction->[$i]->{'charttax'}) {
 
 840           $charttax = $transaction->[$i]->{'charttax'};
 
 842         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]->{'ustid'} ne "") {
 
 876           $ustid = "\xBA" . $transaction->[$haben]->{'ustid'} . "\x1C";
 
 878         if ($transaction->[$haben]->{'duedate'} ne "") {
 
 879           $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
 
 883       $umsatz       = $kne_file->format_amount(abs($umsatz), 0);
 
 884       $umsatzsumme += $umsatz;
 
 885       $kne_file->add_block("+" . $umsatz);
 
 887       # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
 
 888       # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
 
 889       # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
 
 890       # Skript angelegt werden.
 
 891       # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
 
 892       # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
 
 893       # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
 
 894       # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
 
 895       # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
 
 897       if (   ( $datevautomatik || $taxkey)
 
 898           && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
 
 899 #         $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
 
 900         $kne_file->add_block("\x6C${taxkey}");
 
 903       $kne_file->add_block($gegenkonto);
 
 904       $kne_file->add_block($belegfeld1);
 
 905       $kne_file->add_block($belegfeld2);
 
 906       $kne_file->add_block($datum);
 
 907       $kne_file->add_block($konto);
 
 908       $kne_file->add_block($buchungstext);
 
 909       $kne_file->add_block($ustid);
 
 910       $kne_file->add_block($waehrung . "\x79");
 
 913     my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
 
 915     $kne_file->add_block($mandantenendsumme);
 
 918     open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
 
 919     print(ED $kne_file->get_data());
 
 922     $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
 
 926   #Make EV Verwaltungsdatei
 
 927   my $ev_header = $self->make_ev_header($form, $fileno);
 
 928   my $ev_filename = $self->export_path . $evfile;
 
 929   push(@filenames, $evfile);
 
 930   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
 
 931   print(EV $ev_header);
 
 933   foreach my $file (@ed_versionset) {
 
 934     print(EV $ed_versionset[$file]);
 
 939   $self->add_filenames(@filenames);
 
 941   $main::lxdebug->leave_sub();
 
 943   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 
 946 sub kne_stammdatenexport {
 
 947   $main::lxdebug->enter_sub();
 
 952   $self->get_datev_stamm->{abrechnungsnr} = "99";
 
 956   my $filename    = "ED00000";
 
 962   my $remaining_bytes = 256;
 
 963   my $total_bytes     = 256;
 
 964   my $buchungssatz    = "";
 
 966   my $ed_filename = $self->export_path . $filename;
 
 967   push(@filenames, $filename);
 
 968   open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
 
 969   my $header = $self->make_kne_data_header($form);
 
 970   $remaining_bytes -= length($header);
 
 974   my (@where, @values) = ((), ());
 
 975   if ($self->accnofrom) {
 
 976     push @where, 'c.accno >= ?';
 
 977     push @values, $self->accnofrom;
 
 979   if ($self->accnoto) {
 
 980     push @where, 'c.accno <= ?';
 
 981     push @values, $self->accnoto;
 
 984   my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
 
 986   my $query     = qq|SELECT c.accno, c.description
 
 991   my $sth = $self->dbh->prepare($query);
 
 992   $sth->execute(@values) || $form->dberror($query);
 
 994   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
 
 995     if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
 
 996       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
 
 997       $buchungssatz .= "\x00" x $fuellzeichen;
 
 999       $total_bytes = ($blockcount) * 256;
 
1001     $buchungssatz .= "t" . $ref->{'accno'};
 
1002     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
 
1003     $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
 
1004     $ref->{'description'} = substr($ref->{'description'}, 0, 40);
 
1005     $ref->{'description'} =~ s/\ *$//;
 
1008         ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
 
1011       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
 
1012       $buchungssatz .= "\x00" x $fuellzeichen;
 
1014       $total_bytes = ($blockcount) * 256;
 
1016     $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
 
1017     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
 
1022   print(ED $buchungssatz);
 
1023   $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
 
1024   my $dateiende = "\x00" x $fuellzeichen;
 
1026   print(ED $dateiende);
 
1029   #Make EV Verwaltungsdatei
 
1031     $self->make_ed_versionset($header, $filename, $blockcount);
 
1033   my $ev_header = $self->make_ev_header($form, $fileno);
 
1034   my $ev_filename = $self->export_path . $evfile;
 
1035   push(@filenames, $evfile);
 
1036   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
 
1037   print(EV $ev_header);
 
1039   foreach my $file (@ed_versionset) {
 
1040     print(EV $ed_versionset[$file]);
 
1044   $self->add_filenames(@filenames);
 
1046   $main::lxdebug->leave_sub();
 
1048   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 
1052   clean_temporary_directories();
 
1063 SL::DATEV - kivitendo DATEV Export module
 
1067   use SL::DATEV qw(:CONSTANTS);
 
1069   my $datev = SL::DATEV->new(
 
1070     exporttype => DATEV_ET_BUCHUNGEN,
 
1071     format     => DATEV_FORMAT_KNE,
 
1076   my $datev = SL::DATEV->new(
 
1077     exporttype => DATEV_ET_STAMM,
 
1078     format     => DATEV_FORMAT_KNE,
 
1079     accnofrom  => $start_account_number,
 
1080     accnoto    => $end_account_number,
 
1083   # get or set datev stamm
 
1084   my $hashref = $datev->get_datev_stamm;
 
1085   $datev->save_datev_stamm($hashref);
 
1087   # manually clean up temporary directories
 
1088   $datev->clean_temporary_directories;
 
1093   if ($datev->errors) {
 
1094     die join "\n", $datev->error;
 
1097   # get relevant data for saving the export:
 
1098   my $dl_token = $datev->download_token;
 
1099   my $path     = $datev->export_path;
 
1100   my @files    = $datev->filenames;
 
1102   # retrieving an export at a later time
 
1103   my $datev = SL::DATEV->new(
 
1104     download_token => $dl_token_from_user,
 
1107   my $path     = $datev->export_path;
 
1108   my @files    = glob("$path/*");
 
1112 This module implements the DATEV export standard. For usage see above.
 
1120 Generic constructor. See section attributes for information about hat to pass.
 
1122 =item get_datev_stamm
 
1124 Loads DATEV Stammdaten and returns as hashref.
 
1126 =item save_datev_stamm HASHREF
 
1128 Saves DATEV Stammdaten from provided hashref.
 
1132 See L<CONSTANTS> for possible values
 
1134 =item has_exporttype
 
1136 Returns true if an exporttype has been set. Without exporttype most report functions won't work.
 
1140 Specifies the designated format of the export. Currently only KNE export is implemented.
 
1142 See L<CONSTANTS> for possible values
 
1146 Returns true if a format has been set. Without format most report functions won't work.
 
1148 =item download_token
 
1150 Returns a download token for this DATEV object.
 
1152 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
 
1156 Returns an export_path for this DATEV object.
 
1158 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
 
1162 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.
 
1164 =item net_gross_differences
 
1166 If there were any net gross differences during calculation they will be collected here.
 
1168 =item sum_net_gross_differences
 
1170 Sum of all differences.
 
1172 =item clean_temporary_directories
 
1174 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.
 
1178 Returns a list of errors that occured. If no errors occured, the export was a success.
 
1182 Exports data. You have to have set L<exporttype> and L<format> or an error will
 
1183 occur. OBE exports are currently not implemented.
 
1189 This is a list of attributes set in either the C<new> or a method of the same name.
 
1195 Set a database handle to use in the process. This allows for an export to be
 
1196 done on a transaction in progress without committing first.
 
1200 See L<CONSTANTS> for possible values. This MUST be set before export is called.
 
1204 See L<CONSTANTS> for possible values. This MUST be set before export is called.
 
1206 =item download_token
 
1208 Can be set on creation to retrieve a prior export for download.
 
1214 Set boundary dates for the export. Currently thse MUST be set for the export to work.
 
1220 Set boundary account numbers for the export. Only useful for a stammdaten export.
 
1226 =head2 Supplied to L<exporttype>
 
1230 =item DATEV_ET_BUCHUNGEN
 
1232 =item DATEV_ET_STAMM
 
1236 =head2 Supplied to L<format>.
 
1240 =item DATEV_FORMAT_KNE
 
1242 =item DATEV_FORMAT_OBE
 
1246 =head1 ERROR HANDLING
 
1248 This module will die in the following cases:
 
1254 No or unrecognized exporttype or format was provided for an export
 
1258 OBE rxport was called, which is not yet implemented.
 
1266 Errors that occur during th actual export will be collected in L<errors>. The following types can occur at the moment:
 
1272 C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occur.
 
1276 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).>  This error occurs if a
 
1277 transaction could not be reliably sorted out, or had rounding errors over the acceptable threshold.
 
1281 =head1 BUGS AND CAVEATS
 
1287 Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to to get a working export.
 
1291 OBE export is currently not implemented.
 
1297 - handling of export_path and download token is a bit dodgy, clean that up.
 
1301 L<SL::DATEV::KNEFile>
 
1305 Philip Reetz E<lt>p.reetz@linet-services.deE<gt>,
 
1307 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
 
1309 Jan Büren E<lt>jan@lx-office-hosting.deE<gt>,
 
1311 Geoffrey Richardson E<lt>information@lx-office-hosting.deE<gt>,
 
1313 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,