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