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