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