Merge branch 'master' of vc.linet-services.de:public/lx-office-erp
[kivitendo-erp.git] / SL / DATEV.pm
1 #=====================================================================
2 # Lx-Office ERP
3 # Copyright (c) 2004
4 #
5 #  Author: Philip Reetz
6 #   Email: p.reetz@linet-services.de
7 #     Web: http://www.lx-office.org
8 #
9 #
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.
14 #
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 #======================================================================
23 #
24 # Datev export module
25 #======================================================================
26
27 package DATEV;
28
29 use utf8;
30 use strict;
31
32 use SL::DBUtils;
33 use SL::DATEV::KNEFile;
34 use SL::Taxkeys;
35
36 use Data::Dumper;
37 use File::Path;
38 use List::Util qw(max);
39 use Time::HiRes qw(gettimeofday);
40
41 sub _get_export_path {
42   $main::lxdebug->enter_sub();
43
44   my ($a, $b) = gettimeofday();
45   my $path    = get_path_for_download_token("${a}-${b}-${$}");
46
47   mkpath($path) unless (-d $path);
48
49   $main::lxdebug->leave_sub();
50
51   return $path;
52 }
53
54 sub get_path_for_download_token {
55   $main::lxdebug->enter_sub();
56
57   my $token = shift;
58   my $path;
59
60   if ($token =~ m|^(\d+)-(\d+)-(\d+)$|) {
61     $path = $::lx_office_conf{paths}->{userspath} . "/datev-export-${1}-${2}-${3}";
62   }
63
64   $main::lxdebug->leave_sub();
65
66   return $path;
67 }
68
69 sub get_download_token_for_path {
70   $main::lxdebug->enter_sub();
71
72   my $path = shift;
73   my $token;
74
75   if ($path =~ m|.*datev-export-(\d+)-(\d+)-(\d+)/?$|) {
76     $token = "${1}-${2}-${3}";
77   }
78
79   $main::lxdebug->leave_sub();
80
81   return $token;
82 }
83
84 sub clean_temporary_directories {
85   $main::lxdebug->enter_sub();
86
87   foreach my $path (glob($::lx_office_conf{paths}->{userspath} . "/datev-export-*")) {
88     next unless (-d $path);
89
90     my $mtime = (stat($path))[9];
91     next if ((time() - $mtime) < 8 * 60 * 60);
92
93     rmtree $path;
94   }
95
96   $main::lxdebug->leave_sub();
97 }
98
99 sub _fill {
100   $main::lxdebug->enter_sub();
101
102   my $text      = shift;
103   my $field_len = shift;
104   my $fill_char = shift;
105   my $alignment = shift || 'right';
106
107   my $text_len  = length $text;
108
109   if ($field_len < $text_len) {
110     $text = substr $text, 0, $field_len;
111
112   } elsif ($field_len > $text_len) {
113     my $filler = ($fill_char) x ($field_len - $text_len);
114     $text      = $alignment eq 'right' ? $filler . $text : $text . $filler;
115   }
116
117   $main::lxdebug->leave_sub();
118
119   return $text;
120 }
121
122 sub get_datev_stamm {
123   $main::lxdebug->enter_sub();
124
125   my ($self, $myconfig, $form) = @_;
126
127   # connect to database
128   my $dbh = $form->dbconnect($myconfig);
129
130   my $query = qq|SELECT * FROM datev|;
131   my $sth   = $dbh->prepare($query);
132   $sth->execute || $form->dberror($query);
133
134   my $ref = $sth->fetchrow_hashref("NAME_lc");
135
136   map { $form->{$_} = $ref->{$_} } keys %$ref;
137
138   $sth->finish;
139   $dbh->disconnect;
140   $main::lxdebug->leave_sub();
141 }
142
143 sub save_datev_stamm {
144   $main::lxdebug->enter_sub();
145
146   my ($self, $myconfig, $form) = @_;
147
148   # connect to database
149   my $dbh = $form->dbconnect_noauto($myconfig);
150
151   my $query = qq|DELETE FROM datev|;
152   $dbh->do($query) || $form->dberror($query);
153
154   $query = qq|INSERT INTO datev
155               (beraternr, beratername, dfvkz, mandantennr, datentraegernr, abrechnungsnr) VALUES
156               (|
157     . $dbh->quote($form->{beraternr}) . qq|,|
158     . $dbh->quote($form->{beratername}) . qq|,|
159     . $dbh->quote($form->{dfvkz}) . qq|,
160               |
161     . $dbh->quote($form->{mandantennr}) . qq|,|
162     . $dbh->quote($form->{datentraegernr}) . qq|,|
163     . $dbh->quote($form->{abrechnungsnr}) . qq|)|;
164   my $sth = $dbh->prepare($query);
165   $sth->execute || $form->dberror($query);
166   $sth->finish;
167
168   $dbh->commit;
169   $dbh->disconnect;
170   $main::lxdebug->leave_sub();
171 }
172
173 sub kne_export {
174   $main::lxdebug->enter_sub();
175
176   my ($self, $myconfig, $form) = @_;
177   my $result;
178
179   if ($form->{exporttype} == 0) {
180     $result = kne_buchungsexport($myconfig, $form);
181   } else {
182     $result = kne_stammdatenexport($myconfig, $form);
183   }
184
185   $main::lxdebug->leave_sub();
186
187   return $result;
188 }
189
190 sub obe_export {
191   $main::lxdebug->enter_sub();
192
193   my ($self, $myconfig, $form) = @_;
194
195   # connect to database
196   my $dbh = $form->dbconnect_noauto($myconfig);
197   $dbh->commit;
198   $dbh->disconnect;
199   $main::lxdebug->leave_sub();
200 }
201
202 sub get_dates {
203   $main::lxdebug->enter_sub();
204
205   my ($zeitraum, $monat, $quartal, $transdatefrom, $transdateto) = @_;
206   my ($fromto, $jahr, $leap);
207
208   my $form = $main::form;
209
210   $fromto = "transdate >= ";
211
212   my @a = localtime;
213   $a[5] += 1900;
214   $jahr = $a[5];
215   if ($zeitraum eq "monat") {
216   SWITCH: {
217       $monat eq "1" && do {
218         $form->{fromdate} = "1.1.$jahr";
219         $form->{todate}   = "31.1.$jahr";
220         last SWITCH;
221       };
222       $monat eq "2" && do {
223         $form->{fromdate} = "1.2.$jahr";
224
225         #this works from 1901 to 2099, 1900 and 2100 fail.
226         $leap = ($jahr % 4 == 0) ? "29" : "28";
227         $form->{todate} = "$leap.2.$jahr";
228         last SWITCH;
229       };
230       $monat eq "3" && do {
231         $form->{fromdate} = "1.3.$jahr";
232         $form->{todate}   = "31.3.$jahr";
233         last SWITCH;
234       };
235       $monat eq "4" && do {
236         $form->{fromdate} = "1.4.$jahr";
237         $form->{todate}   = "30.4.$jahr";
238         last SWITCH;
239       };
240       $monat eq "5" && do {
241         $form->{fromdate} = "1.5.$jahr";
242         $form->{todate}   = "31.5.$jahr";
243         last SWITCH;
244       };
245       $monat eq "6" && do {
246         $form->{fromdate} = "1.6.$jahr";
247         $form->{todate}   = "30.6.$jahr";
248         last SWITCH;
249       };
250       $monat eq "7" && do {
251         $form->{fromdate} = "1.7.$jahr";
252         $form->{todate}   = "31.7.$jahr";
253         last SWITCH;
254       };
255       $monat eq "8" && do {
256         $form->{fromdate} = "1.8.$jahr";
257         $form->{todate}   = "31.8.$jahr";
258         last SWITCH;
259       };
260       $monat eq "9" && do {
261         $form->{fromdate} = "1.9.$jahr";
262         $form->{todate}   = "30.9.$jahr";
263         last SWITCH;
264       };
265       $monat eq "10" && do {
266         $form->{fromdate} = "1.10.$jahr";
267         $form->{todate}   = "31.10.$jahr";
268         last SWITCH;
269       };
270       $monat eq "11" && do {
271         $form->{fromdate} = "1.11.$jahr";
272         $form->{todate}   = "30.11.$jahr";
273         last SWITCH;
274       };
275       $monat eq "12" && do {
276         $form->{fromdate} = "1.12.$jahr";
277         $form->{todate}   = "31.12.$jahr";
278         last SWITCH;
279       };
280     }
281     $fromto .=
282       "'" . $form->{fromdate} . "' and transdate <= '" . $form->{todate} . "'";
283   }
284
285   elsif ($zeitraum eq "quartal") {
286     if ($quartal == 1) {
287       $fromto .=
288         "'01.01." . $jahr . "' and transdate <= '31.03." . $jahr . "'";
289     } elsif ($quartal == 2) {
290       $fromto .=
291         "'01.04." . $jahr . "' and transdate <= '30.06." . $jahr . "'";
292     } elsif ($quartal == 3) {
293       $fromto .=
294         "'01.07." . $jahr . "' and transdate <= '30.09." . $jahr . "'";
295     } elsif ($quartal == 4) {
296       $fromto .=
297         "'01.10." . $jahr . "' and transdate <= '31.12." . $jahr . "'";
298     }
299   }
300
301   elsif ($zeitraum eq "zeit") {
302     $fromto            .= "'" . $transdatefrom . "' and transdate <= '" . $transdateto . "'";
303     my ($yy, $mm, $dd)  = $main::locale->parse_date(\%main::myconfig, $transdatefrom);
304     $jahr               = $yy;
305   }
306
307   $main::lxdebug->leave_sub();
308
309   return ($fromto, $jahr);
310 }
311
312 sub _sign {
313   my $value = shift;
314
315   return $value < 0 ? -1
316     :    $value > 0 ?  1
317     :                  0;
318 }
319
320 sub _get_transactions {
321   $main::lxdebug->enter_sub();
322
323   my $fromto   =  shift;
324
325   my $myconfig =  \%main::myconfig;
326   my $form     =  $main::form;
327
328   my $dbh      =  $form->get_standard_dbh($myconfig);
329
330   my ($notsplitindex);
331   my @errors   = ();
332
333   $form->{net_gross_differences}     = [];
334   $form->{sum_net_gross_differences} = 0;
335
336   $fromto      =~ s/transdate/ac\.transdate/g;
337
338   my $taxkeys  = Taxkeys->new();
339   my $filter   = '';            # Useful for debugging purposes
340
341   my %all_taxchart_ids = selectall_as_map($form, $dbh, qq|SELECT DISTINCT chart_id, TRUE AS is_set FROM tax|, 'chart_id', 'is_set');
342
343   my $query    =
344     qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey,
345          ar.invnumber, ar.duedate, ar.amount as umsatz,
346          ct.name,
347          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link,
348          ar.invoice
349        FROM acc_trans ac
350        LEFT JOIN ar          ON (ac.trans_id    = ar.id)
351        LEFT JOIN customer ct ON (ar.customer_id = ct.id)
352        LEFT JOIN chart c     ON (ac.chart_id    = c.id)
353        WHERE (ar.id IS NOT NULL)
354          AND $fromto
355          $filter
356
357        UNION ALL
358
359        SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey,
360          ap.invnumber, ap.duedate, ap.amount as umsatz,
361          ct.name,
362          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link,
363          ap.invoice
364        FROM acc_trans ac
365        LEFT JOIN ap        ON (ac.trans_id  = ap.id)
366        LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
367        LEFT JOIN chart c   ON (ac.chart_id  = c.id)
368        WHERE (ap.id IS NOT NULL)
369          AND $fromto
370          $filter
371
372        UNION ALL
373
374        SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey,
375          gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz,
376          gl.description AS name,
377          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, c.link,
378          FALSE AS invoice
379        FROM acc_trans ac
380        LEFT JOIN gl      ON (ac.trans_id  = gl.id)
381        LEFT JOIN chart c ON (ac.chart_id  = c.id)
382        WHERE (gl.id IS NOT NULL)
383          AND $fromto
384          $filter
385
386        ORDER BY trans_id, acc_trans_id|;
387
388   my $sth = prepare_execute_query($form, $dbh, $query);
389   $form->{DATEV} = [];
390
391   my $counter = 0;
392   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
393     $counter++;
394     if (($counter % 500) == 0) {
395       print("$counter ");
396     }
397
398     my $trans    = [ $ref ];
399
400     my $count    = $ref->{amount};
401     my $firstrun = 1;
402     my $subcent  = abs($count) < 0.02;
403
404     while (abs($count) > 0.01 || $firstrun || ($subcent && abs($count) > 0.005)) {
405       my $ref2 = $sth->fetchrow_hashref("NAME_lc");
406       last unless ($ref2);
407
408       if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
409         $form->error("Unbalanced ledger! old trans_id " . $trans->[0]->{trans_id} . " new trans_id " . $ref2->{trans_id} . " count $count");
410         ::end_of_request();
411       }
412
413       push @{ $trans }, $ref2;
414
415       $count    += $ref2->{amount};
416       $firstrun  = 0;
417     }
418
419     foreach my $i (0 .. scalar(@{ $trans }) - 1) {
420       my $ref        = $trans->[$i];
421       my $prev_ref   = 0 < $i ? $trans->[$i - 1] : undef;
422       if (   $all_taxchart_ids{$ref->{id}}
423           && ($ref->{link} =~ m/(?:AP_tax|AR_tax)/)
424           && (   ($prev_ref && $prev_ref->{taxkey} && (_sign($ref->{amount}) == _sign($prev_ref->{amount})))
425               || $ref->{invoice})) {
426         $ref->{is_tax} = 1;
427       }
428
429       if (   !$ref->{invoice}   # we have a non-invoice booking (=gl)
430           &&  $ref->{is_tax}    # that has "is_tax" set
431           && !($prev_ref->{is_tax})  # previous line wasn't is_tax
432           &&  (_sign($ref->{amount}) == _sign($prev_ref->{amount}))) {  # and sign same as previous sign
433         $trans->[$i - 1]->{tax_amount} = $ref->{amount};
434       }
435     }
436
437     my %taxid_taxkeys = ();
438     my $absumsatz     = 0;
439     if (scalar(@{$trans}) <= 2) {
440       push @{ $form->{DATEV} }, $trans;
441       next;
442     }
443
444     for my $j (0 .. (scalar(@{$trans}) - 1)) {
445       # for gl-bookings no split is allowed and there is no AR/AP account, so we always use the maximum value as a reference
446       # for ap/ar bookings we can always search for AR/AP in link and use that
447       if ( ( not $trans->[$j]->{'invoice'} and abs($trans->[$j]->{'amount'}) > abs($absumsatz) )
448          or ($trans->[$j]->{'invoice'} and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP'))) {
449         $absumsatz     = $trans->[$j]->{'amount'};
450         $notsplitindex = $j;
451       }
452     }
453
454     my $ml             = ($trans->[0]->{'umsatz'} > 0) ? 1 : -1;
455     my $rounding_error = 0;
456     my @taxed;
457
458     for my $j (0 .. (scalar(@{$trans}) - 1)) {
459       if (   ($j != $notsplitindex)
460           && !$trans->[$j]->{is_tax}
461           && (   $trans->[$j]->{'taxkey'} eq ""
462               || $trans->[$j]->{'taxkey'} eq "0"
463               || $trans->[$j]->{'taxkey'} eq "1"
464               || $trans->[$j]->{'taxkey'} eq "10"
465               || $trans->[$j]->{'taxkey'} eq "11")) {
466         my %new_trans = ();
467         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
468
469         $absumsatz               += $trans->[$j]->{'amount'};
470         $new_trans{'amount'}      = $trans->[$j]->{'amount'} * (-1);
471         $new_trans{'umsatz'}      = abs($trans->[$j]->{'amount'}) * $ml;
472         $trans->[$j]->{'umsatz'}  = abs($trans->[$j]->{'amount'}) * $ml;
473
474         push @{ $form->{DATEV} }, [ \%new_trans, $trans->[$j] ];
475
476       } elsif (($j != $notsplitindex) && !$trans->[$j]->{is_tax}) {
477         my %tax_info = $taxkeys->get_full_tax_info('transdate' => $trans->[$j]->{transdate});
478
479         my %new_trans = ();
480         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
481
482         my $tax_rate              = $tax_info{taxkeys}->{ $trans->[$j]->{'taxkey'} }->{taxrate};
483         $new_trans{'net_amount'}  = $trans->[$j]->{'amount'} * -1;
484         $new_trans{'tax_rate'}    = 1 + $tax_rate;
485
486         if (!$trans->[$j]->{'invoice'}) {
487           $new_trans{'amount'}      = $form->round_amount(-1 * ($trans->[$j]->{amount} + $trans->[$j]->{tax_amount}), 2);
488           $new_trans{'umsatz'}      = abs($new_trans{'amount'}) * $ml;
489           $trans->[$j]->{'umsatz'}  = $new_trans{'umsatz'};
490           $absumsatz               += -1 * $new_trans{'amount'};
491
492         } else {
493           my $unrounded             = $trans->[$j]->{'amount'} * (1 + $tax_rate) * -1 + $rounding_error;
494           my $rounded               = $form->round_amount($unrounded, 2);
495
496           $rounding_error           = $unrounded - $rounded;
497           $new_trans{'amount'}      = $rounded;
498           $new_trans{'umsatz'}      = abs($rounded) * $ml;
499           $trans->[$j]->{'umsatz'}  = $new_trans{umsatz};
500           $absumsatz               -= $rounded;
501         }
502
503         push @{ $form->{DATEV} }, [ \%new_trans, $trans->[$j] ];
504         push @taxed, $form->{DATEV}->[-1];
505       }
506     }
507
508     my $idx        = 0;
509     my $correction = 0;
510     while ((abs($absumsatz) >= 0.01) && (abs($absumsatz) < 1.00)) {
511       if ($idx >= scalar @taxed) {
512         last if (!$correction);
513
514         $correction = 0;
515         $idx        = 0;
516       }
517
518       my $transaction = $taxed[$idx]->[0];
519
520       my $old_amount     = $transaction->{amount};
521       my $old_correction = $correction;
522       my @possible_diffs;
523
524       if (!$transaction->{diff}) {
525         @possible_diffs = (0.01, -0.01);
526       } else {
527         @possible_diffs = ($transaction->{diff});
528       }
529
530       foreach my $diff (@possible_diffs) {
531         my $net_amount = $form->round_amount(($transaction->{amount} + $diff) / $transaction->{tax_rate}, 2);
532         next if ($net_amount != $transaction->{net_amount});
533
534         $transaction->{diff}    = $diff;
535         $transaction->{amount} += $diff;
536         $transaction->{umsatz} += $diff;
537         $absumsatz             -= $diff;
538         $correction             = 1;
539
540         last;
541       }
542
543       $idx++;
544     }
545
546     $absumsatz = $form->round_amount($absumsatz, 2);
547     if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
548       push @errors, "Datev-Export fehlgeschlagen! Bei Transaktion $trans->[0]->{trans_id} ($absumsatz)\n";
549
550     } elsif (abs($absumsatz) >= 0.01) {
551       push @{ $form->{net_gross_differences} }, $absumsatz;
552       $form->{sum_net_gross_differences} += $absumsatz;
553     }
554   }
555
556   $sth->finish();
557
558   $form->error(join("<br>\n", @errors)) if (@errors);
559
560   $main::lxdebug->leave_sub();
561 }
562
563 sub make_kne_data_header {
564   $main::lxdebug->enter_sub();
565
566   my ($myconfig, $form, $fromto, $start_jahr) = @_;
567   my ($primanota);
568
569   my $jahr = $start_jahr;
570   if (!$jahr) {
571     my @a = localtime;
572     $jahr = $a[5];
573   }
574
575   #Header
576   my $header  = "\x1D\x181";
577   $header    .= _fill($form->{datentraegernr}, 3, ' ', 'left');
578   $header    .= ($fromto) ? "11" : "13"; # Anwendungsnummer
579   $header    .= _fill($form->{dfvkz}, 2, '0');
580   $header    .= _fill($form->{beraternr}, 7, '0');
581   $header    .= _fill($form->{mandantennr}, 5, '0');
582   $header    .= _fill($form->{abrechnungsnr} . $jahr, 6, '0');
583
584   $fromto         =~ s/transdate|>=|and|\'|<=//g;
585   my ($from, $to) =  split /   /, $fromto;
586   $from           =~ s/ //g;
587   $to             =~ s/ //g;
588
589   if ($from ne "") {
590     my ($fday, $fmonth, $fyear) = split(/\./, $from);
591     if (length($fmonth) < 2) {
592       $fmonth = "0" . $fmonth;
593     }
594     if (length($fday) < 2) {
595       $fday = "0" . $fday;
596     }
597     $from = $fday . $fmonth . substr($fyear, -2, 2);
598   } else {
599     $from = "";
600   }
601
602   $header .= $from;
603
604   if ($to ne "") {
605     my ($tday, $tmonth, $tyear) = split(/\./, $to);
606     if (length($tmonth) < 2) {
607       $tmonth = "0" . $tmonth;
608     }
609     if (length($tday) < 2) {
610       $tday = "0" . $tday;
611     }
612     $to = $tday . $tmonth . substr($tyear, -2, 2);
613   } else {
614     $to = "";
615   }
616   $header .= $to;
617
618   if ($fromto ne "") {
619     $primanota = "001";
620     $header .= $primanota;
621   }
622
623   $header .= _fill($form->{passwort}, 4, '0');
624   $header .= " " x 16;       # Anwendungsinfo
625   $header .= " " x 16;       # Inputinfo
626   $header .= "\x79";
627
628   #Versionssatz
629   my $versionssatz  = $form->{exporttype} == 0 ? "\xB5" . "1," : "\xB6" . "1,";
630
631   my $dbh           = $form->get_standard_dbh($myconfig);
632   my $query         = qq|SELECT accno FROM chart LIMIT 1|;
633   my $ref           = selectfirst_hashref_query($form, $dbh, $query);
634
635   $versionssatz    .= length $ref->{accno};
636   $versionssatz    .= ",";
637   $versionssatz    .= length $ref->{accno};
638   $versionssatz    .= ",SELF" . "\x1C\x79";
639
640   $header          .= $versionssatz;
641
642   $main::lxdebug->leave_sub();
643
644   return $header;
645 }
646
647 sub datetofour {
648   $main::lxdebug->enter_sub();
649
650   my ($date, $six) = @_;
651
652   my ($day, $month, $year) = split(/\./, $date);
653
654   if ($day =~ /^0/) {
655     $day = substr($day, 1, 1);
656   }
657   if (length($month) < 2) {
658     $month = "0" . $month;
659   }
660   if (length($year) > 2) {
661     $year = substr($year, -2, 2);
662   }
663
664   if ($six) {
665     $date = $day . $month . $year;
666   } else {
667     $date = $day . $month;
668   }
669
670   $main::lxdebug->leave_sub();
671
672   return $date;
673 }
674
675 sub trim_leading_zeroes {
676   my $str = shift;
677
678   $str =~ s/^0+//g;
679
680   return $str;
681 }
682
683 sub make_ed_versionset {
684   $main::lxdebug->enter_sub();
685
686   my ($header, $filename, $blockcount, $fromto) = @_;
687
688   my $versionset  = "V" . substr($filename, 2, 5);
689   $versionset    .= substr($header, 6, 22);
690
691   if ($fromto ne "") {
692     $versionset .= "0000" . substr($header, 28, 19);
693   } else {
694     my $datum = " " x 16;
695     $versionset .= $datum . "001" . substr($header, 28, 4);
696   }
697
698   $versionset .= _fill($blockcount, 5, '0');
699   $versionset .= "001";
700   $versionset .= " 1";
701   $versionset .= substr($header, -12, 10) . "    ";
702   $versionset .= " " x 53;
703
704   $main::lxdebug->leave_sub();
705
706   return $versionset;
707 }
708
709 sub make_ev_header {
710   $main::lxdebug->enter_sub();
711
712   my ($form, $fileno) = @_;
713
714   my $ev_header  = _fill($form->{datentraegernr}, 3, ' ', 'left');
715   $ev_header    .= "   ";
716   $ev_header    .= _fill($form->{beraternr}, 7, ' ', 'left');
717   $ev_header    .= _fill($form->{beratername}, 9, ' ', 'left');
718   $ev_header    .= " ";
719   $ev_header    .= (_fill($fileno, 5, '0')) x 2;
720   $ev_header    .= " " x 95;
721
722   $main::lxdebug->leave_sub();
723
724   return $ev_header;
725 }
726
727 sub kne_buchungsexport {
728   $main::lxdebug->enter_sub();
729
730   my ($myconfig, $form) = @_;
731
732   my @filenames;
733
734   my $export_path = _get_export_path() . "/";
735   my $filename    = "ED00000";
736   my $evfile      = "EV01";
737   my @ed_versionset;
738   my $fileno = 0;
739
740   $form->header;
741   print qq|
742   <html>
743   <body>Export in Bearbeitung<br>
744   Buchungss&auml;tze verarbeitet:
745 |;
746
747   my ($fromto, $start_jahr) =
748     &get_dates($form->{zeitraum}, $form->{monat},
749                $form->{quartal},  $form->{transdatefrom},
750                $form->{transdateto});
751   _get_transactions($fromto);
752   my $counter = 0;
753   print qq|<br>2. Durchlauf:|;
754   while (scalar(@{ $form->{DATEV} })) {
755     my $umsatzsumme = 0;
756     $filename++;
757     my $ed_filename = $export_path . $filename;
758     push(@filenames, $filename);
759     my $header = &make_kne_data_header($myconfig, $form, $fromto, $start_jahr);
760
761     my $kne_file = SL::DATEV::KNEFile->new();
762     $kne_file->add_block($header);
763
764     while (scalar(@{ $form->{DATEV} }) > 0) {
765       my $transaction = shift @{ $form->{DATEV} };
766       my $trans_lines = scalar(@{$transaction});
767       $counter++;
768       if (($counter % 500) == 0) {
769         print("$counter ");
770       }
771
772       my $umsatz         = 0;
773       my $gegenkonto     = "";
774       my $konto          = "";
775       my $belegfeld1     = "";
776       my $datum          = "";
777       my $waehrung       = "";
778       my $buchungstext   = "";
779       my $belegfeld2     = "";
780       my $datevautomatik = 0;
781       my $taxkey         = 0;
782       my $charttax       = 0;
783       my ($haben, $soll);
784       my $iconv          = $::locale->{iconv_utf8};
785       my %umlaute = ($iconv->convert('ä') => 'ae',
786                      $iconv->convert('ö') => 'oe',
787                      $iconv->convert('ü') => 'ue',
788                      $iconv->convert('Ä') => 'Ae',
789                      $iconv->convert('Ö') => 'Oe',
790                      $iconv->convert('Ü') => 'Ue',
791                      $iconv->convert('ß') => 'sz');
792       for (my $i = 0; $i < $trans_lines; $i++) {
793         if ($trans_lines == 2) {
794           if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
795             $umsatz = $transaction->[$i]->{'amount'};
796           }
797         } else {
798           if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
799             $umsatz = $transaction->[$i]->{'umsatz'};
800           }
801         }
802         if ($transaction->[$i]->{'datevautomatik'}) {
803           $datevautomatik = 1;
804         }
805         if ($transaction->[$i]->{'taxkey'}) {
806           $taxkey = $transaction->[$i]->{'taxkey'};
807         }
808         if ($transaction->[$i]->{'charttax'}) {
809           $charttax = $transaction->[$i]->{'charttax'};
810         }
811         if ($transaction->[$i]->{'amount'} > 0) {
812           $haben = $i;
813         } else {
814           $soll = $i;
815         }
816       }
817
818       # Umwandlung von Umlauten und Sonderzeichen in erlaubte Zeichen bei Textfeldern
819       foreach my $umlaut (keys(%umlaute)) {
820         $transaction->[$haben]->{'invnumber'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
821         $transaction->[$haben]->{'name'}      =~ s/${umlaut}/${umlaute{$umlaut}}/g;
822       }
823
824       $transaction->[$haben]->{'invnumber'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
825       $transaction->[$haben]->{'name'}      =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
826
827       $transaction->[$haben]->{'invnumber'} =  substr($transaction->[$haben]->{'invnumber'}, 0, 12);
828       $transaction->[$haben]->{'name'}      =  substr($transaction->[$haben]->{'name'}, 0, 30);
829       $transaction->[$haben]->{'invnumber'} =~ s/\ *$//;
830       $transaction->[$haben]->{'name'}      =~ s/\ *$//;
831
832       if ($trans_lines >= 2) {
833
834         $gegenkonto = "a" . trim_leading_zeroes($transaction->[$haben]->{'accno'});
835         $konto      = "e" . trim_leading_zeroes($transaction->[$soll]->{'accno'});
836         if ($transaction->[$haben]->{'invnumber'} ne "") {
837           $belegfeld1 = "\xBD" . $transaction->[$haben]->{'invnumber'} . "\x1C";
838         }
839         $datum = "d";
840         $datum .= &datetofour($transaction->[$haben]->{'transdate'}, 0);
841         $waehrung = "\xB3" . "EUR" . "\x1C";
842         if ($transaction->[$haben]->{'name'} ne "") {
843           $buchungstext = "\x1E" . $transaction->[$haben]->{'name'} . "\x1C";
844         }
845         if ($transaction->[$haben]->{'duedate'} ne "") {
846           $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
847         }
848       }
849
850       $umsatz       = $kne_file->format_amount(abs($umsatz), 0);
851       $umsatzsumme += $umsatz;
852       $kne_file->add_block("+" . $umsatz);
853
854       # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
855       # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
856       # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
857       # Skript angelegt werden.
858       # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
859       # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
860       # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
861       # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
862       # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
863       #
864       if (   ( $datevautomatik || $taxkey)
865           && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
866 #         $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
867         $kne_file->add_block("\x6C${taxkey}");
868       }
869
870       $kne_file->add_block($gegenkonto);
871       $kne_file->add_block($belegfeld1);
872       $kne_file->add_block($belegfeld2);
873       $kne_file->add_block($datum);
874       $kne_file->add_block($konto);
875       $kne_file->add_block($buchungstext);
876       $kne_file->add_block($waehrung . "\x79");
877     }
878
879     my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
880
881     $kne_file->add_block($mandantenendsumme);
882     $kne_file->flush();
883
884     open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
885     print(ED $kne_file->get_data());
886     close(ED);
887
888     $ed_versionset[$fileno] = &make_ed_versionset($header, $filename, $kne_file->get_block_count(), $fromto);
889     $fileno++;
890   }
891
892   #Make EV Verwaltungsdatei
893   my $ev_header = &make_ev_header($form, $fileno);
894   my $ev_filename = $export_path . $evfile;
895   push(@filenames, $evfile);
896   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
897   print(EV $ev_header);
898
899   foreach my $file (@ed_versionset) {
900     print(EV $ed_versionset[$file]);
901   }
902   close(EV);
903   print qq|<br>Done. <br>
904 |;
905   ###
906   $main::lxdebug->leave_sub();
907
908   return { 'download_token' => get_download_token_for_path($export_path), 'filenames' => \@filenames };
909 }
910
911 sub kne_stammdatenexport {
912   $main::lxdebug->enter_sub();
913
914   my ($myconfig, $form) = @_;
915   $form->{abrechnungsnr} = "99";
916
917   $form->header;
918   print qq|
919   <html>
920   <body>Export in Bearbeitung<br>
921 |;
922
923   my @filenames;
924
925   my $export_path = _get_export_path() . "/";
926   my $filename    = "ED00000";
927   my $evfile      = "EV01";
928   my @ed_versionset;
929   my $fileno          = 1;
930   my $i               = 0;
931   my $blockcount      = 1;
932   my $remaining_bytes = 256;
933   my $total_bytes     = 256;
934   my $buchungssatz    = "";
935   $filename++;
936   my $ed_filename = $export_path . $filename;
937   push(@filenames, $filename);
938   open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
939   my $header = &make_kne_data_header($myconfig, $form, "");
940   $remaining_bytes -= length($header);
941
942   my $fuellzeichen;
943   our $fromto;
944
945   # connect to database
946   my $dbh = $form->dbconnect($myconfig);
947
948   my (@where, @values) = ((), ());
949   if ($form->{accnofrom}) {
950     push @where, 'c.accno >= ?';
951     push @values, $form->{accnofrom};
952   }
953   if ($form->{accnoto}) {
954     push @where, 'c.accno <= ?';
955     push @values, $form->{accnoto};
956   }
957
958   my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
959
960   my $query     = qq|SELECT c.accno, c.description
961                      FROM chart c
962                      $where_str
963                      ORDER BY c.accno|;
964
965   my $sth = $dbh->prepare($query);
966   $sth->execute(@values) || $form->dberror($query);
967
968   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
969     if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
970       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
971       $buchungssatz .= "\x00" x $fuellzeichen;
972       $blockcount++;
973       $total_bytes = ($blockcount) * 256;
974     }
975     $buchungssatz .= "t" . $ref->{'accno'};
976     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
977     $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
978     $ref->{'description'} = substr($ref->{'description'}, 0, 40);
979     $ref->{'description'} =~ s/\ *$//;
980
981     if (
982         ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
983         ) <= 6
984       ) {
985       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
986       $buchungssatz .= "\x00" x $fuellzeichen;
987       $blockcount++;
988       $total_bytes = ($blockcount) * 256;
989     }
990     $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
991     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
992   }
993
994   $sth->finish;
995   print(ED $header);
996   print(ED $buchungssatz);
997   $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
998   my $dateiende = "\x00" x $fuellzeichen;
999   print(ED "z");
1000   print(ED $dateiende);
1001   close(ED);
1002
1003   #Make EV Verwaltungsdatei
1004   $ed_versionset[0] =
1005     &make_ed_versionset($header, $filename, $blockcount, $fromto);
1006
1007   my $ev_header = &make_ev_header($form, $fileno);
1008   my $ev_filename = $export_path . $evfile;
1009   push(@filenames, $evfile);
1010   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
1011   print(EV $ev_header);
1012
1013   foreach my $file (@ed_versionset) {
1014     print(EV $ed_versionset[$file]);
1015   }
1016   close(EV);
1017
1018   $dbh->disconnect;
1019   ###
1020
1021   print qq|<br>Done. <br>
1022 |;
1023
1024   $main::lxdebug->leave_sub();
1025
1026   return { 'download_token' => get_download_token_for_path($export_path), 'filenames' => \@filenames };
1027 }
1028
1029 1;