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,
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;
814 my $iconv = $::locale->{iconv_utf8};
815 my %umlaute = ($iconv->convert('ä') => 'ae',
816 $iconv->convert('ö') => 'oe',
817 $iconv->convert('ü') => 'ue',
818 $iconv->convert('Ä') => 'Ae',
819 $iconv->convert('Ö') => 'Oe',
820 $iconv->convert('Ü') => 'Ue',
821 $iconv->convert('ß') => 'sz');
822 for (my $i = 0; $i < $trans_lines; $i++) {
823 if ($trans_lines == 2) {
824 if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
825 $umsatz = $transaction->[$i]->{'amount'};
828 if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
829 $umsatz = $transaction->[$i]->{'umsatz'};
832 if ($transaction->[$i]->{'datevautomatik'}) {
835 if ($transaction->[$i]->{'taxkey'}) {
836 $taxkey = $transaction->[$i]->{'taxkey'};
838 if ($transaction->[$i]->{'charttax'}) {
839 $charttax = $transaction->[$i]->{'charttax'};
841 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]->{'duedate'} ne "") {
876 $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
880 $umsatz = $kne_file->format_amount(abs($umsatz), 0);
881 $umsatzsumme += $umsatz;
882 $kne_file->add_block("+" . $umsatz);
884 # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
885 # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
886 # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
887 # Skript angelegt werden.
888 # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
889 # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
890 # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
891 # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
892 # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
894 if ( ( $datevautomatik || $taxkey)
895 && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
896 # $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
897 $kne_file->add_block("\x6C${taxkey}");
900 $kne_file->add_block($gegenkonto);
901 $kne_file->add_block($belegfeld1);
902 $kne_file->add_block($belegfeld2);
903 $kne_file->add_block($datum);
904 $kne_file->add_block($konto);
905 $kne_file->add_block($buchungstext);
906 $kne_file->add_block($waehrung . "\x79");
909 my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
911 $kne_file->add_block($mandantenendsumme);
914 open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
915 print(ED $kne_file->get_data());
918 $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
922 #Make EV Verwaltungsdatei
923 my $ev_header = $self->make_ev_header($form, $fileno);
924 my $ev_filename = $self->export_path . $evfile;
925 push(@filenames, $evfile);
926 open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
927 print(EV $ev_header);
929 foreach my $file (@ed_versionset) {
930 print(EV $ed_versionset[$file]);
935 $self->add_filenames(@filenames);
937 $main::lxdebug->leave_sub();
939 return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
942 sub kne_stammdatenexport {
943 $main::lxdebug->enter_sub();
948 $self->get_datev_stamm->{abrechnungsnr} = "99";
952 my $filename = "ED00000";
958 my $remaining_bytes = 256;
959 my $total_bytes = 256;
960 my $buchungssatz = "";
962 my $ed_filename = $self->export_path . $filename;
963 push(@filenames, $filename);
964 open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
965 my $header = $self->make_kne_data_header($form);
966 $remaining_bytes -= length($header);
970 my (@where, @values) = ((), ());
971 if ($self->accnofrom) {
972 push @where, 'c.accno >= ?';
973 push @values, $self->accnofrom;
975 if ($self->accnoto) {
976 push @where, 'c.accno <= ?';
977 push @values, $self->accnoto;
980 my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
982 my $query = qq|SELECT c.accno, c.description
987 my $sth = $self->dbh->prepare($query);
988 $sth->execute(@values) || $form->dberror($query);
990 while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
991 if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
992 $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
993 $buchungssatz .= "\x00" x $fuellzeichen;
995 $total_bytes = ($blockcount) * 256;
997 $buchungssatz .= "t" . $ref->{'accno'};
998 $remaining_bytes = $total_bytes - length($buchungssatz . $header);
999 $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
1000 $ref->{'description'} = substr($ref->{'description'}, 0, 40);
1001 $ref->{'description'} =~ s/\ *$//;
1004 ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
1007 $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
1008 $buchungssatz .= "\x00" x $fuellzeichen;
1010 $total_bytes = ($blockcount) * 256;
1012 $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
1013 $remaining_bytes = $total_bytes - length($buchungssatz . $header);
1018 print(ED $buchungssatz);
1019 $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
1020 my $dateiende = "\x00" x $fuellzeichen;
1022 print(ED $dateiende);
1025 #Make EV Verwaltungsdatei
1027 $self->make_ed_versionset($header, $filename, $blockcount);
1029 my $ev_header = $self->make_ev_header($form, $fileno);
1030 my $ev_filename = $self->export_path . $evfile;
1031 push(@filenames, $evfile);
1032 open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
1033 print(EV $ev_header);
1035 foreach my $file (@ed_versionset) {
1036 print(EV $ed_versionset[$file]);
1040 $self->add_filenames(@filenames);
1042 $main::lxdebug->leave_sub();
1044 return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
1048 clean_temporary_directories();
1059 SL::DATEV - kivitendo DATEV Export module
1063 use SL::DATEV qw(:CONSTANTS);
1065 my $datev = SL::DATEV->new(
1066 exporttype => DATEV_ET_BUCHUNGEN,
1067 format => DATEV_FORMAT_KNE,
1072 my $datev = SL::DATEV->new(
1073 exporttype => DATEV_ET_STAMM,
1074 format => DATEV_FORMAT_KNE,
1075 accnofrom => $start_account_number,
1076 accnoto => $end_account_number,
1079 # get or set datev stamm
1080 my $hashref = $datev->get_datev_stamm;
1081 $datev->save_datev_stamm($hashref);
1083 # manually clean up temporary directories
1084 $datev->clean_temporary_directories;
1089 if ($datev->errors) {
1090 die join "\n", $datev->error;
1093 # get relevant data for saving the export:
1094 my $dl_token = $datev->download_token;
1095 my $path = $datev->export_path;
1096 my @files = $datev->filenames;
1098 # retrieving an export at a later time
1099 my $datev = SL::DATEV->new(
1100 download_token => $dl_token_from_user,
1103 my $path = $datev->export_path;
1104 my @files = glob("$path/*");
1108 This module implements the DATEV export standard. For usage see above.
1116 Generic constructor. See section attributes for information about hat to pass.
1118 =item get_datev_stamm
1120 Loads DATEV Stammdaten and returns as hashref.
1122 =item save_datev_stamm HASHREF
1124 Saves DATEV Stammdaten from provided hashref.
1128 See L<CONSTANTS> for possible values
1130 =item has_exporttype
1132 Returns true if an exporttype has been set. Without exporttype most report functions won't work.
1136 Specifies the designated format of the export. Currently only KNE export is implemented.
1138 See L<CONSTANTS> for possible values
1142 Returns true if a format has been set. Without format most report functions won't work.
1144 =item download_token
1146 Returns a download token for this DATEV object.
1148 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1152 Returns an export_path for this DATEV object.
1154 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1158 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.
1160 =item net_gross_differences
1162 If there were any net gross differences during calculation they will be collected here.
1164 =item sum_net_gross_differences
1166 Sum of all differences.
1168 =item clean_temporary_directories
1170 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.
1174 Returns a list of errors that occured. If no errors occured, the export was a success.
1178 Exports data. You have to have set L<exporttype> and L<format> or an error will
1179 occur. OBE exports are currently not implemented.
1185 This is a list of attributes set in either the C<new> or a method of the same name.
1191 Set a database handle to use in the process. This allows for an export to be
1192 done on a transaction in progress without committing first.
1196 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1200 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1202 =item download_token
1204 Can be set on creation to retrieve a prior export for download.
1210 Set boundary dates for the export. Currently thse MUST be set for the export to work.
1216 Set boundary account numbers for the export. Only useful for a stammdaten export.
1222 =head2 Supplied to L<exporttype>
1226 =item DATEV_ET_BUCHUNGEN
1228 =item DATEV_ET_STAMM
1232 =head2 Supplied to L<format>.
1236 =item DATEV_FORMAT_KNE
1238 =item DATEV_FORMAT_OBE
1242 =head1 ERROR HANDLING
1244 This module will die in the following cases:
1250 No or unrecognized exporttype or format was provided for an export
1254 OBE rxport was called, which is not yet implemented.
1262 Errors that occur during th actual export will be collected in L<errors>. The following types can occur at the moment:
1268 C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occur.
1272 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).> This error occurs if a
1273 transaction could not be reliably sorted out, or had rounding errors over the acceptable threshold.
1277 =head1 BUGS AND CAVEATS
1283 Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to to get a working export.
1287 OBE export is currently not implemented.
1293 - handling of export_path and download token is a bit dodgy, clean that up.
1297 L<SL::DATEV::KNEFile>
1301 Philip Reetz E<lt>p.reetz@linet-services.deE<gt>,
1303 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
1305 Jan Büren E<lt>jan@lx-office-hosting.deE<gt>,
1307 Geoffrey Richardson E<lt>information@lx-office-hosting.deE<gt>,
1309 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,