X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FDATEV.pm;h=508ed958d50c0712d036e879b22aeb63231a5cb0;hb=5547891f42ef3b13d477eba65bb0537ece1d703c;hp=d6dd7c629f672bc35d81d85e220f7948688d2b2e;hpb=40d52f5031f6e2344a0e1a8d420b5d22ccca5ea8;p=kivitendo-erp.git diff --git a/SL/DATEV.pm b/SL/DATEV.pm index d6dd7c629..508ed958d 100644 --- a/SL/DATEV.pm +++ b/SL/DATEV.pm @@ -26,10 +26,75 @@ package DATEV; +use utf8; +use strict; + use SL::DBUtils; use SL::DATEV::KNEFile; +use SL::Taxkeys; use Data::Dumper; +use File::Path; +use List::Util qw(max); +use Time::HiRes qw(gettimeofday); + +sub _get_export_path { + $main::lxdebug->enter_sub(); + + my ($a, $b) = gettimeofday(); + my $path = get_path_for_download_token("${a}-${b}-${$}"); + + mkpath($path) unless (-d $path); + + $main::lxdebug->leave_sub(); + + return $path; +} + +sub get_path_for_download_token { + $main::lxdebug->enter_sub(); + + my $token = shift; + my $path; + + if ($token =~ m|^(\d+)-(\d+)-(\d+)$|) { + $path = "${main::userspath}/datev-export-${1}-${2}-${3}"; + } + + $main::lxdebug->leave_sub(); + + return $path; +} + +sub get_download_token_for_path { + $main::lxdebug->enter_sub(); + + my $path = shift; + my $token; + + if ($path =~ m|.*datev-export-(\d+)-(\d+)-(\d+)/?$|) { + $token = "${1}-${2}-${3}"; + } + + $main::lxdebug->leave_sub(); + + return $token; +} + +sub clean_temporary_directories { + $main::lxdebug->enter_sub(); + + foreach my $path (glob "${main::userspath}/datev-export-*") { + next unless (-d $path); + + my $mtime = (stat($path))[9]; + next if ((time() - $mtime) < 8 * 60 * 60); + + rmtree $path; + } + + $main::lxdebug->leave_sub(); +} sub _fill { $main::lxdebug->enter_sub(); @@ -62,11 +127,11 @@ sub get_datev_stamm { # connect to database my $dbh = $form->dbconnect($myconfig); - $query = qq|SELECT * FROM datev|; - $sth = $dbh->prepare($query); + my $query = qq|SELECT * FROM datev|; + my $sth = $dbh->prepare($query); $sth->execute || $form->dberror($query); - my $ref = $sth->fetchrow_hashref(NAME_lc); + my $ref = $sth->fetchrow_hashref("NAME_lc"); map { $form->{$_} = $ref->{$_} } keys %$ref; @@ -83,7 +148,7 @@ sub save_datev_stamm { # connect to database my $dbh = $form->dbconnect_noauto($myconfig); - $query = qq|DELETE FROM datev|; + my $query = qq|DELETE FROM datev|; $dbh->do($query) || $form->dberror($query); $query = qq|INSERT INTO datev @@ -96,7 +161,7 @@ sub save_datev_stamm { . $dbh->quote($form->{mandantennr}) . qq|,| . $dbh->quote($form->{datentraegernr}) . qq|,| . $dbh->quote($form->{abrechnungsnr}) . qq|)|; - $sth = $dbh->prepare($query); + my $sth = $dbh->prepare($query); $sth->execute || $form->dberror($query); $sth->finish; @@ -109,17 +174,17 @@ sub kne_export { $main::lxdebug->enter_sub(); my ($self, $myconfig, $form) = @_; - my @rc; + my $result; if ($form->{exporttype} == 0) { - @rc = &kne_buchungsexport($myconfig, $form); + $result = kne_buchungsexport($myconfig, $form); } else { - @rc = &kne_stammdatenexport($myconfig, $form); + $result = kne_stammdatenexport($myconfig, $form); } $main::lxdebug->leave_sub(); - return @rc; + return $result; } sub obe_export { @@ -138,6 +203,9 @@ sub get_dates { $main::lxdebug->enter_sub(); my ($zeitraum, $monat, $quartal, $transdatefrom, $transdateto) = @_; + my ($fromto, $jahr, $leap); + + my $form = $main::form; $fromto = "transdate >= "; @@ -241,6 +309,14 @@ sub get_dates { return ($fromto, $jahr); } +sub _sign { + my $value = shift; + + return $value < 0 ? -1 + : $value > 0 ? 1 + : 0; +} + sub _get_transactions { $main::lxdebug->enter_sub(); @@ -251,60 +327,69 @@ sub _get_transactions { my $dbh = $form->get_standard_dbh($myconfig); + my ($notsplitindex); my @errors = (); + $form->{net_gross_differences} = []; + $form->{sum_net_gross_differences} = 0; + $fromto =~ s/transdate/ac\.transdate/g; - my %taxes = selectall_as_map($form, $dbh, qq|SELECT id, rate FROM tax|, 'id', 'rate'); + my $taxkeys = Taxkeys->new(); + my $filter = ''; # Useful for debugging purposes + + my %all_taxchart_ids = selectall_as_map($form, $dbh, qq|SELECT DISTINCT chart_id, TRUE AS is_set FROM tax|, 'chart_id', 'is_set'); my $query = - qq|SELECT ac.oid, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey, + qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey, ar.invnumber, ar.duedate, ar.amount as umsatz, ct.name, - c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, - t.chart_id, t.rate, t.id AS taxid, t.taxkey AS taxtaxkey - FROM acc_trans ac,ar ar, customer ct, chart c - LEFT JOIN tax t ON (t.chart_id = c.id) - WHERE $fromto - AND (ac.trans_id = ar.id) - AND (ac.trans_id = ar.id) - AND (ar.customer_id = ct.id) - AND (ac.chart_id = c.id) + c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link, + ar.invoice + FROM acc_trans ac + LEFT JOIN ar ON (ac.trans_id = ar.id) + LEFT JOIN customer ct ON (ar.customer_id = ct.id) + LEFT JOIN chart c ON (ac.chart_id = c.id) + WHERE (ar.id IS NOT NULL) + AND $fromto + $filter UNION ALL - SELECT ac.oid, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey, + SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey, ap.invnumber, ap.duedate, ap.amount as umsatz, ct.name, - c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, - t.chart_id, t.rate, t.id AS taxid, t.taxkey AS taxtaxkey - FROM acc_trans ac, ap ap, vendor ct, chart c - LEFT JOIN tax t ON (t.chart_id = c.id) - WHERE $fromto - AND (ac.trans_id = ap.id) - AND (ap.vendor_id = ct.id) - AND (ac.chart_id = c.id) + c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link, + ap.invoice + FROM acc_trans ac + LEFT JOIN ap ON (ac.trans_id = ap.id) + LEFT JOIN vendor ct ON (ap.vendor_id = ct.id) + LEFT JOIN chart c ON (ac.chart_id = c.id) + WHERE (ap.id IS NOT NULL) + AND $fromto + $filter UNION ALL - SELECT ac.oid, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey, + SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey, gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz, gl.description AS name, - c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, - t.chart_id, t.rate, t.id AS taxid, t.taxkey AS taxtaxkey - FROM acc_trans ac, gl gl, chart c - LEFT JOIN tax t ON (t.chart_id = c.id) - WHERE $fromto - AND (ac.trans_id = gl.id) - AND (ac.chart_id = c.id) + c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link, + FALSE AS invoice + FROM acc_trans ac + LEFT JOIN gl ON (ac.trans_id = gl.id) + LEFT JOIN chart c ON (ac.chart_id = c.id) + WHERE (gl.id IS NOT NULL) + AND $fromto + $filter - ORDER BY trans_id, oid|; + ORDER BY trans_id, acc_trans_id|; - my $sth = prepare_execute_query($form, $dbh, $query); + my $sth = prepare_execute_query($form, $dbh, $query); + $form->{DATEV} = []; - my @splits; my $counter = 0; - while (my $ref = $sth->fetchrow_hashref(NAME_lc)) { + while (my $ref = $sth->fetchrow_hashref("NAME_lc")) { $counter++; if (($counter % 500) == 0) { print("$counter "); @@ -316,15 +401,38 @@ sub _get_transactions { my $firstrun = 1; while (abs($count) > 0.01 || $firstrun) { - my $ref2 = $sth->fetchrow_hashref(NAME_lc); + my $ref2 = $sth->fetchrow_hashref("NAME_lc"); last unless ($ref2); + if ($ref2->{trans_id} != $trans->[0]->{trans_id}) { + $form->error("Unbalanced ledger! old trans_id " . $trans->[0]->{trans_id} . " new trans_id " . $ref2->{trans_id} . " count $count"); + ::end_of_request(); + } + push @{ $trans }, $ref2; $count += $ref2->{amount}; $firstrun = 0; } + foreach my $i (0 .. scalar(@{ $trans }) - 1) { + my $ref = $trans->[$i]; + my $prev_ref = 0 < $i ? $trans->[$i - 1] : undef; + if ( $all_taxchart_ids{$ref->{id}} + && ($ref->{link} =~ m/(?:AP_tax|AR_tax)/) + && ( ($prev_ref && $prev_ref->{taxkey} && (_sign($ref->{amount}) == _sign($prev_ref->{amount}))) + || $ref->{invoice})) { + $ref->{is_tax} = 1; + } + + if ( !$ref->{invoice} + && $ref->{is_tax} + && !($prev_ref->{is_tax}) + && (_sign($ref->{amount}) == _sign($prev_ref->{amount}))) { + $trans->[$i - 1]->{tax_amount} = $ref->{amount}; + } + } + my %taxid_taxkeys = (); my $absumsatz = 0; if (scalar(@{$trans}) <= 2) { @@ -337,15 +445,15 @@ sub _get_transactions { $absumsatz = $trans->[$j]->{'amount'}; $notsplitindex = $j; } - if (($trans->[$j]->{'taxtaxkey'}) && ($trans->[$j]->{'taxid'})) { - $taxid_taxkeys{$trans->[$j]->{'taxtaxkey'}} = $trans->[$j]->{'taxid'}; - } } - my $ml = ($trans->[0]->{'umsatz'} > 0) ? 1 : -1; + my $ml = ($trans->[0]->{'umsatz'} > 0) ? 1 : -1; + my $rounding_error = 0; + my @taxed; + for my $j (0 .. (scalar(@{$trans}) - 1)) { if ( ($j != $notsplitindex) - && ($trans->[$j]->{'chart_id'} eq "") + && !$trans->[$j]->{is_tax} && ( $trans->[$j]->{'taxkey'} eq "" || $trans->[$j]->{'taxkey'} eq "0" || $trans->[$j]->{'taxkey'} eq "1" @@ -359,27 +467,85 @@ sub _get_transactions { $new_trans{'umsatz'} = abs($trans->[$j]->{'amount'}) * $ml; $trans->[$j]->{'umsatz'} = abs($trans->[$j]->{'amount'}) * $ml; - push @splits, [ \%new_trans, $trans->[$j] ]; - push @{ $form->{DATEV} }, $splits[-1]; + push @{ $form->{DATEV} }, [ \%new_trans, $trans->[$j] ]; - } elsif (($j != $notsplitindex) && ($trans->[$j]->{'chart_id'} eq "")) { - $absumsatz += ($trans->[$j]->{'amount'} * (1 + $taxes{ $taxid_taxkeys{$trans->[$j]->{'taxkey'}} })); + } elsif (($j != $notsplitindex) && !$trans->[$j]->{is_tax}) { + my %tax_info = $taxkeys->get_full_tax_info('transdate' => $trans->[$j]->{transdate}); my %new_trans = (); map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] }; - my $tax_rate = 1 + $taxes{ $taxid_taxkeys{$trans->[$j]->{'taxkey'}} }; - $new_trans{'amount'} = $form->round_amount(($trans->[$j]->{'amount'} * $tax_rate * -1), 2); - $new_trans{'umsatz'} = abs($form->round_amount(($trans->[$j]->{'amount'} * $tax_rate), 2)) * $ml; - $trans->[$j]->{'umsatz'} = abs($form->round_amount(($trans->[$j]->{'amount'} * $tax_rate), 2)) * $ml; + my $tax_rate = $tax_info{taxkeys}->{ $trans->[$j]->{'taxkey'} }->{taxrate}; + $new_trans{'net_amount'} = $trans->[$j]->{'amount'} * -1; + $new_trans{'tax_rate'} = 1 + $tax_rate; - push @splits, [ \%new_trans, $trans->[$j] ]; - push @{ $form->{DATEV} }, $splits[-1]; + if (!$trans->[$j]->{'invoice'}) { + $new_trans{'amount'} = $form->round_amount(-1 * ($trans->[$j]->{amount} + $trans->[$j]->{tax_amount}), 2); + $new_trans{'umsatz'} = abs($new_trans{'amount'}) * $ml; + $trans->[$j]->{'umsatz'} = $new_trans{'umsatz'}; + $absumsatz += -1 * $new_trans{'amount'}; + + } else { + my $unrounded = $trans->[$j]->{'amount'} * (1 + $tax_rate) * -1 + $rounding_error; + my $rounded = $form->round_amount($unrounded, 2); + + $rounding_error = $unrounded - $rounded; + $new_trans{'amount'} = $rounded; + $new_trans{'umsatz'} = abs($rounded) * $ml; + $trans->[$j]->{'umsatz'} = $new_trans{umsatz}; + $absumsatz -= $rounded; + } + + push @{ $form->{DATEV} }, [ \%new_trans, $trans->[$j] ]; + push @taxed, $form->{DATEV}->[-1]; } } - if (abs($absumsatz) > 0.01) { + my $idx = 0; + my $correction = 0; + while ((abs($absumsatz) >= 0.01) && (abs($absumsatz) < 1.00)) { + if ($idx >= scalar @taxed) { + last if (!$correction); + + $correction = 0; + $idx = 0; + } + + my $transaction = $taxed[$idx]->[0]; + + my $old_amount = $transaction->{amount}; + my $old_correction = $correction; + my @possible_diffs; + + if (!$transaction->{diff}) { + @possible_diffs = (0.01, -0.01); + } else { + @possible_diffs = ($transaction->{diff}); + } + + foreach my $diff (@possible_diffs) { + my $net_amount = $form->round_amount(($transaction->{amount} + $diff) / $transaction->{tax_rate}, 2); + next if ($net_amount != $transaction->{net_amount}); + + $transaction->{diff} = $diff; + $transaction->{amount} += $diff; + $transaction->{umsatz} += $diff; + $absumsatz -= $diff; + $correction = 1; + + last; + } + + $idx++; + } + + $absumsatz = $form->round_amount($absumsatz, 2); + if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) { push @errors, "Datev-Export fehlgeschlagen! Bei Transaktion $trans->[0]->{trans_id} ($absumsatz)\n"; + + } elsif (abs($absumsatz) >= 0.01) { + push @{ $form->{net_gross_differences} }, $absumsatz; + $form->{sum_net_gross_differences} += $absumsatz; } } @@ -394,6 +560,7 @@ sub make_kne_data_header { $main::lxdebug->enter_sub(); my ($myconfig, $form, $fromto, $start_jahr) = @_; + my ($primanota); my $jahr = $start_jahr; if (!$jahr) { @@ -403,7 +570,7 @@ sub make_kne_data_header { #Header my $header = "\x1D\x181"; - $header .= _fill($form->{datentraegernr}, 3, '0'); + $header .= _fill($form->{datentraegernr}, 3, ' ', 'left'); $header .= ($fromto) ? "11" : "13"; # Anwendungsnummer $header .= _fill($form->{dfvkz}, 2, '0'); $header .= _fill($form->{beraternr}, 7, '0'); @@ -478,7 +645,7 @@ sub datetofour { my ($date, $six) = @_; - ($day, $month, $year) = split(/\./, $date); + my ($day, $month, $year) = split(/\./, $date); if ($day =~ /^0/) { $day = substr($day, 1, 1); @@ -520,7 +687,7 @@ sub make_ed_versionset { if ($fromto ne "") { $versionset .= "0000" . substr($header, 28, 19); } else { - $datum = " " x 16; + my $datum = " " x 16; $versionset .= $datum . "001" . substr($header, 28, 4); } @@ -560,10 +727,10 @@ sub kne_buchungsexport { my @filenames; - my $export_path = $main::userspath . "/"; + my $export_path = _get_export_path() . "/"; my $filename = "ED00000"; my $evfile = "EV01"; - my @ed_versionsets; + my @ed_versionset; my $fileno = 0; $form->header; @@ -573,7 +740,7 @@ sub kne_buchungsexport { Buchungssätze verarbeitet: |; - ($fromto, $start_jahr) = + my ($fromto, $start_jahr) = &get_dates($form->{zeitraum}, $form->{monat}, $form->{quartal}, $form->{transdatefrom}, $form->{transdateto}); @@ -585,14 +752,14 @@ sub kne_buchungsexport { $filename++; my $ed_filename = $export_path . $filename; push(@filenames, $filename); - $header = &make_kne_data_header($myconfig, $form, $fromto, $start_jahr); + my $header = &make_kne_data_header($myconfig, $form, $fromto, $start_jahr); my $kne_file = SL::DATEV::KNEFile->new(); $kne_file->add_block($header); while (scalar(@{ $form->{DATEV} }) > 0) { - $transaction = shift @{ $form->{DATEV} }; - $trans_lines = scalar(@{$transaction}); + my $transaction = shift @{ $form->{DATEV} }; + my $trans_lines = scalar(@{$transaction}); $counter++; if (($counter % 500) == 0) { print("$counter "); @@ -609,13 +776,15 @@ sub kne_buchungsexport { my $datevautomatik = 0; my $taxkey = 0; my $charttax = 0; - my %umlaute = ('ä' => 'ae', - 'ö' => 'oe', - 'ü' => 'ue', - 'Ä' => 'Ae', - 'Ö' => 'Oe', - 'Ü' => 'Ue', - 'ß' => 'sz'); + my ($haben, $soll); + my $iconv = $::locale->{iconv_utf8}; + my %umlaute = ($iconv->convert('ä') => 'ae', + $iconv->convert('ö') => 'oe', + $iconv->convert('ü') => 'ue', + $iconv->convert('Ä') => 'Ae', + $iconv->convert('Ö') => 'Oe', + $iconv->convert('Ü') => 'Ue', + $iconv->convert('ß') => 'sz'); for (my $i = 0; $i < $trans_lines; $i++) { if ($trans_lines == 2) { if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) { @@ -635,10 +804,7 @@ sub kne_buchungsexport { if ($transaction->[$i]->{'charttax'}) { $charttax = $transaction->[$i]->{'charttax'}; } - if ( ($transaction->[$i]->{'id'} eq $transaction->[$i]->{'chart_id'}) - && ($trans_lines > 2)) { - undef($transaction->[$i]); - } elsif ($transaction->[$i]->{'amount'} > 0) { + if ($transaction->[$i]->{'amount'} > 0) { $haben = $i; } else { $soll = $i; @@ -646,7 +812,7 @@ sub kne_buchungsexport { } # Umwandlung von Umlauten und Sonderzeichen in erlaubte Zeichen bei Textfeldern - foreach $umlaut (keys(%umlaute)) { + foreach my $umlaut (keys(%umlaute)) { $transaction->[$haben]->{'invnumber'} =~ s/${umlaut}/${umlaute{$umlaut}}/g; $transaction->[$haben]->{'name'} =~ s/${umlaut}/${umlaute{$umlaut}}/g; } @@ -683,7 +849,8 @@ sub kne_buchungsexport { if ( ( $datevautomatik || $taxkey) && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) { - $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4")); +# $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4")); + $kne_file->add_block("\x6C${taxkey}"); } $kne_file->add_block($gegenkonto); @@ -709,13 +876,13 @@ sub kne_buchungsexport { } #Make EV Verwaltungsdatei - $ev_header = &make_ev_header($form, $fileno); - $ev_filename = $export_path . $evfile; + my $ev_header = &make_ev_header($form, $fileno); + my $ev_filename = $export_path . $evfile; push(@filenames, $evfile); open(EV, "> $ev_filename") or die "can't open outputfile: EV01\n"; print(EV $ev_header); - foreach $file (@ed_versionset) { + foreach my $file (@ed_versionset) { print(EV $ed_versionset[$file]); } close(EV); @@ -724,7 +891,7 @@ sub kne_buchungsexport { ### $main::lxdebug->leave_sub(); - return @filenames; + return { 'download_token' => get_download_token_for_path($export_path), 'filenames' => \@filenames }; } sub kne_stammdatenexport { @@ -741,10 +908,10 @@ sub kne_stammdatenexport { my @filenames; - my $export_path = $main::userspath . "/"; + my $export_path = _get_export_path() . "/"; my $filename = "ED00000"; my $evfile = "EV01"; - my @ed_versionsets; + my @ed_versionset; my $fileno = 1; my $i = 0; my $blockcount = 1; @@ -755,9 +922,12 @@ sub kne_stammdatenexport { my $ed_filename = $export_path . $filename; push(@filenames, $filename); open(ED, "> $ed_filename") or die "can't open outputfile: $!\n"; - $header = &make_kne_data_header($myconfig, $form, ""); + my $header = &make_kne_data_header($myconfig, $form, ""); $remaining_bytes -= length($header); + my $fuellzeichen; + our $fromto; + # connect to database my $dbh = $form->dbconnect($myconfig); @@ -781,7 +951,7 @@ sub kne_stammdatenexport { my $sth = $dbh->prepare($query); $sth->execute(@values) || $form->dberror($query); - while (my $ref = $sth->fetchrow_hashref(NAME_lc)) { + while (my $ref = $sth->fetchrow_hashref("NAME_lc")) { if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) { $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header)); $buchungssatz .= "\x00" x $fuellzeichen; @@ -811,7 +981,7 @@ sub kne_stammdatenexport { print(ED $header); print(ED $buchungssatz); $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256); - $dateiende = "\x00" x $fuellzeichen; + my $dateiende = "\x00" x $fuellzeichen; print(ED "z"); print(ED $dateiende); close(ED); @@ -820,13 +990,13 @@ sub kne_stammdatenexport { $ed_versionset[0] = &make_ed_versionset($header, $filename, $blockcount, $fromto); - $ev_header = &make_ev_header($form, $fileno); - $ev_filename = $export_path . $evfile; + my $ev_header = &make_ev_header($form, $fileno); + my $ev_filename = $export_path . $evfile; push(@filenames, $evfile); open(EV, "> $ev_filename") or die "can't open outputfile: EV01\n"; print(EV $ev_header); - foreach $file (@ed_versionset) { + foreach my $file (@ed_versionset) { print(EV $ed_versionset[$file]); } close(EV); @@ -839,7 +1009,7 @@ sub kne_stammdatenexport { $main::lxdebug->leave_sub(); - return @filenames; + return { 'download_token' => get_download_token_for_path($export_path), 'filenames' => \@filenames }; } 1;