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>,