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