DATEV Export - Vorbereitung für CSV Export
[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 _get_transactions {
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 kne_buchungsexport {
825   $main::lxdebug->enter_sub();
826
827   my ($self) = @_;
828
829   my $form = $::form;
830
831   my @filenames;
832
833   my $filename    = "ED00000";
834   my $evfile      = "EV01";
835   my @ed_versionset;
836   my $fileno = 0;
837
838   my $fromto = $self->fromto;
839
840   $self->_get_transactions(from_to => $fromto);
841
842   return if $self->errors;
843
844   my $counter = 0;
845
846   while (scalar(@{ $self->{DATEV} || [] })) {
847     my $umsatzsumme = 0;
848     $filename++;
849     my $ed_filename = $self->export_path . $filename;
850     push(@filenames, $filename);
851
852     # transform $self->{DATEV} into an array of hashrefs containing all the
853     # necessary information for the actual DATEV export, storing it in @kne_lines.
854     my @kne_lines = ();
855     while (scalar(@{ $self->{DATEV} }) > 0) {
856       my %kne_data = ();
857       my $transaction = shift @{ $self->{DATEV} };
858       my $trans_lines = scalar(@{$transaction});
859       $counter++;
860
861       my $umsatz         = 0;
862       my $gegenkonto     = "";
863       my $konto          = "";
864       my $belegfeld1     = "";
865       my $datum          = "";
866       my $waehrung       = "";
867       my $buchungstext   = "";
868       my $belegfeld2     = "";
869       my $datevautomatik = 0;
870       my $taxkey         = 0;
871       my $charttax       = 0;
872       my $ustid          ="";
873       my ($haben, $soll);
874       for (my $i = 0; $i < $trans_lines; $i++) {
875         if ($trans_lines == 2) {
876           if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
877             $umsatz = $transaction->[$i]->{'amount'};
878           }
879         } else {
880           if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
881             $umsatz = $transaction->[$i]->{'umsatz'};
882           }
883         }
884         if ($transaction->[$i]->{'datevautomatik'}) {
885           $datevautomatik = 1;
886         }
887         if ($transaction->[$i]->{'taxkey'}) {
888           $taxkey = $transaction->[$i]->{'taxkey'};
889         }
890         if ($transaction->[$i]->{'charttax'}) {
891           $charttax = $transaction->[$i]->{'charttax'};
892         }
893         if ($transaction->[$i]->{'amount'} > 0) {
894           $haben = $i;
895         } else {
896           $soll = $i;
897         }
898       }
899
900       if ($trans_lines >= 2) {
901
902         $kne_data{'gegenkonto'} = $transaction->[$haben]->{'accno'};
903         $kne_data{'konto'}      = $transaction->[$soll]->{'accno'};
904         if ($transaction->[$haben]->{'invnumber'} ne "") {
905           $kne_data{belegfeld1} = $transaction->[$haben]->{'invnumber'};
906         }
907         $kne_data{datum} = $transaction->[$haben]->{'transdate'};
908         $kne_data{waehrung} = 'EUR';
909
910         if ($transaction->[$haben]->{'name'} ne "") {
911           $kne_data{buchungstext} = $transaction->[$haben]->{'name'};
912         }
913         if (($transaction->[$haben]->{'ustid'} // '') ne "") {
914           $kne_data{ustid} = $transaction->[$haben]->{'ustid'};
915         }
916         if (($transaction->[$haben]->{'duedate'} // '') ne "") {
917           $kne_data{belegfeld2} = $transaction->[$haben]->{'duedate'};
918         }
919       }
920
921       $kne_data{umsatz} = abs($umsatz); # sales invoices without tax have a different sign???
922       $umsatzsumme += $kne_data{umsatz}; #umsatz; # add the abs amount
923
924       # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
925       # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
926       # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
927       # Skript angelegt werden.
928       # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
929       # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
930       # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
931       # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
932       # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
933       #
934
935       # only set buchungsschluessel if the following conditions are met:
936       if (   ( $datevautomatik || $taxkey)
937           && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
938         # $kne_data{buchungsschluessel} = !$datevautomatik ? $taxkey : "4";
939         $kne_data{buchungsschluessel} = $taxkey;
940       }
941
942       push(@kne_lines, \%kne_data);
943     }
944
945     # the data in @kne_lines is now ready to be transformed to a kne file, or even to csv
946
947     my $iconv   = $::locale->{iconv_utf8};
948     my %umlaute = ($iconv->convert('ä') => 'ae',
949                    $iconv->convert('ö') => 'oe',
950                    $iconv->convert('ü') => 'ue',
951                    $iconv->convert('Ä') => 'Ae',
952                    $iconv->convert('Ö') => 'Oe',
953                    $iconv->convert('Ü') => 'Ue',
954                    $iconv->convert('ß') => 'sz');
955
956     my $header = $self->make_kne_data_header($form);
957
958     my $kne_file = SL::DATEV::KNEFile->new();
959     $kne_file->add_block($header);
960     # add the data from @kne_lines to the kne_file, formatting as needed
961     foreach my $kne (@kne_lines) {
962
963       $kne_file->add_block("+" . $kne_file->format_amount(abs($kne->{umsatz}), 0));
964
965       # only add buchungsschluessel if it was previously defined
966       $kne_file->add_block("\x6C" . $kne->{buchungsschluessel}) if defined $kne->{buchungsschluessel};
967
968       # ($kne->{gegenkonto}) = $kne->{gegenkonto} =~ /^(\d+)/;
969       $kne_file->add_block("a" . trim_leading_zeroes($kne->{gegenkonto}));
970
971       if ( $kne->{belegfeld1} ) {
972         my $invnumber = $kne->{belegfeld1};
973         foreach my $umlaut (keys(%umlaute)) {
974           $invnumber =~ s/${umlaut}/${umlaute{$umlaut}}/g;
975         }
976         $invnumber =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
977         $invnumber =  substr($invnumber, 0, 12);
978         $invnumber =~ s/\ *$//;
979         $kne_file->add_block("\xBD" . $invnumber . "\x1C");
980       }
981
982       $kne_file->add_block("\xBE" . &datetofour($kne->{belegfeld2},1) . "\x1C");
983
984       $kne_file->add_block("d" . &datetofour($kne->{datum},0));
985
986       # ($kne->{konto}) = $kne->{konto} =~ /^(\d+)/;
987       $kne_file->add_block("e" . trim_leading_zeroes($kne->{konto}));
988
989       my $name = $kne->{buchungstext};
990       foreach my $umlaut (keys(%umlaute)) {
991         $name =~ s/${umlaut}/${umlaute{$umlaut}}/g;
992       }
993       $name =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
994       $name =  substr($name, 0, 30);
995       $name =~ s/\ *$//;
996       $kne_file->add_block("\x1E" . $name . "\x1C");
997
998       $kne_file->add_block("\xBA" . $kne->{'ustid'}    . "\x1C") if $kne->{'ustid'};
999
1000       $kne_file->add_block("\xB3" . $kne->{'waehrung'} . "\x1C" . "\x79");
1001     };
1002
1003     $umsatzsumme          = $kne_file->format_amount(abs($umsatzsumme), 0);
1004     my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
1005
1006     $kne_file->add_block($mandantenendsumme);
1007     $kne_file->flush();
1008
1009     open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
1010     print(ED $kne_file->get_data());
1011     close(ED);
1012
1013     $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
1014     $fileno++;
1015   }
1016
1017   #Make EV Verwaltungsdatei
1018   my $ev_header = $self->make_ev_header($form, $fileno);
1019   my $ev_filename = $self->export_path . $evfile;
1020   push(@filenames, $evfile);
1021   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
1022   print(EV $ev_header);
1023
1024   foreach my $file (@ed_versionset) {
1025     print(EV $file);
1026   }
1027   close(EV);
1028   ###
1029
1030   $self->add_filenames(@filenames);
1031
1032   $main::lxdebug->leave_sub();
1033
1034   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
1035 }
1036
1037 sub kne_stammdatenexport {
1038   $main::lxdebug->enter_sub();
1039
1040   my ($self) = @_;
1041   my $form = $::form;
1042
1043   $self->get_datev_stamm->{abrechnungsnr} = "99";
1044
1045   my @filenames;
1046
1047   my $filename    = "ED00000";
1048   my $evfile      = "EV01";
1049   my @ed_versionset;
1050   my $fileno          = 1;
1051   my $i               = 0;
1052   my $blockcount      = 1;
1053   my $remaining_bytes = 256;
1054   my $total_bytes     = 256;
1055   my $buchungssatz    = "";
1056   $filename++;
1057   my $ed_filename = $self->export_path . $filename;
1058   push(@filenames, $filename);
1059   open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
1060   my $header = $self->make_kne_data_header($form);
1061   $remaining_bytes -= length($header);
1062
1063   my $fuellzeichen;
1064
1065   my (@where, @values) = ((), ());
1066   if ($self->accnofrom) {
1067     push @where, 'c.accno >= ?';
1068     push @values, $self->accnofrom;
1069   }
1070   if ($self->accnoto) {
1071     push @where, 'c.accno <= ?';
1072     push @values, $self->accnoto;
1073   }
1074
1075   my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
1076
1077   my $query     = qq|SELECT c.accno, c.description
1078                      FROM chart c
1079                      $where_str
1080                      ORDER BY c.accno|;
1081
1082   my $sth = $self->dbh->prepare($query);
1083   $sth->execute(@values) || $form->dberror($query);
1084
1085   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
1086     if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
1087       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
1088       $buchungssatz .= "\x00" x $fuellzeichen;
1089       $blockcount++;
1090       $total_bytes = ($blockcount) * 256;
1091     }
1092     $buchungssatz .= "t" . $ref->{'accno'};
1093     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
1094     $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
1095     $ref->{'description'} = substr($ref->{'description'}, 0, 40);
1096     $ref->{'description'} =~ s/\ *$//;
1097
1098     if (
1099         ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
1100         ) <= 6
1101       ) {
1102       $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
1103       $buchungssatz .= "\x00" x $fuellzeichen;
1104       $blockcount++;
1105       $total_bytes = ($blockcount) * 256;
1106     }
1107     $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
1108     $remaining_bytes = $total_bytes - length($buchungssatz . $header);
1109   }
1110
1111   $sth->finish;
1112   print(ED $header);
1113   print(ED $buchungssatz);
1114   $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
1115   my $dateiende = "\x00" x $fuellzeichen;
1116   print(ED "z");
1117   print(ED $dateiende);
1118   close(ED);
1119
1120   #Make EV Verwaltungsdatei
1121   $ed_versionset[0] =
1122     $self->make_ed_versionset($header, $filename, $blockcount);
1123
1124   my $ev_header = $self->make_ev_header($form, $fileno);
1125   my $ev_filename = $self->export_path . $evfile;
1126   push(@filenames, $evfile);
1127   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
1128   print(EV $ev_header);
1129
1130   foreach my $file (@ed_versionset) {
1131     print(EV $ed_versionset[$file]);
1132   }
1133   close(EV);
1134
1135   $self->add_filenames(@filenames);
1136
1137   $main::lxdebug->leave_sub();
1138
1139   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
1140 }
1141
1142 sub _format_accno {
1143   my ($accno) = @_;
1144   return $accno . ('0' x (6 - min(length($accno), 6)));
1145 }
1146
1147 sub csv_export_for_tax_accountant {
1148   my ($self) = @_;
1149
1150   $self->_get_transactions(from_to => $self->fromto);
1151
1152   foreach my $transaction (@{ $self->{DATEV} }) {
1153     foreach my $entry (@{ $transaction }) {
1154       $entry->{sortkey} = join '-', map { lc } (DateTime->from_kivitendo($entry->{transdate})->strftime('%Y%m%d'), $entry->{name}, $entry->{reference});
1155     }
1156   }
1157
1158   my %transactions =
1159     partition_by { $_->[0]->{table} }
1160     sort_by      { $_->[0]->{sortkey} }
1161     grep         { 2 == scalar(@{ $_ }) }
1162     @{ $self->{DATEV} };
1163
1164   my %column_defs = (
1165     acc_trans_id      => { 'text' => $::locale->text('ID'), },
1166     amount            => { 'text' => $::locale->text('Amount'), },
1167     credit_accname    => { 'text' => $::locale->text('Credit Account Name'), },
1168     credit_accno      => { 'text' => $::locale->text('Credit Account'), },
1169     debit_accname     => { 'text' => $::locale->text('Debit Account Name'), },
1170     debit_accno       => { 'text' => $::locale->text('Debit Account'), },
1171     invnumber         => { 'text' => $::locale->text('Reference'), },
1172     name              => { 'text' => $::locale->text('Name'), },
1173     notes             => { 'text' => $::locale->text('Notes'), },
1174     tax               => { 'text' => $::locale->text('Tax'), },
1175     taxkey            => { 'text' => $::locale->text('Taxkey'), },
1176     tax_accname       => { 'text' => $::locale->text('Tax Account Name'), },
1177     tax_accno         => { 'text' => $::locale->text('Tax Account'), },
1178     transdate         => { 'text' => $::locale->text('Invoice Date'), },
1179     vcnumber          => { 'text' => $::locale->text('Customer/Vendor Number'), },
1180   );
1181
1182   my @columns = qw(
1183     acc_trans_id name           vcnumber
1184     transdate    invnumber      amount
1185     debit_accno  debit_accname
1186     credit_accno credit_accname
1187     tax
1188     tax_accno    tax_accname    taxkey
1189     notes
1190   );
1191
1192   my %filenames_by_type = (
1193     ar => $::locale->text('AR Transactions'),
1194     ap => $::locale->text('AP Transactions'),
1195     gl => $::locale->text('GL Transactions'),
1196   );
1197
1198   my @filenames;
1199   foreach my $type (qw(ap ar)) {
1200     my %csvs = (
1201       invoices   => {
1202         content  => '',
1203         filename => sprintf('%s %s - %s.csv', $filenames_by_type{$type}, $self->from->to_kivitendo, $self->to->to_kivitendo),
1204         csv      => Text::CSV_XS->new({
1205           binary   => 1,
1206           eol      => "\n",
1207           sep_char => ";",
1208         }),
1209       },
1210       payments   => {
1211         content  => '',
1212         filename => sprintf('Zahlungen %s %s - %s.csv', $filenames_by_type{$type}, $self->from->to_kivitendo, $self->to->to_kivitendo),
1213         csv      => Text::CSV_XS->new({
1214           binary   => 1,
1215           eol      => "\n",
1216           sep_char => ";",
1217         }),
1218       },
1219     );
1220
1221     foreach my $csv (values %csvs) {
1222       $csv->{out} = IO::File->new($self->export_path . '/' . $csv->{filename}, '>:encoding(utf8)') ;
1223       $csv->{csv}->print($csv->{out}, [ map { $column_defs{$_}->{text} } @columns ]);
1224
1225       push @filenames, $csv->{filename};
1226     }
1227
1228     foreach my $transaction (@{ $transactions{$type} }) {
1229       my $is_payment     = any { $_->{link} =~ m{A[PR]_paid} } @{ $transaction };
1230       my $csv            = $is_payment ? $csvs{payments} : $csvs{invoices};
1231
1232       my ($soll, $haben) = map { $transaction->[$_] } ($transaction->[0]->{amount} > 0 ? (1, 0) : (0, 1));
1233       my $tax            = defined($soll->{tax_accno})  ? $soll : $haben;
1234       my $amount         = defined($soll->{net_amount}) ? $soll : $haben;
1235       $haben->{notes}    = ($haben->{memo} || $soll->{memo}) if $is_payment;
1236       $haben->{notes}  //= '';
1237       $haben->{notes}    =  SL::HTML::Util->strip($haben->{notes});
1238       $haben->{notes}    =~ s{\r}{}g;
1239       $haben->{notes}    =~ s{\n+}{ }g;
1240
1241       my %row            = (
1242         amount           => $::form->format_amount({ numberformat => '1000,00' }, abs($amount->{amount}), 2),
1243         debit_accno      => _format_accno($soll->{accno}),
1244         debit_accname    => $soll->{accname},
1245         credit_accno     => _format_accno($haben->{accno}),
1246         credit_accname   => $haben->{accname},
1247         tax              => $::form->format_amount({ numberformat => '1000,00' }, abs($amount->{amount}) - abs($amount->{net_amount}), 2),
1248         notes            => $haben->{notes},
1249         (map { ($_ => $tax->{$_})                    } qw(taxkey tax_accname tax_accno)),
1250         (map { ($_ => ($haben->{$_} // $soll->{$_})) } qw(acc_trans_id invnumber name vcnumber transdate)),
1251       );
1252
1253       $csv->{csv}->print($csv->{out}, [ map { $row{$_} } @columns ]);
1254     }
1255
1256     $_->{out}->close for values %csvs;
1257   }
1258
1259   $self->add_filenames(@filenames);
1260
1261   return { download_token => $self->download_token, filenames => \@filenames };
1262 }
1263
1264 sub DESTROY {
1265   clean_temporary_directories();
1266 }
1267
1268 1;
1269
1270 __END__
1271
1272 =encoding utf-8
1273
1274 =head1 NAME
1275
1276 SL::DATEV - kivitendo DATEV Export module
1277
1278 =head1 SYNOPSIS
1279
1280   use SL::DATEV qw(:CONSTANTS);
1281
1282   my $startdate = DateTime->new(year => 2014, month => 9, day => 1);
1283   my $enddate   = DateTime->new(year => 2014, month => 9, day => 31);
1284   my $datev = SL::DATEV->new(
1285     exporttype => DATEV_ET_BUCHUNGEN,
1286     format     => DATEV_FORMAT_KNE,
1287     from       => $startdate,
1288     to         => $enddate,
1289   );
1290
1291   # To only export transactions from a specific trans_id: (from and to are ignored)
1292   my $invoice = SL::DB::Manager::Invoice->find_by( invnumber => '216' );
1293   my $datev = SL::DATEV->new(
1294     exporttype => DATEV_ET_BUCHUNGEN,
1295     format     => DATEV_FORMAT_KNE,
1296     trans_id   => $invoice->trans_id,
1297   );
1298
1299   my $datev = SL::DATEV->new(
1300     exporttype => DATEV_ET_STAMM,
1301     format     => DATEV_FORMAT_KNE,
1302     accnofrom  => $start_account_number,
1303     accnoto    => $end_account_number,
1304   );
1305
1306   # get or set datev stamm
1307   my $hashref = $datev->get_datev_stamm;
1308   $datev->save_datev_stamm($hashref);
1309
1310   # manually clean up temporary directories older than 8 hours
1311   $datev->clean_temporary_directories;
1312
1313   # export
1314   $datev->export;
1315
1316   if ($datev->errors) {
1317     die join "\n", $datev->error;
1318   }
1319
1320   # get relevant data for saving the export:
1321   my $dl_token = $datev->download_token;
1322   my $path     = $datev->export_path;
1323   my @files    = $datev->filenames;
1324
1325   # retrieving an export at a later time
1326   my $datev = SL::DATEV->new(
1327     download_token => $dl_token_from_user,
1328   );
1329
1330   my $path     = $datev->export_path;
1331   my @files    = glob("$path/*");
1332
1333 =head1 DESCRIPTION
1334
1335 This module implements the DATEV export standard. For usage see above.
1336
1337 =head1 FUNCTIONS
1338
1339 =over 4
1340
1341 =item new PARAMS
1342
1343 Generic constructor. See section attributes for information about what to pass.
1344
1345 =item get_datev_stamm
1346
1347 Loads DATEV Stammdaten and returns as hashref.
1348
1349 =item save_datev_stamm HASHREF
1350
1351 Saves DATEV Stammdaten from provided hashref.
1352
1353 =item exporttype
1354
1355 See L<CONSTANTS> for possible values
1356
1357 =item has_exporttype
1358
1359 Returns true if an exporttype has been set. Without exporttype most report functions won't work.
1360
1361 =item format
1362
1363 Specifies the designated format of the export. Currently only KNE export is implemented.
1364
1365 See L<CONSTANTS> for possible values
1366
1367 =item has_format
1368
1369 Returns true if a format has been set. Without format most report functions won't work.
1370
1371 =item download_token
1372
1373 Returns a download token for this DATEV object.
1374
1375 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1376
1377 =item export_path
1378
1379 Returns an export_path for this DATEV object.
1380
1381 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1382
1383 =item filenames
1384
1385 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.
1386
1387 =item net_gross_differences
1388
1389 If there were any net gross differences during calculation they will be collected here.
1390
1391 =item sum_net_gross_differences
1392
1393 Sum of all differences.
1394
1395 =item clean_temporary_directories
1396
1397 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.
1398
1399 =item errors
1400
1401 Returns a list of errors that occured. If no errors occured, the export was a success.
1402
1403 =item export
1404
1405 Exports data. You have to have set L<exporttype> and L<format> or an error will
1406 occur. OBE exports are currently not implemented.
1407
1408 =back
1409
1410 =head1 ATTRIBUTES
1411
1412 This is a list of attributes set in either the C<new> or a method of the same name.
1413
1414 =over 4
1415
1416 =item dbh
1417
1418 Set a database handle to use in the process. This allows for an export to be
1419 done on a transaction in progress without committing first.
1420
1421 Note: If you don't want this code to commit, simply providing a dbh is not
1422 enough enymore. You'll have to wrap the call into a transaction yourself, so
1423 that the internal transaction does not commit.
1424
1425 =item exporttype
1426
1427 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1428
1429 =item format
1430
1431 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1432
1433 =item download_token
1434
1435 Can be set on creation to retrieve a prior export for download.
1436
1437 =item from
1438
1439 =item to
1440
1441 Set boundary dates for the export. Unless a trans_id is passed these MUST be
1442 set for the export to work.
1443
1444 =item trans_id
1445
1446 To check only one gl/ar/ap transaction, pass the trans_id. The attributes
1447 L<from> and L<to> are currently still needed for the query to be assembled
1448 correctly.
1449
1450 =item accnofrom
1451
1452 =item accnoto
1453
1454 Set boundary account numbers for the export. Only useful for a stammdaten export.
1455
1456 =back
1457
1458 =head1 CONSTANTS
1459
1460 =head2 Supplied to L<exporttype>
1461
1462 =over 4
1463
1464 =item DATEV_ET_BUCHUNGEN
1465
1466 =item DATEV_ET_STAMM
1467
1468 =back
1469
1470 =head2 Supplied to L<format>.
1471
1472 =over 4
1473
1474 =item DATEV_FORMAT_KNE
1475
1476 =item DATEV_FORMAT_OBE
1477
1478 =back
1479
1480 =head1 ERROR HANDLING
1481
1482 This module will die in the following cases:
1483
1484 =over 4
1485
1486 =item *
1487
1488 No or unrecognized exporttype or format was provided for an export
1489
1490 =item *
1491
1492 OBE export was called, which is not yet implemented.
1493
1494 =item *
1495
1496 general I/O errors
1497
1498 =back
1499
1500 Errors that occur during th actual export will be collected in L<errors>. The following types can occur at the moment:
1501
1502 =over 4
1503
1504 =item *
1505
1506 C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occur.
1507
1508 =item *
1509
1510 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).>  This error occurs if a
1511 transaction could not be reliably sorted out, or had rounding errors above the acceptable threshold.
1512
1513 =back
1514
1515 =head1 BUGS AND CAVEATS
1516
1517 =over 4
1518
1519 =item *
1520
1521 Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to in order to get a working export.
1522
1523 =item *
1524
1525 OBE export is currently not implemented.
1526
1527 =back
1528
1529 =head1 TODO
1530
1531 - handling of export_path and download token is a bit dodgy, clean that up.
1532
1533 =head1 SEE ALSO
1534
1535 L<SL::DATEV::KNEFile>
1536
1537 =head1 AUTHORS
1538
1539 Philip Reetz E<lt>p.reetz@linet-services.deE<gt>,
1540
1541 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
1542
1543 Jan Büren E<lt>jan@lx-office-hosting.deE<gt>,
1544
1545 Geoffrey Richardson E<lt>information@lx-office-hosting.deE<gt>,
1546
1547 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,
1548
1549 Stephan Köhler
1550
1551 =cut