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->{accnofrom} = $_[0];
203 return $self->{accnofrom};
210 $self->{accnoto} = $_[0];
213 return $self->{accnoto};
221 $self->{dbh} = $_[0];
222 $self->{provided_dbh} = 1;
225 $self->{dbh} ||= $::form->get_standard_dbh;
232 sub clean_temporary_directories {
233 $::lxdebug->enter_sub;
235 foreach my $path (glob($::lx_office_conf{paths}->{userspath} . "/datev-export-*")) {
236 next unless -d $path;
238 my $mtime = (stat($path))[9];
239 next if ((time() - $mtime) < 8 * 60 * 60);
244 $::lxdebug->leave_sub;
248 $main::lxdebug->enter_sub();
251 my $field_len = shift;
252 my $fill_char = shift;
253 my $alignment = shift || 'right';
255 my $text_len = length $text;
257 if ($field_len < $text_len) {
258 $text = substr $text, 0, $field_len;
260 } elsif ($field_len > $text_len) {
261 my $filler = ($fill_char) x ($field_len - $text_len);
262 $text = $alignment eq 'right' ? $filler . $text : $text . $filler;
265 $main::lxdebug->leave_sub();
270 sub get_datev_stamm {
271 return $_[0]{stamm} ||= selectfirst_hashref_query($::form, $_[0]->dbh, 'SELECT * FROM datev');
274 sub save_datev_stamm {
275 my ($self, $data) = @_;
277 do_query($::form, $self->dbh, 'DELETE FROM datev');
279 my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
281 my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
282 do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
284 $self->dbh->commit unless $self->provided_dbh;
291 die 'no format set!' unless $self->has_format;
293 if ($self->format == DATEV_FORMAT_KNE) {
294 $result = $self->kne_export;
295 } elsif ($self->format == DATEV_FORMAT_OBE) {
296 $result = $self->obe_export;
298 die 'unrecognized export format';
308 die 'no exporttype set!' unless $self->has_exporttype;
310 if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
311 $result = $self->kne_buchungsexport;
312 } elsif ($self->exporttype == DATEV_ET_STAMM) {
313 $result = $self->kne_stammdatenexport;
315 die 'unrecognized exporttype';
322 die 'not yet implemented';
328 return unless $self->from && $self->to;
330 return "transdate >= '" . $self->from->to_lxoffice . "' and transdate <= '" . $self->to->to_lxoffice . "'";
337 sub _get_transactions {
338 $main::lxdebug->enter_sub();
341 my $progress_callback = shift || sub {};
343 my $form = $main::form;
347 $fromto =~ s/transdate/ac\.transdate/g;
349 my $taxkeys = Taxkeys->new();
350 my $filter = ''; # Useful for debugging purposes
352 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');
355 qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey,
356 ar.invnumber, ar.duedate, ar.amount as umsatz,
358 c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link,
361 LEFT JOIN ar ON (ac.trans_id = ar.id)
362 LEFT JOIN customer ct ON (ar.customer_id = ct.id)
363 LEFT JOIN chart c ON (ac.chart_id = c.id)
364 WHERE (ar.id IS NOT NULL)
370 SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey,
371 ap.invnumber, ap.duedate, ap.amount as umsatz,
373 c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link,
376 LEFT JOIN ap ON (ac.trans_id = ap.id)
377 LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
378 LEFT JOIN chart c ON (ac.chart_id = c.id)
379 WHERE (ap.id IS NOT NULL)
385 SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey,
386 gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz,
387 gl.description AS name,
388 c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link,
391 LEFT JOIN gl ON (ac.trans_id = gl.id)
392 LEFT JOIN chart c ON (ac.chart_id = c.id)
393 WHERE (gl.id IS NOT NULL)
397 ORDER BY trans_id, acc_trans_id|;
399 my $sth = prepare_execute_query($form, $self->dbh, $query);
403 while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
405 if (($counter % 500) == 0) {
406 $progress_callback->($counter);
409 my $trans = [ $ref ];
411 my $count = $ref->{amount};
413 my $subcent = abs($count) < 0.02;
415 while (abs($count) > 0.01 || $firstrun || ($subcent && abs($count) > 0.005)) {
416 my $ref2 = $sth->fetchrow_hashref("NAME_lc");
419 if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
420 $self->add_error("Unbalanced ledger! old trans_id " . $trans->[0]->{trans_id} . " new trans_id " . $ref2->{trans_id} . " count $count");
424 push @{ $trans }, $ref2;
426 $count += $ref2->{amount};
430 foreach my $i (0 .. scalar(@{ $trans }) - 1) {
431 my $ref = $trans->[$i];
432 my $prev_ref = 0 < $i ? $trans->[$i - 1] : undef;
433 if ( $all_taxchart_ids{$ref->{id}}
434 && ($ref->{link} =~ m/(?:AP_tax|AR_tax)/)
435 && ( ($prev_ref && $prev_ref->{taxkey} && (_sign($ref->{amount}) == _sign($prev_ref->{amount})))
436 || $ref->{invoice})) {
440 if ( !$ref->{invoice} # we have a non-invoice booking (=gl)
441 && $ref->{is_tax} # that has "is_tax" set
442 && !($prev_ref->{is_tax}) # previous line wasn't is_tax
443 && (_sign($ref->{amount}) == _sign($prev_ref->{amount}))) { # and sign same as previous sign
444 $trans->[$i - 1]->{tax_amount} = $ref->{amount};
448 my %taxid_taxkeys = ();
450 if (scalar(@{$trans}) <= 2) {
451 push @{ $self->{DATEV} }, $trans;
455 for my $j (0 .. (scalar(@{$trans}) - 1)) {
456 # for gl-bookings no split is allowed and there is no AR/AP account, so we always use the maximum value as a reference
457 # for ap/ar bookings we can always search for AR/AP in link and use that
458 if ( ( not $trans->[$j]->{'invoice'} and abs($trans->[$j]->{'amount'}) > abs($absumsatz) )
459 or ($trans->[$j]->{'invoice'} and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP'))) {
460 $absumsatz = $trans->[$j]->{'amount'};
465 my $ml = ($trans->[0]->{'umsatz'} > 0) ? 1 : -1;
466 my $rounding_error = 0;
469 for my $j (0 .. (scalar(@{$trans}) - 1)) {
470 if ( ($j != $notsplitindex)
471 && !$trans->[$j]->{is_tax}
472 && ( $trans->[$j]->{'taxkey'} eq ""
473 || $trans->[$j]->{'taxkey'} eq "0"
474 || $trans->[$j]->{'taxkey'} eq "1"
475 || $trans->[$j]->{'taxkey'} eq "10"
476 || $trans->[$j]->{'taxkey'} eq "11")) {
478 map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
480 $absumsatz += $trans->[$j]->{'amount'};
481 $new_trans{'amount'} = $trans->[$j]->{'amount'} * (-1);
482 $new_trans{'umsatz'} = abs($trans->[$j]->{'amount'}) * $ml;
483 $trans->[$j]->{'umsatz'} = abs($trans->[$j]->{'amount'}) * $ml;
485 push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
487 } elsif (($j != $notsplitindex) && !$trans->[$j]->{is_tax}) {
488 my %tax_info = $taxkeys->get_full_tax_info('transdate' => $trans->[$j]->{transdate});
491 map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
493 my $tax_rate = $tax_info{taxkeys}->{ $trans->[$j]->{'taxkey'} }->{taxrate};
494 $new_trans{'net_amount'} = $trans->[$j]->{'amount'} * -1;
495 $new_trans{'tax_rate'} = 1 + $tax_rate;
497 if (!$trans->[$j]->{'invoice'}) {
498 $new_trans{'amount'} = $form->round_amount(-1 * ($trans->[$j]->{amount} + $trans->[$j]->{tax_amount}), 2);
499 $new_trans{'umsatz'} = abs($new_trans{'amount'}) * $ml;
500 $trans->[$j]->{'umsatz'} = $new_trans{'umsatz'};
501 $absumsatz += -1 * $new_trans{'amount'};
504 my $unrounded = $trans->[$j]->{'amount'} * (1 + $tax_rate) * -1 + $rounding_error;
505 my $rounded = $form->round_amount($unrounded, 2);
507 $rounding_error = $unrounded - $rounded;
508 $new_trans{'amount'} = $rounded;
509 $new_trans{'umsatz'} = abs($rounded) * $ml;
510 $trans->[$j]->{'umsatz'} = $new_trans{umsatz};
511 $absumsatz -= $rounded;
514 push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
515 push @taxed, $self->{DATEV}->[-1];
521 while ((abs($absumsatz) >= 0.01) && (abs($absumsatz) < 1.00)) {
522 if ($idx >= scalar @taxed) {
523 last if (!$correction);
529 my $transaction = $taxed[$idx]->[0];
531 my $old_amount = $transaction->{amount};
532 my $old_correction = $correction;
535 if (!$transaction->{diff}) {
536 @possible_diffs = (0.01, -0.01);
538 @possible_diffs = ($transaction->{diff});
541 foreach my $diff (@possible_diffs) {
542 my $net_amount = $form->round_amount(($transaction->{amount} + $diff) / $transaction->{tax_rate}, 2);
543 next if ($net_amount != $transaction->{net_amount});
545 $transaction->{diff} = $diff;
546 $transaction->{amount} += $diff;
547 $transaction->{umsatz} += $diff;
557 $absumsatz = $form->round_amount($absumsatz, 2);
558 if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
559 $self->add_error("Datev-Export fehlgeschlagen! Bei Transaktion $trans->[0]->{trans_id} ($absumsatz)");
561 } elsif (abs($absumsatz) >= 0.01) {
562 $self->add_net_gross_differences($absumsatz);
568 $::lxdebug->leave_sub;
571 sub make_kne_data_header {
572 $main::lxdebug->enter_sub();
574 my ($self, $form) = @_;
577 my $stamm = $self->get_datev_stamm;
579 my $jahr = $self->from ? $self->from->year : DateTime->today->year;
582 my $header = "\x1D\x181";
583 $header .= _fill($stamm->{datentraegernr}, 3, ' ', 'left');
584 $header .= ($self->fromto) ? "11" : "13"; # Anwendungsnummer
585 $header .= _fill($stamm->{dfvkz}, 2, '0');
586 $header .= _fill($stamm->{beraternr}, 7, '0');
587 $header .= _fill($stamm->{mandantennr}, 5, '0');
588 $header .= _fill($stamm->{abrechnungsnr} . $jahr, 6, '0');
590 $header .= $self->from ? $self->from->strftime('%d%m%y') : '';
591 $header .= $self->to ? $self->to->strftime('%d%m%y') : '';
595 $header .= $primanota;
598 $header .= _fill($stamm->{passwort}, 4, '0');
599 $header .= " " x 16; # Anwendungsinfo
600 $header .= " " x 16; # Inputinfo
604 my $versionssatz = $self->exporttype == DATEV_ET_BUCHUNGEN ? "\xB5" . "1," : "\xB6" . "1,";
606 my $query = qq|SELECT accno FROM chart LIMIT 1|;
607 my $ref = selectfirst_hashref_query($form, $self->dbh, $query);
609 $versionssatz .= length $ref->{accno};
610 $versionssatz .= ",";
611 $versionssatz .= length $ref->{accno};
612 $versionssatz .= ",SELF" . "\x1C\x79";
614 $header .= $versionssatz;
616 $main::lxdebug->leave_sub();
622 $main::lxdebug->enter_sub();
624 my ($date, $six) = @_;
626 my ($day, $month, $year) = split(/\./, $date);
629 $day = substr($day, 1, 1);
631 if (length($month) < 2) {
632 $month = "0" . $month;
634 if (length($year) > 2) {
635 $year = substr($year, -2, 2);
639 $date = $day . $month . $year;
641 $date = $day . $month;
644 $main::lxdebug->leave_sub();
649 sub trim_leading_zeroes {
657 sub make_ed_versionset {
658 $main::lxdebug->enter_sub();
660 my ($self, $header, $filename, $blockcount) = @_;
662 my $versionset = "V" . substr($filename, 2, 5);
663 $versionset .= substr($header, 6, 22);
666 $versionset .= "0000" . substr($header, 28, 19);
668 my $datum = " " x 16;
669 $versionset .= $datum . "001" . substr($header, 28, 4);
672 $versionset .= _fill($blockcount, 5, '0');
673 $versionset .= "001";
675 $versionset .= substr($header, -12, 10) . " ";
676 $versionset .= " " x 53;
678 $main::lxdebug->leave_sub();
684 $main::lxdebug->enter_sub();
686 my ($self, $form, $fileno) = @_;
688 my $stamm = $self->get_datev_stamm;
690 my $ev_header = _fill($stamm->{datentraegernr}, 3, ' ', 'left');
692 $ev_header .= _fill($stamm->{beraternr}, 7, ' ', 'left');
693 $ev_header .= _fill($stamm->{beratername}, 9, ' ', 'left');
695 $ev_header .= (_fill($fileno, 5, '0')) x 2;
696 $ev_header .= " " x 95;
698 $main::lxdebug->leave_sub();
703 sub kne_buchungsexport {
704 $main::lxdebug->enter_sub();
712 my $filename = "ED00000";
717 my $fromto = $self->fromto;
719 $self->_get_transactions($fromto);
721 return if $self->errors;
725 while (scalar(@{ $self->{DATEV} || [] })) {
728 my $ed_filename = $self->export_path . $filename;
729 push(@filenames, $filename);
730 my $header = $self->make_kne_data_header($form);
732 my $kne_file = SL::DATEV::KNEFile->new();
733 $kne_file->add_block($header);
735 while (scalar(@{ $self->{DATEV} }) > 0) {
736 my $transaction = shift @{ $self->{DATEV} };
737 my $trans_lines = scalar(@{$transaction});
746 my $buchungstext = "";
748 my $datevautomatik = 0;
752 my $iconv = $::locale->{iconv_utf8};
753 my %umlaute = ($iconv->convert('ä') => 'ae',
754 $iconv->convert('ö') => 'oe',
755 $iconv->convert('ü') => 'ue',
756 $iconv->convert('Ä') => 'Ae',
757 $iconv->convert('Ö') => 'Oe',
758 $iconv->convert('Ü') => 'Ue',
759 $iconv->convert('ß') => 'sz');
760 for (my $i = 0; $i < $trans_lines; $i++) {
761 if ($trans_lines == 2) {
762 if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
763 $umsatz = $transaction->[$i]->{'amount'};
766 if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
767 $umsatz = $transaction->[$i]->{'umsatz'};
770 if ($transaction->[$i]->{'datevautomatik'}) {
773 if ($transaction->[$i]->{'taxkey'}) {
774 $taxkey = $transaction->[$i]->{'taxkey'};
776 if ($transaction->[$i]->{'charttax'}) {
777 $charttax = $transaction->[$i]->{'charttax'};
779 if ($transaction->[$i]->{'amount'} > 0) {
786 # Umwandlung von Umlauten und Sonderzeichen in erlaubte Zeichen bei Textfeldern
787 foreach my $umlaut (keys(%umlaute)) {
788 $transaction->[$haben]->{'invnumber'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
789 $transaction->[$haben]->{'name'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
792 $transaction->[$haben]->{'invnumber'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
793 $transaction->[$haben]->{'name'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
795 $transaction->[$haben]->{'invnumber'} = substr($transaction->[$haben]->{'invnumber'}, 0, 12);
796 $transaction->[$haben]->{'name'} = substr($transaction->[$haben]->{'name'}, 0, 30);
797 $transaction->[$haben]->{'invnumber'} =~ s/\ *$//;
798 $transaction->[$haben]->{'name'} =~ s/\ *$//;
800 if ($trans_lines >= 2) {
802 $gegenkonto = "a" . trim_leading_zeroes($transaction->[$haben]->{'accno'});
803 $konto = "e" . trim_leading_zeroes($transaction->[$soll]->{'accno'});
804 if ($transaction->[$haben]->{'invnumber'} ne "") {
805 $belegfeld1 = "\xBD" . $transaction->[$haben]->{'invnumber'} . "\x1C";
808 $datum .= &datetofour($transaction->[$haben]->{'transdate'}, 0);
809 $waehrung = "\xB3" . "EUR" . "\x1C";
810 if ($transaction->[$haben]->{'name'} ne "") {
811 $buchungstext = "\x1E" . $transaction->[$haben]->{'name'} . "\x1C";
813 if ($transaction->[$haben]->{'duedate'} ne "") {
814 $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
818 $umsatz = $kne_file->format_amount(abs($umsatz), 0);
819 $umsatzsumme += $umsatz;
820 $kne_file->add_block("+" . $umsatz);
822 # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
823 # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
824 # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
825 # Skript angelegt werden.
826 # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
827 # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
828 # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
829 # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
830 # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
832 if ( ( $datevautomatik || $taxkey)
833 && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
834 # $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
835 $kne_file->add_block("\x6C${taxkey}");
838 $kne_file->add_block($gegenkonto);
839 $kne_file->add_block($belegfeld1);
840 $kne_file->add_block($belegfeld2);
841 $kne_file->add_block($datum);
842 $kne_file->add_block($konto);
843 $kne_file->add_block($buchungstext);
844 $kne_file->add_block($waehrung . "\x79");
847 my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
849 $kne_file->add_block($mandantenendsumme);
852 open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
853 print(ED $kne_file->get_data());
856 $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
860 #Make EV Verwaltungsdatei
861 my $ev_header = $self->make_ev_header($form, $fileno);
862 my $ev_filename = $self->export_path . $evfile;
863 push(@filenames, $evfile);
864 open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
865 print(EV $ev_header);
867 foreach my $file (@ed_versionset) {
868 print(EV $ed_versionset[$file]);
873 $self->add_filenames(@filenames);
875 $main::lxdebug->leave_sub();
877 return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
880 sub kne_stammdatenexport {
881 $main::lxdebug->enter_sub();
886 $self->get_datev_stamm->{abrechnungsnr} = "99";
890 my $filename = "ED00000";
896 my $remaining_bytes = 256;
897 my $total_bytes = 256;
898 my $buchungssatz = "";
900 my $ed_filename = $self->export_path . $filename;
901 push(@filenames, $filename);
902 open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
903 my $header = $self->make_kne_data_header($form);
904 $remaining_bytes -= length($header);
908 my (@where, @values) = ((), ());
909 if ($self->accnofrom) {
910 push @where, 'c.accno >= ?';
911 push @values, $self->accnofrom;
913 if ($self->accnoto) {
914 push @where, 'c.accno <= ?';
915 push @values, $self->accnoto;
918 my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
920 my $query = qq|SELECT c.accno, c.description
925 my $sth = $self->dbh->prepare($query);
926 $sth->execute(@values) || $form->dberror($query);
928 while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
929 if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
930 $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
931 $buchungssatz .= "\x00" x $fuellzeichen;
933 $total_bytes = ($blockcount) * 256;
935 $buchungssatz .= "t" . $ref->{'accno'};
936 $remaining_bytes = $total_bytes - length($buchungssatz . $header);
937 $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
938 $ref->{'description'} = substr($ref->{'description'}, 0, 40);
939 $ref->{'description'} =~ s/\ *$//;
942 ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
945 $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
946 $buchungssatz .= "\x00" x $fuellzeichen;
948 $total_bytes = ($blockcount) * 256;
950 $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
951 $remaining_bytes = $total_bytes - length($buchungssatz . $header);
956 print(ED $buchungssatz);
957 $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
958 my $dateiende = "\x00" x $fuellzeichen;
960 print(ED $dateiende);
963 #Make EV Verwaltungsdatei
965 $self->make_ed_versionset($header, $filename, $blockcount);
967 my $ev_header = $self->make_ev_header($form, $fileno);
968 my $ev_filename = $self->export_path . $evfile;
969 push(@filenames, $evfile);
970 open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
971 print(EV $ev_header);
973 foreach my $file (@ed_versionset) {
974 print(EV $ed_versionset[$file]);
978 $self->add_filenames(@filenames);
980 $main::lxdebug->leave_sub();
982 return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
986 clean_temporary_directories();
997 SL::DATEV - Lx-Office DATEV Export module
1001 use SL::DATEV qw(:CONSTANTS);
1003 my $datev = SL::DATEV->new(
1004 exporttype => DATEV_ET_BUCHUNGEN,
1005 format => DATEV_FORMAT_KNE,
1010 my $datev = SL::DATEV->new(
1011 exporttype => DATEV_ET_STAMM,
1012 format => DATEV_FORMAT_KNE,
1013 accnofrom => $start_account_number,
1014 accnoto => $end_account_number,
1017 # get or set datev stamm
1018 my $hashref = $datev->get_datev_stamm;
1019 $datev->save_datev_stamm($hashref);
1021 # manually clean up temporary directories
1022 $datev->clean_temporary_directories;
1027 if ($datev->errors) {
1028 die join "\n", $datev->error;
1031 # get relevant data for saving the export:
1032 my $dl_token = $datev->download_token;
1033 my $path = $datev->export_path;
1034 my @files = $datev->filenames;
1036 # retrieving an export at a later time
1037 my $datev = SL::DATEV->new(
1038 download_token => $dl_token_from_user,
1041 my $path = $datev->export_path;
1042 my @files = glob("$path/*");
1046 This module implements the DATEV export standard. For usage see above.
1054 Generic constructor. See section attributes for information about hat to pass.
1056 =item get_datev_stamm
1058 Loads DATEV Stammdaten and returns as hashref.
1060 =item save_datev_stamm HASHREF
1062 Saves DATEV Stammdaten from provided hashref.
1066 See L<CONSTANTS> for possible values
1068 =item has_exporttype
1070 Returns true if an exporttype has been set. Without exporttype most report functions won't work.
1074 Specifies the designated format of the export. Currently only KNE export is implemented.
1076 See L<CONSTANTS> for possible values
1080 Returns true if a format has been set. Without format most report functions won't work.
1082 =item download_token
1084 Returns a download token for this DATEV object.
1086 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1090 Returns an export_path for this DATEV object.
1092 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1096 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.
1098 =item net_gross_differences
1100 If there were any net gross differences during calculation they will be collected here.
1102 =item sum_net_gross_differences
1104 Sum of all differences.
1106 =item clean_temporary_directories
1108 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.
1112 Returns a list of errors that occured. If no errors occured, the export was a success.
1116 Exports data. You have to have set L<exporttype> and L<format> or an error will
1117 occur. OBE exports are currently not implemented.
1123 This is a list of attributes set in either the C<new> or a method of the same name.
1129 Set a database handle to use in the process. This allows for an export to be
1130 done on a transaction in progress without committing first.
1134 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1138 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1140 =item download_token
1142 Can be set on creation to retrieve a prior export for download.
1148 Set boundary dates for the export. Currently thse MUST be set for the export to work.
1154 Set boundary account numbers for the export. Only useful for a stammdaten export.
1160 =head2 Supplied to L<exporttype>
1164 =item DATEV_ET_BUCHUNGEN
1166 =item DATEV_ET_STAMM
1170 =head2 Supplied to L<format>.
1174 =item DATEV_FORMAT_KNE
1176 =item DATEV_FORMAT_OBE
1180 =head1 ERROR HANDLING
1182 This module will die in the following cases:
1188 No or unrecognized exporttype or format was provided for an export
1192 OBE rxport was called, which is not yet implemented.
1200 Errors that occur during th actual export will be collected in L<errors>. The following types can occur at the moment:
1206 C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occur.
1210 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).> This error occurs if a
1211 transaction could not be reliably sorted out, or had rounding errors over the acceptable threshold.
1215 =head1 BUGS AND CAVEATS
1221 Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to to get a working export.
1225 OBE export is currently not implemented.
1231 - handling of export_path and download token is a bit dodgy, clean that up.
1235 L<SL::DATEV::KNEFile>
1239 Philip Reetz E<lt>p.reetz@linet-services.deE<gt>,
1241 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
1243 Jan Büren E<lt>jan@lx-office-hosting.deE<gt>,
1245 Geoffrey Richardson E<lt>information@lx-office-hosting.deE<gt>,
1247 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,