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 die "illegal trans_id passed for DATEV export: " . $self->{trans_id} . "\n" unless $self->{trans_id} =~ m/^\d+$/;
204 return $self->{trans_id};
211 $self->{accnofrom} = $_[0];
214 return $self->{accnofrom};
221 $self->{accnoto} = $_[0];
224 return $self->{accnoto};
232 $self->{dbh} = $_[0];
233 $self->{provided_dbh} = 1;
236 $self->{dbh} ||= $::form->get_standard_dbh;
243 sub clean_temporary_directories {
244 $::lxdebug->enter_sub;
246 foreach my $path (glob($::lx_office_conf{paths}->{userspath} . "/datev-export-*")) {
247 next unless -d $path;
249 my $mtime = (stat($path))[9];
250 next if ((time() - $mtime) < 8 * 60 * 60);
255 $::lxdebug->leave_sub;
259 $main::lxdebug->enter_sub();
262 my $field_len = shift;
263 my $fill_char = shift;
264 my $alignment = shift || 'right';
266 my $text_len = length $text;
268 if ($field_len < $text_len) {
269 $text = substr $text, 0, $field_len;
271 } elsif ($field_len > $text_len) {
272 my $filler = ($fill_char) x ($field_len - $text_len);
273 $text = $alignment eq 'right' ? $filler . $text : $text . $filler;
276 $main::lxdebug->leave_sub();
281 sub get_datev_stamm {
282 return $_[0]{stamm} ||= selectfirst_hashref_query($::form, $_[0]->dbh, 'SELECT * FROM datev');
285 sub save_datev_stamm {
286 my ($self, $data) = @_;
288 do_query($::form, $self->dbh, 'DELETE FROM datev');
290 my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
292 my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
293 do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
295 $self->dbh->commit unless $self->provided_dbh;
302 die 'no format set!' unless $self->has_format;
304 if ($self->format == DATEV_FORMAT_KNE) {
305 $result = $self->kne_export;
306 } elsif ($self->format == DATEV_FORMAT_OBE) {
307 $result = $self->obe_export;
309 die 'unrecognized export format';
319 die 'no exporttype set!' unless $self->has_exporttype;
321 if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
322 $result = $self->kne_buchungsexport;
323 } elsif ($self->exporttype == DATEV_ET_STAMM) {
324 $result = $self->kne_stammdatenexport;
326 die 'unrecognized exporttype';
333 die 'not yet implemented';
339 return unless $self->from && $self->to;
341 return "transdate >= '" . $self->from->to_lxoffice . "' and transdate <= '" . $self->to->to_lxoffice . "'";
348 sub _get_transactions {
349 $main::lxdebug->enter_sub();
352 my $progress_callback = shift || sub {};
354 my $form = $main::form;
356 my $trans_id_filter = '';
358 if ( $self->{trans_id} ) {
359 # ignore dates when trans_id is passed so that the entire transaction is
360 # checked, not just either the initial bookings or the subsequent payments
361 # (the transdates will likely differ)
363 $trans_id_filter = 'ac.trans_id = ' . $self->trans_id;
365 $fromto =~ s/transdate/ac\.transdate/g;
370 my $filter = ''; # Useful for debugging purposes
372 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');
375 qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey,
376 ar.invnumber, ar.duedate, ar.amount as umsatz, ar.deliverydate,
378 c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
382 LEFT JOIN ar ON (ac.trans_id = ar.id)
383 LEFT JOIN customer ct ON (ar.customer_id = ct.id)
384 LEFT JOIN chart c ON (ac.chart_id = c.id)
385 LEFT JOIN tax t ON (ac.tax_id = t.id)
386 WHERE (ar.id IS NOT NULL)
393 SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey,
394 ap.invnumber, ap.duedate, ap.amount as umsatz, ap.deliverydate,
396 c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
400 LEFT JOIN ap ON (ac.trans_id = ap.id)
401 LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
402 LEFT JOIN chart c ON (ac.chart_id = c.id)
403 LEFT JOIN tax t ON (ac.tax_id = t.id)
404 WHERE (ap.id IS NOT NULL)
411 SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey,
412 gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz, NULL as deliverydate,
413 gl.description AS name, NULL as ustid,
414 c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
418 LEFT JOIN gl ON (ac.trans_id = gl.id)
419 LEFT JOIN chart c ON (ac.chart_id = c.id)
420 LEFT JOIN tax t ON (ac.tax_id = t.id)
421 WHERE (gl.id IS NOT NULL)
426 ORDER BY trans_id, acc_trans_id|;
428 my $sth = prepare_execute_query($form, $self->dbh, $query);
434 while ( $continue && (my $ref = $sth->fetchrow_hashref("NAME_lc")) ) {
435 last unless $ref; # for single transactions
437 if (($counter % 500) == 0) {
438 $progress_callback->($counter);
441 my $trans = [ $ref ];
443 my $count = $ref->{amount};
446 # if the amount of a booking in a group is smaller than 0.02, any tax
447 # amounts will likely be smaller than 1 cent, so go into subcent mode
448 my $subcent = abs($count) < 0.02;
450 # records from acc_trans are ordered by trans_id and acc_trans_id
451 # first check for unbalanced ledger inside one trans_id
452 # there may be several groups inside a trans_id, e.g. the original booking and the payment
453 # each group individually should be exactly balanced and each group
454 # individually needs its own datev lines
456 # keep fetching new acc_trans lines until the end of a balanced group is reached
457 while (abs($count) > 0.01 || $firstrun || ($subcent && abs($count) > 0.005)) {
458 my $ref2 = $sth->fetchrow_hashref("NAME_lc");
464 # check if trans_id of current acc_trans line is still the same as the
465 # trans_id of the first line in group, i.e. we haven't finished a 0-group
466 # before moving on to the next trans_id, error will likely be in the old
469 if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
470 require SL::DB::Manager::AccTransaction;
471 if ( $trans->[0]->{trans_id} ) {
472 my $acc_trans_old_obj = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
473 $self->add_error("Unbalanced ledger! Old: " . $acc_trans_old_obj->transaction_name) if ref($acc_trans_old_obj);
475 if ( $ref2->{trans_id} ) {
476 my $acc_trans_curr_obj = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $ref2->{trans_id} ]);
477 $self->add_error("Unbalanced ledger! New:" . $acc_trans_curr_obj->transaction_name) if ref($acc_trans_curr_obj);
479 $self->add_error("count: $count");
483 push @{ $trans }, $ref2;
485 $count += $ref2->{amount};
489 foreach my $i (0 .. scalar(@{ $trans }) - 1) {
490 my $ref = $trans->[$i];
491 my $prev_ref = 0 < $i ? $trans->[$i - 1] : undef;
492 if ( $all_taxchart_ids{$ref->{id}}
493 && ($ref->{link} =~ m/(?:AP_tax|AR_tax)/)
494 && ( ($prev_ref && $prev_ref->{taxkey} && (_sign($ref->{amount}) == _sign($prev_ref->{amount})))
495 || $ref->{invoice})) {
499 if ( !$ref->{invoice} # we have a non-invoice booking (=gl)
500 && $ref->{is_tax} # that has "is_tax" set
501 && !($prev_ref->{is_tax}) # previous line wasn't is_tax
502 && (_sign($ref->{amount}) == _sign($prev_ref->{amount}))) { # and sign same as previous sign
503 $trans->[$i - 1]->{tax_amount} = $ref->{amount};
508 if (scalar(@{$trans}) <= 2) {
509 push @{ $self->{DATEV} }, $trans;
513 # determine at which array position the reference value (called absumsatz) is
514 # and which amount it has
516 for my $j (0 .. (scalar(@{$trans}) - 1)) {
519 # 1: gl transaction (Dialogbuchung), invoice is false, no double split booking allowed
521 # 2: sales or vendor invoice (Verkaufs- und Einkaufsrechnung): invoice is
522 # true, instead of absumsatz use link AR/AP (there should only be one
525 # 3. AR/AP transaction (Kreditoren- und Debitorenbuchung): invoice is false,
526 # instead of absumsatz use link AR/AP (there should only be one, so jump
527 # out of search as soon as you find it )
530 # for gl-bookings no split is allowed and there is no AR/AP account, so we always use the maximum value as a reference
531 # for ap/ar bookings we can always search for AR/AP in link and use that
532 if ( ( not $trans->[$j]->{'invoice'} and abs($trans->[$j]->{'amount'}) > abs($absumsatz) )
533 or ($trans->[$j]->{'invoice'} and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP'))) {
534 $absumsatz = $trans->[$j]->{'amount'};
539 # Problem: we can't distinguish between AR and AP and normal invoices via boolean "invoice"
540 # for AR and AP transaction exit the loop as soon as an AR or AP account is found
541 # there must be only one AR or AP chart in the booking
542 if ( $trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP') {
543 $notsplitindex = $j; # position in booking with highest amount
544 $absumsatz = $trans->[$j]->{'amount'};
549 my $ml = ($trans->[0]->{'umsatz'} > 0) ? 1 : -1;
550 my $rounding_error = 0;
553 # go through each line and determine if it is a tax booking or not
554 # skip all tax lines and notsplitindex line
555 # push all other accounts (e.g. income or expense) with corresponding taxkey
557 for my $j (0 .. (scalar(@{$trans}) - 1)) {
558 if ( ($j != $notsplitindex)
559 && !$trans->[$j]->{is_tax}
560 && ( $trans->[$j]->{'taxkey'} eq ""
561 || $trans->[$j]->{'taxkey'} eq "0"
562 || $trans->[$j]->{'taxkey'} eq "1"
563 || $trans->[$j]->{'taxkey'} eq "10"
564 || $trans->[$j]->{'taxkey'} eq "11")) {
566 map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
568 $absumsatz += $trans->[$j]->{'amount'};
569 $new_trans{'amount'} = $trans->[$j]->{'amount'} * (-1);
570 $new_trans{'umsatz'} = abs($trans->[$j]->{'amount'}) * $ml;
571 $trans->[$j]->{'umsatz'} = abs($trans->[$j]->{'amount'}) * $ml;
573 push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
575 } elsif (($j != $notsplitindex) && !$trans->[$j]->{is_tax}) {
578 map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
580 my $tax_rate = $trans->[$j]->{'taxrate'};
581 $new_trans{'net_amount'} = $trans->[$j]->{'amount'} * -1;
582 $new_trans{'tax_rate'} = 1 + $tax_rate;
584 if (!$trans->[$j]->{'invoice'}) {
585 $new_trans{'amount'} = $form->round_amount(-1 * ($trans->[$j]->{amount} + $trans->[$j]->{tax_amount}), 2);
586 $new_trans{'umsatz'} = abs($new_trans{'amount'}) * $ml;
587 $trans->[$j]->{'umsatz'} = $new_trans{'umsatz'};
588 $absumsatz += -1 * $new_trans{'amount'};
591 my $unrounded = $trans->[$j]->{'amount'} * (1 + $tax_rate) * -1 + $rounding_error;
592 my $rounded = $form->round_amount($unrounded, 2);
594 $rounding_error = $unrounded - $rounded;
595 $new_trans{'amount'} = $rounded;
596 $new_trans{'umsatz'} = abs($rounded) * $ml;
597 $trans->[$j]->{'umsatz'} = $new_trans{umsatz};
598 $absumsatz -= $rounded;
601 push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
602 push @taxed, $self->{DATEV}->[-1];
608 while ((abs($absumsatz) >= 0.01) && (abs($absumsatz) < 1.00)) {
609 if ($idx >= scalar @taxed) {
610 last if (!$correction);
616 my $transaction = $taxed[$idx]->[0];
618 my $old_amount = $transaction->{amount};
619 my $old_correction = $correction;
622 if (!$transaction->{diff}) {
623 @possible_diffs = (0.01, -0.01);
625 @possible_diffs = ($transaction->{diff});
628 foreach my $diff (@possible_diffs) {
629 my $net_amount = $form->round_amount(($transaction->{amount} + $diff) / $transaction->{tax_rate}, 2);
630 next if ($net_amount != $transaction->{net_amount});
632 $transaction->{diff} = $diff;
633 $transaction->{amount} += $diff;
634 $transaction->{umsatz} += $diff;
644 $absumsatz = $form->round_amount($absumsatz, 2);
645 if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
646 require SL::DB::Manager::AccTransaction;
647 my $acc_trans_obj = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
648 $self->add_error("Datev-Export fehlgeschlagen! Bei Transaktion " . $acc_trans_obj->transaction_name . " ($absumsatz)");
650 } elsif (abs($absumsatz) >= 0.01) {
651 $self->add_net_gross_differences($absumsatz);
657 $::lxdebug->leave_sub;
660 sub make_kne_data_header {
661 $main::lxdebug->enter_sub();
663 my ($self, $form) = @_;
666 my $stamm = $self->get_datev_stamm;
668 my $jahr = $self->from ? $self->from->year : DateTime->today->year;
671 my $header = "\x1D\x181";
672 $header .= _fill($stamm->{datentraegernr}, 3, ' ', 'left');
673 $header .= ($self->fromto) ? "11" : "13"; # Anwendungsnummer
674 $header .= _fill($stamm->{dfvkz}, 2, '0');
675 $header .= _fill($stamm->{beraternr}, 7, '0');
676 $header .= _fill($stamm->{mandantennr}, 5, '0');
677 $header .= _fill($stamm->{abrechnungsnr} . $jahr, 6, '0');
679 $header .= $self->from ? $self->from->strftime('%d%m%y') : '';
680 $header .= $self->to ? $self->to->strftime('%d%m%y') : '';
684 $header .= $primanota;
687 $header .= _fill($stamm->{passwort}, 4, '0');
688 $header .= " " x 16; # Anwendungsinfo
689 $header .= " " x 16; # Inputinfo
693 my $versionssatz = $self->exporttype == DATEV_ET_BUCHUNGEN ? "\xB5" . "1," : "\xB6" . "1,";
695 my $query = qq|SELECT accno FROM chart LIMIT 1|;
696 my $ref = selectfirst_hashref_query($form, $self->dbh, $query);
698 $versionssatz .= length $ref->{accno};
699 $versionssatz .= ",";
700 $versionssatz .= length $ref->{accno};
701 $versionssatz .= ",SELF" . "\x1C\x79";
703 $header .= $versionssatz;
705 $main::lxdebug->leave_sub();
711 $main::lxdebug->enter_sub();
713 my ($date, $six) = @_;
715 my ($day, $month, $year) = split(/\./, $date);
718 $day = substr($day, 1, 1);
720 if (length($month) < 2) {
721 $month = "0" . $month;
723 if (length($year) > 2) {
724 $year = substr($year, -2, 2);
728 $date = $day . $month . $year;
730 $date = $day . $month;
733 $main::lxdebug->leave_sub();
738 sub trim_leading_zeroes {
746 sub make_ed_versionset {
747 $main::lxdebug->enter_sub();
749 my ($self, $header, $filename, $blockcount) = @_;
751 my $versionset = "V" . substr($filename, 2, 5);
752 $versionset .= substr($header, 6, 22);
755 $versionset .= "0000" . substr($header, 28, 19);
757 my $datum = " " x 16;
758 $versionset .= $datum . "001" . substr($header, 28, 4);
761 $versionset .= _fill($blockcount, 5, '0');
762 $versionset .= "001";
764 $versionset .= substr($header, -12, 10) . " ";
765 $versionset .= " " x 53;
767 $main::lxdebug->leave_sub();
773 $main::lxdebug->enter_sub();
775 my ($self, $form, $fileno) = @_;
777 my $stamm = $self->get_datev_stamm;
779 my $ev_header = _fill($stamm->{datentraegernr}, 3, ' ', 'left');
781 $ev_header .= _fill($stamm->{beraternr}, 7, ' ', 'left');
782 $ev_header .= _fill($stamm->{beratername}, 9, ' ', 'left');
784 $ev_header .= (_fill($fileno, 5, '0')) x 2;
785 $ev_header .= " " x 95;
787 $main::lxdebug->leave_sub();
792 sub kne_buchungsexport {
793 $main::lxdebug->enter_sub();
801 my $filename = "ED00000";
806 my $fromto = $self->fromto;
808 $self->_get_transactions($fromto);
810 return if $self->errors;
814 while (scalar(@{ $self->{DATEV} || [] })) {
817 my $ed_filename = $self->export_path . $filename;
818 push(@filenames, $filename);
819 my $header = $self->make_kne_data_header($form);
821 my $kne_file = SL::DATEV::KNEFile->new();
822 $kne_file->add_block($header);
824 while (scalar(@{ $self->{DATEV} }) > 0) {
825 my $transaction = shift @{ $self->{DATEV} };
826 my $trans_lines = scalar(@{$transaction});
835 my $buchungstext = "";
837 my $datevautomatik = 0;
842 my $iconv = $::locale->{iconv_utf8};
843 my %umlaute = ($iconv->convert('ä') => 'ae',
844 $iconv->convert('ö') => 'oe',
845 $iconv->convert('ü') => 'ue',
846 $iconv->convert('Ä') => 'Ae',
847 $iconv->convert('Ö') => 'Oe',
848 $iconv->convert('Ü') => 'Ue',
849 $iconv->convert('ß') => 'sz');
850 for (my $i = 0; $i < $trans_lines; $i++) {
851 if ($trans_lines == 2) {
852 if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
853 $umsatz = $transaction->[$i]->{'amount'};
856 if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
857 $umsatz = $transaction->[$i]->{'umsatz'};
860 if ($transaction->[$i]->{'datevautomatik'}) {
863 if ($transaction->[$i]->{'taxkey'}) {
864 $taxkey = $transaction->[$i]->{'taxkey'};
866 if ($transaction->[$i]->{'charttax'}) {
867 $charttax = $transaction->[$i]->{'charttax'};
869 if ($transaction->[$i]->{'amount'} > 0) {
875 # Umwandlung von Umlauten und Sonderzeichen in erlaubte Zeichen bei Textfeldern
876 foreach my $umlaut (keys(%umlaute)) {
877 $transaction->[$haben]->{'invnumber'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
878 $transaction->[$haben]->{'name'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
881 $transaction->[$haben]->{'invnumber'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
882 $transaction->[$haben]->{'name'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
884 $transaction->[$haben]->{'invnumber'} = substr($transaction->[$haben]->{'invnumber'}, 0, 12);
885 $transaction->[$haben]->{'name'} = substr($transaction->[$haben]->{'name'}, 0, 30);
886 $transaction->[$haben]->{'invnumber'} =~ s/\ *$//;
887 $transaction->[$haben]->{'name'} =~ s/\ *$//;
889 if ($trans_lines >= 2) {
891 $gegenkonto = "a" . trim_leading_zeroes($transaction->[$haben]->{'accno'});
892 $konto = "e" . trim_leading_zeroes($transaction->[$soll]->{'accno'});
893 if ($transaction->[$haben]->{'invnumber'} ne "") {
894 $belegfeld1 = "\xBD" . $transaction->[$haben]->{'invnumber'} . "\x1C";
897 $datum .= &datetofour($transaction->[$haben]->{'transdate'}, 0);
898 $waehrung = "\xB3" . "EUR" . "\x1C";
899 if ($transaction->[$haben]->{'name'} ne "") {
900 $buchungstext = "\x1E" . $transaction->[$haben]->{'name'} . "\x1C";
902 if ($transaction->[$haben]->{'ustid'} ne "") {
903 $ustid = "\xBA" . $transaction->[$haben]->{'ustid'} . "\x1C";
905 if ($transaction->[$haben]->{'duedate'} ne "") {
906 $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
910 $umsatz = $kne_file->format_amount(abs($umsatz), 0);
911 $umsatzsumme += $umsatz;
912 $kne_file->add_block("+" . $umsatz);
914 # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
915 # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
916 # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
917 # Skript angelegt werden.
918 # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
919 # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
920 # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
921 # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
922 # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
924 if ( ( $datevautomatik || $taxkey)
925 && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
926 # $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
927 $kne_file->add_block("\x6C${taxkey}");
930 $kne_file->add_block($gegenkonto);
931 $kne_file->add_block($belegfeld1);
932 $kne_file->add_block($belegfeld2);
933 $kne_file->add_block($datum);
934 $kne_file->add_block($konto);
935 $kne_file->add_block($buchungstext);
936 $kne_file->add_block($ustid);
937 $kne_file->add_block($waehrung . "\x79");
940 my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
942 $kne_file->add_block($mandantenendsumme);
945 open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
946 print(ED $kne_file->get_data());
949 $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
953 #Make EV Verwaltungsdatei
954 my $ev_header = $self->make_ev_header($form, $fileno);
955 my $ev_filename = $self->export_path . $evfile;
956 push(@filenames, $evfile);
957 open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
958 print(EV $ev_header);
960 foreach my $file (@ed_versionset) {
961 print(EV $ed_versionset[$file]);
966 $self->add_filenames(@filenames);
968 $main::lxdebug->leave_sub();
970 return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
973 sub kne_stammdatenexport {
974 $main::lxdebug->enter_sub();
979 $self->get_datev_stamm->{abrechnungsnr} = "99";
983 my $filename = "ED00000";
989 my $remaining_bytes = 256;
990 my $total_bytes = 256;
991 my $buchungssatz = "";
993 my $ed_filename = $self->export_path . $filename;
994 push(@filenames, $filename);
995 open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
996 my $header = $self->make_kne_data_header($form);
997 $remaining_bytes -= length($header);
1001 my (@where, @values) = ((), ());
1002 if ($self->accnofrom) {
1003 push @where, 'c.accno >= ?';
1004 push @values, $self->accnofrom;
1006 if ($self->accnoto) {
1007 push @where, 'c.accno <= ?';
1008 push @values, $self->accnoto;
1011 my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
1013 my $query = qq|SELECT c.accno, c.description
1018 my $sth = $self->dbh->prepare($query);
1019 $sth->execute(@values) || $form->dberror($query);
1021 while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
1022 if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
1023 $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
1024 $buchungssatz .= "\x00" x $fuellzeichen;
1026 $total_bytes = ($blockcount) * 256;
1028 $buchungssatz .= "t" . $ref->{'accno'};
1029 $remaining_bytes = $total_bytes - length($buchungssatz . $header);
1030 $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
1031 $ref->{'description'} = substr($ref->{'description'}, 0, 40);
1032 $ref->{'description'} =~ s/\ *$//;
1035 ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
1038 $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
1039 $buchungssatz .= "\x00" x $fuellzeichen;
1041 $total_bytes = ($blockcount) * 256;
1043 $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
1044 $remaining_bytes = $total_bytes - length($buchungssatz . $header);
1049 print(ED $buchungssatz);
1050 $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
1051 my $dateiende = "\x00" x $fuellzeichen;
1053 print(ED $dateiende);
1056 #Make EV Verwaltungsdatei
1058 $self->make_ed_versionset($header, $filename, $blockcount);
1060 my $ev_header = $self->make_ev_header($form, $fileno);
1061 my $ev_filename = $self->export_path . $evfile;
1062 push(@filenames, $evfile);
1063 open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
1064 print(EV $ev_header);
1066 foreach my $file (@ed_versionset) {
1067 print(EV $ed_versionset[$file]);
1071 $self->add_filenames(@filenames);
1073 $main::lxdebug->leave_sub();
1075 return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
1079 clean_temporary_directories();
1090 SL::DATEV - kivitendo DATEV Export module
1094 use SL::DATEV qw(:CONSTANTS);
1096 my $startdate = DateTime->new(year => 2014, month => 9, day => 1);
1097 my $enddate = DateTime->new(year => 2014, month => 9, day => 31);
1098 my $datev = SL::DATEV->new(
1099 exporttype => DATEV_ET_BUCHUNGEN,
1100 format => DATEV_FORMAT_KNE,
1105 # To only export transactions from a specific trans_id: (from and to are ignored)
1106 my $invoice = SL::DB::Manager::Invoice->find_by( invnumber => '216' );
1107 my $datev = SL::DATEV->new(
1108 exporttype => DATEV_ET_BUCHUNGEN,
1109 format => DATEV_FORMAT_KNE,
1110 trans_id => $invoice->trans_id,
1113 my $datev = SL::DATEV->new(
1114 exporttype => DATEV_ET_STAMM,
1115 format => DATEV_FORMAT_KNE,
1116 accnofrom => $start_account_number,
1117 accnoto => $end_account_number,
1120 # get or set datev stamm
1121 my $hashref = $datev->get_datev_stamm;
1122 $datev->save_datev_stamm($hashref);
1124 # manually clean up temporary directories older than 8 hours
1125 $datev->clean_temporary_directories;
1130 if ($datev->errors) {
1131 die join "\n", $datev->error;
1134 # get relevant data for saving the export:
1135 my $dl_token = $datev->download_token;
1136 my $path = $datev->export_path;
1137 my @files = $datev->filenames;
1139 # retrieving an export at a later time
1140 my $datev = SL::DATEV->new(
1141 download_token => $dl_token_from_user,
1144 my $path = $datev->export_path;
1145 my @files = glob("$path/*");
1149 This module implements the DATEV export standard. For usage see above.
1157 Generic constructor. See section attributes for information about what to pass.
1159 =item get_datev_stamm
1161 Loads DATEV Stammdaten and returns as hashref.
1163 =item save_datev_stamm HASHREF
1165 Saves DATEV Stammdaten from provided hashref.
1169 See L<CONSTANTS> for possible values
1171 =item has_exporttype
1173 Returns true if an exporttype has been set. Without exporttype most report functions won't work.
1177 Specifies the designated format of the export. Currently only KNE export is implemented.
1179 See L<CONSTANTS> for possible values
1183 Returns true if a format has been set. Without format most report functions won't work.
1185 =item download_token
1187 Returns a download token for this DATEV object.
1189 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1193 Returns an export_path for this DATEV object.
1195 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1199 Returns a list of filenames generated by this DATEV object. This only works if the files were generated during its lifetime, not if the object was created from a download_token.
1201 =item net_gross_differences
1203 If there were any net gross differences during calculation they will be collected here.
1205 =item sum_net_gross_differences
1207 Sum of all differences.
1209 =item clean_temporary_directories
1211 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.
1215 Returns a list of errors that occured. If no errors occured, the export was a success.
1219 Exports data. You have to have set L<exporttype> and L<format> or an error will
1220 occur. OBE exports are currently not implemented.
1226 This is a list of attributes set in either the C<new> or a method of the same name.
1232 Set a database handle to use in the process. This allows for an export to be
1233 done on a transaction in progress without committing first.
1237 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1241 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1243 =item download_token
1245 Can be set on creation to retrieve a prior export for download.
1251 Set boundary dates for the export. Unless a trans_id is passed these MUST be
1252 set for the export to work.
1256 To check only one gl/ar/ap transaction, pass the trans_id. The attributes
1257 L<from> and L<to> are currently still needed for the query to be assembled
1264 Set boundary account numbers for the export. Only useful for a stammdaten export.
1270 =head2 Supplied to L<exporttype>
1274 =item DATEV_ET_BUCHUNGEN
1276 =item DATEV_ET_STAMM
1280 =head2 Supplied to L<format>.
1284 =item DATEV_FORMAT_KNE
1286 =item DATEV_FORMAT_OBE
1290 =head1 ERROR HANDLING
1292 This module will die in the following cases:
1298 No or unrecognized exporttype or format was provided for an export
1302 OBE export was called, which is not yet implemented.
1310 Errors that occur during th actual export will be collected in L<errors>. The following types can occur at the moment:
1316 C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occur.
1320 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).> This error occurs if a
1321 transaction could not be reliably sorted out, or had rounding errors above the acceptable threshold.
1325 =head1 BUGS AND CAVEATS
1331 Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to in order to get a working export.
1335 OBE export is currently not implemented.
1341 - handling of export_path and download token is a bit dodgy, clean that up.
1345 L<SL::DATEV::KNEFile>
1349 Philip Reetz E<lt>p.reetz@linet-services.deE<gt>,
1351 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
1353 Jan Büren E<lt>jan@lx-office-hosting.deE<gt>,
1355 Geoffrey Richardson E<lt>information@lx-office-hosting.deE<gt>,
1357 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,