Merge branch 'master' into currency
[kivitendo-erp.git] / SL / DATEV.pm
1 #=====================================================================
2 # kivitendo 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 SL::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 DateTime;
38 use Exporter qw(import);
39 use File::Path;
40 use List::Util qw(max sum);
41 use Time::HiRes qw(gettimeofday);
42
43 {
44   my $i = 0;
45   use constant {
46     DATEV_ET_BUCHUNGEN => $i++,
47     DATEV_ET_STAMM     => $i++,
48
49     DATEV_FORMAT_KNE   => $i++,
50     DATEV_FORMAT_OBE   => $i++,
51   };
52 }
53
54 my @export_constants = qw(DATEV_ET_BUCHUNGEN DATEV_ET_STAMM DATEV_FORMAT_KNE DATEV_FORMAT_OBE);
55 our @EXPORT_OK = (@export_constants);
56 our %EXPORT_TAGS = (CONSTANTS => [ @export_constants ]);
57
58
59 sub new {
60   my $class = shift;
61   my %data  = @_;
62
63   my $obj = bless {}, $class;
64
65   $obj->$_($data{$_}) for keys %data;
66
67   $obj;
68 }
69
70 sub exporttype {
71   my $self = shift;
72   $self->{exporttype} = $_[0] if @_;
73   return $self->{exporttype};
74 }
75
76 sub has_exporttype {
77   defined $_[0]->{exporttype};
78 }
79
80 sub format {
81   my $self = shift;
82   $self->{format} = $_[0] if @_;
83   return $self->{format};
84 }
85
86 sub has_format {
87   defined $_[0]->{format};
88 }
89
90 sub _get_export_path {
91   $main::lxdebug->enter_sub();
92
93   my ($a, $b) = gettimeofday();
94   my $path    = _get_path_for_download_token("${a}-${b}-${$}");
95
96   mkpath($path) unless (-d $path);
97
98   $main::lxdebug->leave_sub();
99
100   return $path;
101 }
102
103 sub _get_path_for_download_token {
104   $main::lxdebug->enter_sub();
105
106   my $token = shift || '';
107   my $path;
108
109   if ($token =~ m|^(\d+)-(\d+)-(\d+)$|) {
110     $path = $::lx_office_conf{paths}->{userspath} . "/datev-export-${1}-${2}-${3}/";
111   }
112
113   $main::lxdebug->leave_sub();
114
115   return $path;
116 }
117
118 sub _get_download_token_for_path {
119   $main::lxdebug->enter_sub();
120
121   my $path = shift;
122   my $token;
123
124   if ($path =~ m|.*datev-export-(\d+)-(\d+)-(\d+)/?$|) {
125     $token = "${1}-${2}-${3}";
126   }
127
128   $main::lxdebug->leave_sub();
129
130   return $token;
131 }
132
133 sub download_token {
134   my $self = shift;
135   $self->{download_token} = $_[0] if @_;
136   return $self->{download_token} ||= _get_download_token_for_path($self->export_path);
137 }
138
139 sub export_path {
140   my ($self) = @_;
141
142   return  $self->{export_path} ||= _get_path_for_download_token($self->{download_token}) || _get_export_path();
143 }
144
145 sub add_filenames {
146   my $self = shift;
147   push @{ $self->{filenames} ||= [] }, @_;
148 }
149
150 sub filenames {
151   return @{ $_[0]{filenames} || [] };
152 }
153
154 sub add_error {
155   my $self = shift;
156   push @{ $self->{errors} ||= [] }, @_;
157 }
158
159 sub errors {
160   return @{ $_[0]{errors} || [] };
161 }
162
163 sub add_net_gross_differences {
164   my $self = shift;
165   push @{ $self->{net_gross_differences} ||= [] }, @_;
166 }
167
168 sub net_gross_differences {
169   return @{ $_[0]{net_gross_differences} || [] };
170 }
171
172 sub sum_net_gross_differences {
173   return sum $_[0]->net_gross_differences;
174 }
175
176 sub from {
177  my $self = shift;
178
179  if (@_) {
180    $self->{from} = $_[0];
181  }
182
183  return $self->{from};
184 }
185
186 sub to {
187  my $self = shift;
188
189  if (@_) {
190    $self->{to} = $_[0];
191  }
192
193  return $self->{to};
194 }
195
196 sub accnofrom {
197  my $self = shift;
198
199  if (@_) {
200    $self->{accnofrom} = $_[0];
201  }
202
203  return $self->{accnofrom};
204 }
205
206 sub accnoto {
207  my $self = shift;
208
209  if (@_) {
210    $self->{accnoto} = $_[0];
211  }
212
213  return $self->{accnoto};
214 }
215
216
217 sub dbh {
218   my $self = shift;
219
220   if (@_) {
221     $self->{dbh} = $_[0];
222     $self->{provided_dbh} = 1;
223   }
224
225   $self->{dbh} ||= $::form->get_standard_dbh;
226 }
227
228 sub provided_dbh {
229   $_[0]{provided_dbh};
230 }
231
232 sub clean_temporary_directories {
233   $::lxdebug->enter_sub;
234
235   foreach my $path (glob($::lx_office_conf{paths}->{userspath} . "/datev-export-*")) {
236     next unless -d $path;
237
238     my $mtime = (stat($path))[9];
239     next if ((time() - $mtime) < 8 * 60 * 60);
240
241     rmtree $path;
242   }
243
244   $::lxdebug->leave_sub;
245 }
246
247 sub _fill {
248   $main::lxdebug->enter_sub();
249
250   my $text      = shift;
251   my $field_len = shift;
252   my $fill_char = shift;
253   my $alignment = shift || 'right';
254
255   my $text_len  = length $text;
256
257   if ($field_len < $text_len) {
258     $text = substr $text, 0, $field_len;
259
260   } elsif ($field_len > $text_len) {
261     my $filler = ($fill_char) x ($field_len - $text_len);
262     $text      = $alignment eq 'right' ? $filler . $text : $text . $filler;
263   }
264
265   $main::lxdebug->leave_sub();
266
267   return $text;
268 }
269
270 sub get_datev_stamm {
271   return $_[0]{stamm} ||= selectfirst_hashref_query($::form, $_[0]->dbh, 'SELECT * FROM datev');
272 }
273
274 sub save_datev_stamm {
275   my ($self, $data) = @_;
276
277   do_query($::form, $self->dbh, 'DELETE FROM datev');
278
279   my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
280
281   my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
282   do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
283
284   $self->dbh->commit unless $self->provided_dbh;
285 }
286
287 sub export {
288   my ($self) = @_;
289   my $result;
290
291   die 'no format set!' unless $self->has_format;
292
293   if ($self->format == DATEV_FORMAT_KNE) {
294     $result = $self->kne_export;
295   } elsif ($self->format == DATEV_FORMAT_OBE) {
296     $result = $self->obe_export;
297   } else {
298     die 'unrecognized export format';
299   }
300
301   return $result;
302 }
303
304 sub kne_export {
305   my ($self) = @_;
306   my $result;
307
308   die 'no exporttype set!' unless $self->has_exporttype;
309
310   if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
311     $result = $self->kne_buchungsexport;
312   } elsif ($self->exporttype == DATEV_ET_STAMM) {
313     $result = $self->kne_stammdatenexport;
314   } else {
315     die 'unrecognized exporttype';
316   }
317
318   return $result;
319 }
320
321 sub obe_export {
322   die 'not yet implemented';
323 }
324
325 sub fromto {
326   my ($self) = @_;
327
328   return unless $self->from && $self->to;
329
330   return "transdate >= '" . $self->from->to_lxoffice . "' and transdate <= '" . $self->to->to_lxoffice . "'";
331 }
332
333 sub _sign {
334   $_[0] <=> 0;
335 }
336
337 sub _get_transactions {
338   $main::lxdebug->enter_sub();
339   my $self     = shift;
340   my $fromto   =  shift;
341   my $progress_callback = shift || sub {};
342
343   my $form     =  $main::form;
344
345   my ($notsplitindex);
346
347   $fromto      =~ s/transdate/ac\.transdate/g;
348
349   my $taxkeys  = Taxkeys->new();
350   my $filter   = '';            # Useful for debugging purposes
351
352   my %all_taxchart_ids = selectall_as_map($form, $self->dbh, qq|SELECT DISTINCT chart_id, TRUE AS is_set FROM tax|, 'chart_id', 'is_set');
353
354   my $query    =
355     qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey,
356          ar.invnumber, ar.duedate, ar.amount as umsatz, ar.deliverydate,
357          ct.name,
358          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
359          ar.invoice
360        FROM acc_trans ac
361        LEFT JOIN ar          ON (ac.trans_id    = ar.id)
362        LEFT JOIN customer ct ON (ar.customer_id = ct.id)
363        LEFT JOIN chart c     ON (ac.chart_id    = c.id)
364        WHERE (ar.id IS NOT NULL)
365          AND $fromto
366          $filter
367
368        UNION ALL
369
370        SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey,
371          ap.invnumber, ap.duedate, ap.amount as umsatz, ap.deliverydate,
372          ct.name,
373          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
374          ap.invoice
375        FROM acc_trans ac
376        LEFT JOIN ap        ON (ac.trans_id  = ap.id)
377        LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
378        LEFT JOIN chart c   ON (ac.chart_id  = c.id)
379        WHERE (ap.id IS NOT NULL)
380          AND $fromto
381          $filter
382
383        UNION ALL
384
385        SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey,
386          gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz, NULL as deliverydate,
387          gl.description AS name,
388          c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
389          FALSE AS invoice
390        FROM acc_trans ac
391        LEFT JOIN gl      ON (ac.trans_id  = gl.id)
392        LEFT JOIN chart c ON (ac.chart_id  = c.id)
393        WHERE (gl.id IS NOT NULL)
394          AND $fromto
395          $filter
396
397        ORDER BY trans_id, acc_trans_id|;
398
399   my $sth = prepare_execute_query($form, $self->dbh, $query);
400   $self->{DATEV} = [];
401
402   my $counter = 0;
403   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
404     $counter++;
405     if (($counter % 500) == 0) {
406       $progress_callback->($counter);
407     }
408
409     my $trans    = [ $ref ];
410
411     my $count    = $ref->{amount};
412     my $firstrun = 1;
413
414     # if the amount of a booking in a group is smaller than 0.02, any tax
415     # amounts will likely be smaller than 1 cent, so go into subcent mode
416     my $subcent  = abs($count) < 0.02;
417
418     # records from acc_trans are ordered by trans_id and acc_trans_id
419     # first check for unbalanced ledger inside one trans_id
420     # there may be several groups inside a trans_id, e.g. the original booking and the payment
421     # each group individually should be exactly balanced and each group
422     # individually needs its own datev lines
423
424     # keep fetching new acc_trans lines until the end of a balanced group is reached
425     while (abs($count) > 0.01 || $firstrun || ($subcent && abs($count) > 0.005)) {
426       my $ref2 = $sth->fetchrow_hashref("NAME_lc");
427       last unless ($ref2);
428
429       # check if trans_id of current acc_trans line is still the same as the
430       # trans_id of the first line in group
431
432       if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
433         $self->add_error("Unbalanced ledger! old trans_id " . $trans->[0]->{trans_id} . " new trans_id " . $ref2->{trans_id} . " count $count");
434         return;
435       }
436
437       push @{ $trans }, $ref2;
438
439       $count    += $ref2->{amount};
440       $firstrun  = 0;
441     }
442
443     foreach my $i (0 .. scalar(@{ $trans }) - 1) {
444       my $ref        = $trans->[$i];
445       my $prev_ref   = 0 < $i ? $trans->[$i - 1] : undef;
446       if (   $all_taxchart_ids{$ref->{id}}
447           && ($ref->{link} =~ m/(?:AP_tax|AR_tax)/)
448           && (   ($prev_ref && $prev_ref->{taxkey} && (_sign($ref->{amount}) == _sign($prev_ref->{amount})))
449               || $ref->{invoice})) {
450         $ref->{is_tax} = 1;
451       }
452
453       if (   !$ref->{invoice}   # we have a non-invoice booking (=gl)
454           &&  $ref->{is_tax}    # that has "is_tax" set
455           && !($prev_ref->{is_tax})  # previous line wasn't is_tax
456           &&  (_sign($ref->{amount}) == _sign($prev_ref->{amount}))) {  # and sign same as previous sign
457         $trans->[$i - 1]->{tax_amount} = $ref->{amount};
458       }
459     }
460
461     my %taxid_taxkeys = ();
462     my $absumsatz     = 0;
463     if (scalar(@{$trans}) <= 2) {
464       push @{ $self->{DATEV} }, $trans;
465       next;
466     }
467
468     # determine at which array position the reference value (called absumsatz) is
469     # and which amount it has
470
471     for my $j (0 .. (scalar(@{$trans}) - 1)) {
472
473       # Three cases:
474       # 1: gl transaction (Dialogbuchung), invoice is false, no double split booking allowed
475
476       # 2: sales or vendor invoice (Verkaufs- und Einkaufsrechnung): invoice is
477       # true, instead of absumsatz use link AR/AP (there should only be one
478       # entry)
479
480       # 3. AR/AP transaction (Kreditoren- und Debitorenbuchung): invoice is false,
481       # instead of absumsatz use link AR/AP (there should only be one, so jump
482       # out of search as soon as you find it )
483
484       # case 1 and 2
485       # for gl-bookings no split is allowed and there is no AR/AP account, so we always use the maximum value as a reference
486       # for ap/ar bookings we can always search for AR/AP in link and use that
487       if ( ( not $trans->[$j]->{'invoice'} and abs($trans->[$j]->{'amount'}) > abs($absumsatz) )
488          or ($trans->[$j]->{'invoice'} and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP'))) {
489         $absumsatz     = $trans->[$j]->{'amount'};
490         $notsplitindex = $j;
491       }
492
493       # case 3
494       # Problem: we can't distinguish between AR and AP and normal invoices via boolean "invoice"
495       # for AR and AP transaction exit the loop as soon as an AR or AP account is found
496       # there must be only one AR or AP chart in the booking
497       if ( $trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP') {
498         $notsplitindex = $j;   # position in booking with highest amount
499         $absumsatz     = $trans->[$j]->{'amount'};
500         last;
501       };
502     }
503
504     my $ml             = ($trans->[0]->{'umsatz'} > 0) ? 1 : -1;
505     my $rounding_error = 0;
506     my @taxed;
507
508     # go through each line and determine if it is a tax booking or not
509     # skip all tax lines and notsplitindex line
510     # push all other accounts (e.g. income or expense) with corresponding taxkey
511
512     for my $j (0 .. (scalar(@{$trans}) - 1)) {
513       if (   ($j != $notsplitindex)
514           && !$trans->[$j]->{is_tax}
515           && (   $trans->[$j]->{'taxkey'} eq ""
516               || $trans->[$j]->{'taxkey'} eq "0"
517               || $trans->[$j]->{'taxkey'} eq "1"
518               || $trans->[$j]->{'taxkey'} eq "10"
519               || $trans->[$j]->{'taxkey'} eq "11")) {
520         my %new_trans = ();
521         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
522
523         $absumsatz               += $trans->[$j]->{'amount'};
524         $new_trans{'amount'}      = $trans->[$j]->{'amount'} * (-1);
525         $new_trans{'umsatz'}      = abs($trans->[$j]->{'amount'}) * $ml;
526         $trans->[$j]->{'umsatz'}  = abs($trans->[$j]->{'amount'}) * $ml;
527
528         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
529
530       } elsif (($j != $notsplitindex) && !$trans->[$j]->{is_tax}) {
531         my %tax_info = $taxkeys->get_full_tax_info('transdate' => $trans->[$j]->{transdate},
532                                                    'deliverydate' => $trans->[$j]->{deliverydate});
533
534         my %new_trans = ();
535         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
536
537         my $tax_rate              = $tax_info{taxkeys}->{ $trans->[$j]->{'taxkey'} }->{taxrate};
538         $new_trans{'net_amount'}  = $trans->[$j]->{'amount'} * -1;
539         $new_trans{'tax_rate'}    = 1 + $tax_rate;
540
541         if (!$trans->[$j]->{'invoice'}) {
542           $new_trans{'amount'}      = $form->round_amount(-1 * ($trans->[$j]->{amount} + $trans->[$j]->{tax_amount}), 2);
543           $new_trans{'umsatz'}      = abs($new_trans{'amount'}) * $ml;
544           $trans->[$j]->{'umsatz'}  = $new_trans{'umsatz'};
545           $absumsatz               += -1 * $new_trans{'amount'};
546
547         } else {
548           my $unrounded             = $trans->[$j]->{'amount'} * (1 + $tax_rate) * -1 + $rounding_error;
549           my $rounded               = $form->round_amount($unrounded, 2);
550
551           $rounding_error           = $unrounded - $rounded;
552           $new_trans{'amount'}      = $rounded;
553           $new_trans{'umsatz'}      = abs($rounded) * $ml;
554           $trans->[$j]->{'umsatz'}  = $new_trans{umsatz};
555           $absumsatz               -= $rounded;
556         }
557
558         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
559         push @taxed, $self->{DATEV}->[-1];
560       }
561     }
562
563     my $idx        = 0;
564     my $correction = 0;
565     while ((abs($absumsatz) >= 0.01) && (abs($absumsatz) < 1.00)) {
566       if ($idx >= scalar @taxed) {
567         last if (!$correction);
568
569         $correction = 0;
570         $idx        = 0;
571       }
572
573       my $transaction = $taxed[$idx]->[0];
574
575       my $old_amount     = $transaction->{amount};
576       my $old_correction = $correction;
577       my @possible_diffs;
578
579       if (!$transaction->{diff}) {
580         @possible_diffs = (0.01, -0.01);
581       } else {
582         @possible_diffs = ($transaction->{diff});
583       }
584
585       foreach my $diff (@possible_diffs) {
586         my $net_amount = $form->round_amount(($transaction->{amount} + $diff) / $transaction->{tax_rate}, 2);
587         next if ($net_amount != $transaction->{net_amount});
588
589         $transaction->{diff}    = $diff;
590         $transaction->{amount} += $diff;
591         $transaction->{umsatz} += $diff;
592         $absumsatz             -= $diff;
593         $correction             = 1;
594
595         last;
596       }
597
598       $idx++;
599     }
600
601     $absumsatz = $form->round_amount($absumsatz, 2);
602     if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
603       $self->add_error("Datev-Export fehlgeschlagen! Bei Transaktion $trans->[0]->{trans_id} ($absumsatz)");
604
605     } elsif (abs($absumsatz) >= 0.01) {
606       $self->add_net_gross_differences($absumsatz);
607     }
608   }
609
610   $sth->finish();
611
612   $::lxdebug->leave_sub;
613 }
614
615 sub make_kne_data_header {
616   $main::lxdebug->enter_sub();
617
618   my ($self, $form) = @_;
619   my ($primanota);
620
621   my $stamm = $self->get_datev_stamm;
622
623   my $jahr = $self->from ? $self->from->year : DateTime->today->year;
624
625   #Header
626   my $header  = "\x1D\x181";
627   $header    .= _fill($stamm->{datentraegernr}, 3, ' ', 'left');
628   $header    .= ($self->fromto) ? "11" : "13"; # Anwendungsnummer
629   $header    .= _fill($stamm->{dfvkz}, 2, '0');
630   $header    .= _fill($stamm->{beraternr}, 7, '0');
631   $header    .= _fill($stamm->{mandantennr}, 5, '0');
632   $header    .= _fill($stamm->{abrechnungsnr} . $jahr, 6, '0');
633
634   $header .= $self->from ? $self->from->strftime('%d%m%y') : '';
635   $header .= $self->to   ? $self->to->strftime('%d%m%y')   : '';
636
637   if ($self->fromto) {
638     $primanota = "001";
639     $header .= $primanota;
640   }
641
642   $header .= _fill($stamm->{passwort}, 4, '0');
643   $header .= " " x 16;       # Anwendungsinfo
644   $header .= " " x 16;       # Inputinfo
645   $header .= "\x79";
646
647   #Versionssatz
648   my $versionssatz  = $self->exporttype == DATEV_ET_BUCHUNGEN ? "\xB5" . "1," : "\xB6" . "1,";
649
650   my $query         = qq|SELECT accno FROM chart LIMIT 1|;
651   my $ref           = selectfirst_hashref_query($form, $self->dbh, $query);
652
653   $versionssatz    .= length $ref->{accno};
654   $versionssatz    .= ",";
655   $versionssatz    .= length $ref->{accno};
656   $versionssatz    .= ",SELF" . "\x1C\x79";
657
658   $header          .= $versionssatz;
659
660   $main::lxdebug->leave_sub();
661
662   return $header;
663 }
664
665 sub datetofour {
666   $main::lxdebug->enter_sub();
667
668   my ($date, $six) = @_;
669
670   my ($day, $month, $year) = split(/\./, $date);
671
672   if ($day =~ /^0/) {
673     $day = substr($day, 1, 1);
674   }
675   if (length($month) < 2) {
676     $month = "0" . $month;
677   }
678   if (length($year) > 2) {
679     $year = substr($year, -2, 2);
680   }
681
682   if ($six) {
683     $date = $day . $month . $year;
684   } else {
685     $date = $day . $month;
686   }
687
688   $main::lxdebug->leave_sub();
689
690   return $date;
691 }
692
693 sub trim_leading_zeroes {
694   my $str = shift;
695
696   $str =~ s/^0+//g;
697
698   return $str;
699 }
700
701 sub make_ed_versionset {
702   $main::lxdebug->enter_sub();
703
704   my ($self, $header, $filename, $blockcount) = @_;
705
706   my $versionset  = "V" . substr($filename, 2, 5);
707   $versionset    .= substr($header, 6, 22);
708
709   if ($self->fromto) {
710     $versionset .= "0000" . substr($header, 28, 19);
711   } else {
712     my $datum = " " x 16;
713     $versionset .= $datum . "001" . substr($header, 28, 4);
714   }
715
716   $versionset .= _fill($blockcount, 5, '0');
717   $versionset .= "001";
718   $versionset .= " 1";
719   $versionset .= substr($header, -12, 10) . "    ";
720   $versionset .= " " x 53;
721
722   $main::lxdebug->leave_sub();
723
724   return $versionset;
725 }
726
727 sub make_ev_header {
728   $main::lxdebug->enter_sub();
729
730   my ($self, $form, $fileno) = @_;
731
732   my $stamm = $self->get_datev_stamm;
733
734   my $ev_header  = _fill($stamm->{datentraegernr}, 3, ' ', 'left');
735   $ev_header    .= "   ";
736   $ev_header    .= _fill($stamm->{beraternr}, 7, ' ', 'left');
737   $ev_header    .= _fill($stamm->{beratername}, 9, ' ', 'left');
738   $ev_header    .= " ";
739   $ev_header    .= (_fill($fileno, 5, '0')) x 2;
740   $ev_header    .= " " x 95;
741
742   $main::lxdebug->leave_sub();
743
744   return $ev_header;
745 }
746
747 sub kne_buchungsexport {
748   $main::lxdebug->enter_sub();
749
750   my ($self) = @_;
751
752   my $form = $::form;
753
754   my @filenames;
755
756   my $filename    = "ED00000";
757   my $evfile      = "EV01";
758   my @ed_versionset;
759   my $fileno = 0;
760
761   my $fromto = $self->fromto;
762
763   $self->_get_transactions($fromto);
764
765   return if $self->errors;
766
767   my $counter = 0;
768
769   while (scalar(@{ $self->{DATEV} || [] })) {
770     my $umsatzsumme = 0;
771     $filename++;
772     my $ed_filename = $self->export_path . $filename;
773     push(@filenames, $filename);
774     my $header = $self->make_kne_data_header($form);
775
776     my $kne_file = SL::DATEV::KNEFile->new();
777     $kne_file->add_block($header);
778
779     while (scalar(@{ $self->{DATEV} }) > 0) {
780       my $transaction = shift @{ $self->{DATEV} };
781       my $trans_lines = scalar(@{$transaction});
782       $counter++;
783
784       my $umsatz         = 0;
785       my $gegenkonto     = "";
786       my $konto          = "";
787       my $belegfeld1     = "";
788       my $datum          = "";
789       my $waehrung       = "";
790       my $buchungstext   = "";
791       my $belegfeld2     = "";
792       my $datevautomatik = 0;
793       my $taxkey         = 0;
794       my $charttax       = 0;
795       my ($haben, $soll);
796       my $iconv          = $::locale->{iconv_utf8};
797       my %umlaute = ($iconv->convert('ä') => 'ae',
798                      $iconv->convert('ö') => 'oe',
799                      $iconv->convert('ü') => 'ue',
800                      $iconv->convert('Ä') => 'Ae',
801                      $iconv->convert('Ö') => 'Oe',
802                      $iconv->convert('Ü') => 'Ue',
803                      $iconv->convert('ß') => 'sz');
804       for (my $i = 0; $i < $trans_lines; $i++) {
805         if ($trans_lines == 2) {
806           if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
807             $umsatz = $transaction->[$i]->{'amount'};
808           }
809         } else {
810           if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
811             $umsatz = $transaction->[$i]->{'umsatz'};
812           }
813         }
814         if ($transaction->[$i]->{'datevautomatik'}) {
815           $datevautomatik = 1;
816         }
817         if ($transaction->[$i]->{'taxkey'}) {
818           $taxkey = $transaction->[$i]->{'taxkey'};
819         }
820         if ($transaction->[$i]->{'charttax'}) {
821           $charttax = $transaction->[$i]->{'charttax'};
822         }
823         if ($transaction->[$i]->{'amount'} > 0) {
824           $haben = $i;
825         } else {
826           $soll = $i;
827         }
828       }
829
830       # Umwandlung von Umlauten und Sonderzeichen in erlaubte Zeichen bei Textfeldern
831       foreach my $umlaut (keys(%umlaute)) {
832         $transaction->[$haben]->{'invnumber'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
833         $transaction->[$haben]->{'name'}      =~ s/${umlaut}/${umlaute{$umlaut}}/g;
834       }
835
836       $transaction->[$haben]->{'invnumber'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
837       $transaction->[$haben]->{'name'}      =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
838
839       $transaction->[$haben]->{'invnumber'} =  substr($transaction->[$haben]->{'invnumber'}, 0, 12);
840       $transaction->[$haben]->{'name'}      =  substr($transaction->[$haben]->{'name'}, 0, 30);
841       $transaction->[$haben]->{'invnumber'} =~ s/\ *$//;
842       $transaction->[$haben]->{'name'}      =~ s/\ *$//;
843
844       if ($trans_lines >= 2) {
845
846         $gegenkonto = "a" . trim_leading_zeroes($transaction->[$haben]->{'accno'});
847         $konto      = "e" . trim_leading_zeroes($transaction->[$soll]->{'accno'});
848         if ($transaction->[$haben]->{'invnumber'} ne "") {
849           $belegfeld1 = "\xBD" . $transaction->[$haben]->{'invnumber'} . "\x1C";
850         }
851         $datum = "d";
852         $datum .= &datetofour($transaction->[$haben]->{'transdate'}, 0);
853         $waehrung = "\xB3" . "EUR" . "\x1C";
854         if ($transaction->[$haben]->{'name'} ne "") {
855           $buchungstext = "\x1E" . $transaction->[$haben]->{'name'} . "\x1C";
856         }
857         if ($transaction->[$haben]->{'duedate'} ne "") {
858           $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
859         }
860       }
861
862       $umsatz       = $kne_file->format_amount(abs($umsatz), 0);
863       $umsatzsumme += $umsatz;
864       $kne_file->add_block("+" . $umsatz);
865
866       # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
867       # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
868       # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
869       # Skript angelegt werden.
870       # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
871       # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
872       # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
873       # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
874       # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
875       #
876       if (   ( $datevautomatik || $taxkey)
877           && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
878 #         $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
879         $kne_file->add_block("\x6C${taxkey}");
880       }
881
882       $kne_file->add_block($gegenkonto);
883       $kne_file->add_block($belegfeld1);
884       $kne_file->add_block($belegfeld2);
885       $kne_file->add_block($datum);
886       $kne_file->add_block($konto);
887       $kne_file->add_block($buchungstext);
888       $kne_file->add_block($waehrung . "\x79");
889     }
890
891     my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
892
893     $kne_file->add_block($mandantenendsumme);
894     $kne_file->flush();
895
896     open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
897     print(ED $kne_file->get_data());
898     close(ED);
899
900     $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
901     $fileno++;
902   }
903
904   #Make EV Verwaltungsdatei
905   my $ev_header = $self->make_ev_header($form, $fileno);
906   my $ev_filename = $self->export_path . $evfile;
907   push(@filenames, $evfile);
908   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
909   print(EV $ev_header);
910
911   foreach my $file (@ed_versionset) {
912     print(EV $ed_versionset[$file]);
913   }
914   close(EV);
915   ###
916
917   $self->add_filenames(@filenames);
918
919   $main::lxdebug->leave_sub();
920
921   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
922 }
923
924 sub kne_stammdatenexport {
925   $main::lxdebug->enter_sub();
926
927   my ($self) = @_;
928   my $form = $::form;
929
930   $self->get_datev_stamm->{abrechnungsnr} = "99";
931
932   my @filenames;
933
934   my $filename    = "ED00000";
935   my $evfile      = "EV01";
936   my @ed_versionset;
937   my $fileno          = 1;
938   my $i               = 0;
939   my $blockcount      = 1;
940   my $remaining_bytes = 256;
941   my $total_bytes     = 256;
942   my $buchungssatz    = "";
943   $filename++;
944   my $ed_filename = $self->export_path . $filename;
945   push(@filenames, $filename);
946   open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
947   my $header = $self->make_kne_data_header($form);
948   $remaining_bytes -= length($header);
949
950   my $fuellzeichen;
951
952   my (@where, @values) = ((), ());
953   if ($self->accnofrom) {
954     push @where, 'c.accno >= ?';
955     push @values, $self->accnofrom;
956   }
957   if ($self->accnoto) {
958     push @where, 'c.accno <= ?';
959     push @values, $self->accnoto;
960   }
961
962   my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
963
964   my $query     = qq|SELECT c.accno, c.description
965                      FROM chart c
966                      $where_str
967                      ORDER BY c.accno|;
968
969   my $sth = $self->dbh->prepare($query);
970   $sth->execute(@values) || $form->dberror($query);
971
972   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
973     if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
974       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
975       $buchungssatz .= "\x00" x $fuellzeichen;
976       $blockcount++;
977       $total_bytes = ($blockcount) * 256;
978     }
979     $buchungssatz .= "t" . $ref->{'accno'};
980     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
981     $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
982     $ref->{'description'} = substr($ref->{'description'}, 0, 40);
983     $ref->{'description'} =~ s/\ *$//;
984
985     if (
986         ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
987         ) <= 6
988       ) {
989       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
990       $buchungssatz .= "\x00" x $fuellzeichen;
991       $blockcount++;
992       $total_bytes = ($blockcount) * 256;
993     }
994     $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
995     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
996   }
997
998   $sth->finish;
999   print(ED $header);
1000   print(ED $buchungssatz);
1001   $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
1002   my $dateiende = "\x00" x $fuellzeichen;
1003   print(ED "z");
1004   print(ED $dateiende);
1005   close(ED);
1006
1007   #Make EV Verwaltungsdatei
1008   $ed_versionset[0] =
1009     $self->make_ed_versionset($header, $filename, $blockcount);
1010
1011   my $ev_header = $self->make_ev_header($form, $fileno);
1012   my $ev_filename = $self->export_path . $evfile;
1013   push(@filenames, $evfile);
1014   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
1015   print(EV $ev_header);
1016
1017   foreach my $file (@ed_versionset) {
1018     print(EV $ed_versionset[$file]);
1019   }
1020   close(EV);
1021
1022   $self->add_filenames(@filenames);
1023
1024   $main::lxdebug->leave_sub();
1025
1026   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
1027 }
1028
1029 sub DESTROY {
1030   clean_temporary_directories();
1031 }
1032
1033 1;
1034
1035 __END__
1036
1037 =encoding utf-8
1038
1039 =head1 NAME
1040
1041 SL::DATEV - kivitendo DATEV Export module
1042
1043 =head1 SYNOPSIS
1044
1045   use SL::DATEV qw(:CONSTANTS);
1046
1047   my $datev = SL::DATEV->new(
1048     exporttype => DATEV_ET_BUCHUNGEN,
1049     format     => DATEV_FORMAT_KNE,
1050     from       => $startdate,
1051     to         => $enddate,
1052   );
1053
1054   my $datev = SL::DATEV->new(
1055     exporttype => DATEV_ET_STAMM,
1056     format     => DATEV_FORMAT_KNE,
1057     accnofrom  => $start_account_number,
1058     accnoto    => $end_account_number,
1059   );
1060
1061   # get or set datev stamm
1062   my $hashref = $datev->get_datev_stamm;
1063   $datev->save_datev_stamm($hashref);
1064
1065   # manually clean up temporary directories
1066   $datev->clean_temporary_directories;
1067
1068   # export
1069   $datev->export;
1070
1071   if ($datev->errors) {
1072     die join "\n", $datev->error;
1073   }
1074
1075   # get relevant data for saving the export:
1076   my $dl_token = $datev->download_token;
1077   my $path     = $datev->export_path;
1078   my @files    = $datev->filenames;
1079
1080   # retrieving an export at a later time
1081   my $datev = SL::DATEV->new(
1082     download_token => $dl_token_from_user,
1083   );
1084
1085   my $path     = $datev->export_path;
1086   my @files    = glob("$path/*");
1087
1088 =head1 DESCRIPTION
1089
1090 This module implements the DATEV export standard. For usage see above.
1091
1092 =head1 FUNCTIONS
1093
1094 =over 4
1095
1096 =item new PARAMS
1097
1098 Generic constructor. See section attributes for information about hat to pass.
1099
1100 =item get_datev_stamm
1101
1102 Loads DATEV Stammdaten and returns as hashref.
1103
1104 =item save_datev_stamm HASHREF
1105
1106 Saves DATEV Stammdaten from provided hashref.
1107
1108 =item exporttype
1109
1110 See L<CONSTANTS> for possible values
1111
1112 =item has_exporttype
1113
1114 Returns true if an exporttype has been set. Without exporttype most report functions won't work.
1115
1116 =item format
1117
1118 Specifies the designated format of the export. Currently only KNE export is implemented.
1119
1120 See L<CONSTANTS> for possible values
1121
1122 =item has_format
1123
1124 Returns true if a format has been set. Without format most report functions won't work.
1125
1126 =item download_token
1127
1128 Returns a download token for this DATEV object.
1129
1130 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1131
1132 =item export_path
1133
1134 Returns an export_path for this DATEV object.
1135
1136 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1137
1138 =item filenames
1139
1140 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.
1141
1142 =item net_gross_differences
1143
1144 If there were any net gross differences during calculation they will be collected here.
1145
1146 =item sum_net_gross_differences
1147
1148 Sum of all differences.
1149
1150 =item clean_temporary_directories
1151
1152 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.
1153
1154 =item errors
1155
1156 Returns a list of errors that occured. If no errors occured, the export was a success.
1157
1158 =item export
1159
1160 Exports data. You have to have set L<exporttype> and L<format> or an error will
1161 occur. OBE exports are currently not implemented.
1162
1163 =back
1164
1165 =head1 ATTRIBUTES
1166
1167 This is a list of attributes set in either the C<new> or a method of the same name.
1168
1169 =over 4
1170
1171 =item dbh
1172
1173 Set a database handle to use in the process. This allows for an export to be
1174 done on a transaction in progress without committing first.
1175
1176 =item exporttype
1177
1178 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1179
1180 =item format
1181
1182 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1183
1184 =item download_token
1185
1186 Can be set on creation to retrieve a prior export for download.
1187
1188 =item from
1189
1190 =item to
1191
1192 Set boundary dates for the export. Currently thse MUST be set for the export to work.
1193
1194 =item accnofrom
1195
1196 =item accnoto
1197
1198 Set boundary account numbers for the export. Only useful for a stammdaten export.
1199
1200 =back
1201
1202 =head1 CONSTANTS
1203
1204 =head2 Supplied to L<exporttype>
1205
1206 =over 4
1207
1208 =item DATEV_ET_BUCHUNGEN
1209
1210 =item DATEV_ET_STAMM
1211
1212 =back
1213
1214 =head2 Supplied to L<format>.
1215
1216 =over 4
1217
1218 =item DATEV_FORMAT_KNE
1219
1220 =item DATEV_FORMAT_OBE
1221
1222 =back
1223
1224 =head1 ERROR HANDLING
1225
1226 This module will die in the following cases:
1227
1228 =over 4
1229
1230 =item *
1231
1232 No or unrecognized exporttype or format was provided for an export
1233
1234 =item *
1235
1236 OBE rxport was called, which is not yet implemented.
1237
1238 =item *
1239
1240 general I/O errors
1241
1242 =back
1243
1244 Errors that occur during th actual export will be collected in L<errors>. The following types can occur at the moment:
1245
1246 =over 4
1247
1248 =item *
1249
1250 C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occur.
1251
1252 =item *
1253
1254 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).>  This error occurs if a
1255 transaction could not be reliably sorted out, or had rounding errors over the acceptable threshold.
1256
1257 =back
1258
1259 =head1 BUGS AND CAVEATS
1260
1261 =over 4
1262
1263 =item *
1264
1265 Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to to get a working export.
1266
1267 =item *
1268
1269 OBE export is currently not implemented.
1270
1271 =back
1272
1273 =head1 TODO
1274
1275 - handling of export_path and download token is a bit dodgy, clean that up.
1276
1277 =head1 SEE ALSO
1278
1279 L<SL::DATEV::KNEFile>
1280
1281 =head1 AUTHORS
1282
1283 Philip Reetz E<lt>p.reetz@linet-services.deE<gt>,
1284
1285 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
1286
1287 Jan Büren E<lt>jan@lx-office-hosting.deE<gt>,
1288
1289 Geoffrey Richardson E<lt>information@lx-office-hosting.deE<gt>,
1290
1291 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,
1292
1293 Stephan Köhler
1294
1295 =cut