]> wagnertech.de Git - mfinanz.git/blob - SL/DATEV.pm
restart apache2 in postinst
[mfinanz.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::CSV;
35 use SL::DB;
36 use Encode qw(encode);
37 use SL::HTML::Util ();
38 use SL::Iconv;
39 use SL::Locale::String qw(t8);
40 use SL::VATIDNr;
41
42 use Archive::Zip;
43 use Data::Dumper;
44 use DateTime;
45 use Exporter qw(import);
46 use File::Path;
47 use IO::File;
48 use List::MoreUtils qw(any);
49 use List::Util qw(min max sum);
50 use List::UtilsBy qw(partition_by sort_by);
51 use Text::CSV_XS;
52 use Time::HiRes qw(gettimeofday);
53 use XML::LibXML;
54
55 {
56   my $i = 0;
57   use constant {
58     DATEV_ET_BUCHUNGEN => $i++,
59     DATEV_ET_STAMM     => $i++,
60     DATEV_ET_CSV       => $i++,
61
62     DATEV_FORMAT_KNE   => $i++,
63     DATEV_FORMAT_OBE   => $i++,
64     DATEV_FORMAT_CSV   => $i++,
65   };
66 }
67
68 my @export_constants = qw(DATEV_ET_BUCHUNGEN DATEV_ET_STAMM DATEV_ET_CSV DATEV_FORMAT_KNE DATEV_FORMAT_OBE DATEV_FORMAT_CSV);
69 our @EXPORT_OK = (@export_constants);
70 our %EXPORT_TAGS = (CONSTANTS => [ @export_constants ]);
71
72
73 sub new {
74   my $class = shift;
75   my %data  = @_;
76
77   my $obj = bless {}, $class;
78
79   $obj->$_($data{$_}) for keys %data;
80
81   $obj;
82 }
83
84 sub exporttype {
85   my $self = shift;
86   $self->{exporttype} = $_[0] if @_;
87   return $self->{exporttype};
88 }
89
90 sub has_exporttype {
91   defined $_[0]->{exporttype};
92 }
93
94 sub format {
95   my $self = shift;
96   $self->{format} = $_[0] if @_;
97   return $self->{format};
98 }
99
100 sub has_format {
101   defined $_[0]->{format};
102 }
103
104 sub _get_export_path {
105   $main::lxdebug->enter_sub();
106
107   my ($a, $b) = gettimeofday();
108   my $path    = _get_path_for_download_token("${a}-${b}-${$}");
109
110   mkpath($path) unless (-d $path);
111
112   $main::lxdebug->leave_sub();
113
114   return $path;
115 }
116
117 sub _get_path_for_download_token {
118   $main::lxdebug->enter_sub();
119
120   my $token = shift || '';
121   my $path;
122
123   if ($token =~ m|^(\d+)-(\d+)-(\d+)$|) {
124     $path = $::lx_office_conf{paths}->{userspath} . "/datev-export-${1}-${2}-${3}/";
125   }
126
127   $main::lxdebug->leave_sub();
128
129   return $path;
130 }
131
132 sub _get_download_token_for_path {
133   $main::lxdebug->enter_sub();
134
135   my $path = shift;
136   my $token;
137
138   if ($path =~ m|.*datev-export-(\d+)-(\d+)-(\d+)/?$|) {
139     $token = "${1}-${2}-${3}";
140   }
141
142   $main::lxdebug->leave_sub();
143
144   return $token;
145 }
146
147 sub download_token {
148   my $self = shift;
149   $self->{download_token} = $_[0] if @_;
150   return $self->{download_token} ||= _get_download_token_for_path($self->export_path);
151 }
152
153 sub export_path {
154   my ($self) = @_;
155
156   return  $self->{export_path} ||= _get_path_for_download_token($self->{download_token}) || _get_export_path();
157 }
158
159 sub add_filenames {
160   my $self = shift;
161   push @{ $self->{filenames} ||= [] }, @_;
162 }
163
164 sub filenames {
165   return @{ $_[0]{filenames} || [] };
166 }
167
168 sub add_error {
169   my $self = shift;
170   push @{ $self->{errors} ||= [] }, @_;
171 }
172
173 sub errors {
174   return @{ $_[0]{errors} || [] };
175 }
176
177 sub add_net_gross_differences {
178   my $self = shift;
179   push @{ $self->{net_gross_differences} ||= [] }, @_;
180 }
181
182 sub net_gross_differences {
183   return @{ $_[0]{net_gross_differences} || [] };
184 }
185
186 sub sum_net_gross_differences {
187   return sum $_[0]->net_gross_differences;
188 }
189
190 sub from {
191  my $self = shift;
192
193  if (@_) {
194    die "Invalid type, need DateTime Object" unless ref $_[0] eq 'DateTime';
195    $self->{from} = $_[0];
196  }
197
198  return $self->{from};
199 }
200
201 sub to {
202  my $self = shift;
203
204  if (@_) {
205    die "Invalid type, need DateTime Object" unless ref $_[0] eq 'DateTime';
206    $self->{to} = $_[0];
207  }
208
209  return $self->{to};
210 }
211
212 sub trans_id {
213   my $self = shift;
214
215   if (@_) {
216     $self->{trans_id} = $_[0];
217   }
218
219   die "illegal trans_id passed for DATEV export: " . $self->{trans_id} . "\n" unless $self->{trans_id} =~ m/^\d+$/;
220
221   return $self->{trans_id};
222 }
223
224 sub warnings {
225   my $self = shift;
226
227   if (@_) {
228     $self->{warnings} = [@_];
229   } else {
230    return $self->{warnings};
231   }
232 }
233
234 sub use_pk {
235  my $self = shift;
236
237  if (@_) {
238    $self->{use_pk} = $_[0];
239  }
240
241  return $self->{use_pk};
242 }
243
244 sub accnofrom {
245  my $self = shift;
246
247  if (@_) {
248    $self->{accnofrom} = $_[0];
249  }
250
251  return $self->{accnofrom};
252 }
253
254 sub accnoto {
255  my $self = shift;
256
257  if (@_) {
258    $self->{accnoto} = $_[0];
259  }
260
261  return $self->{accnoto};
262 }
263
264
265 sub dbh {
266   my $self = shift;
267
268   if (@_) {
269     $self->{dbh} = $_[0];
270     $self->{provided_dbh} = 1;
271   }
272
273   $self->{dbh} ||= SL::DB->client->dbh;
274 }
275
276 sub provided_dbh {
277   $_[0]{provided_dbh};
278 }
279
280 sub clean_temporary_directories {
281   $::lxdebug->enter_sub;
282
283   foreach my $path (glob($::lx_office_conf{paths}->{userspath} . "/datev-export-*")) {
284     next unless -d $path;
285
286     my $mtime = (stat($path))[9];
287     next if ((time() - $mtime) < 8 * 60 * 60);
288
289     rmtree $path;
290   }
291
292   $::lxdebug->leave_sub;
293 }
294
295 sub get_datev_stamm {
296   return $_[0]{stamm} ||= selectfirst_hashref_query($::form, $_[0]->dbh, 'SELECT * FROM datev');
297 }
298
299 sub save_datev_stamm {
300   my ($self, $data) = @_;
301
302   SL::DB->client->with_transaction(sub {
303     do_query($::form, $self->dbh, 'DELETE FROM datev');
304
305     my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
306
307     my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
308     do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
309     1;
310   }) or do { die SL::DB->client->error };
311 }
312
313 sub export {
314   my ($self) = @_;
315
316   return $self->csv_export;
317 }
318
319 sub csv_export {
320   my ($self) = @_;
321   my $result;
322
323   die 'no exporttype set!' unless $self->has_exporttype;
324
325   if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
326
327     $self->generate_datev_data(from_to => $self->fromto);
328     return if $self->errors;
329
330     my $datev_csv = SL::DATEV::CSV->new(
331       datev_lines  => $self->generate_datev_lines,
332       from         => $self->from,
333       to           => $self->to,
334       locked       => $self->locked,
335     );
336
337
338     my $filename = "EXTF_DATEV_kivitendo" . $self->from->ymd() . '-' . $self->to->ymd() . ".csv";
339
340     my $csv = Text::CSV_XS->new({
341                 binary       => 1,
342                 sep_char     => ";",
343                 always_quote => 1,
344                 eol          => "\r\n",
345               }) or die "Cannot use CSV: ".Text::CSV_XS->error_diag();
346
347     # get encoding from defaults - use cp1252 if DATEV strict export is used
348     my $enc = ($::instance_conf->get_datev_export_format eq 'cp1252') ? 'cp1252' : 'utf-8';
349     my $csv_file = IO::File->new($self->export_path . '/' . $filename, ">:encoding($enc)") or die "Can't open: $!";
350
351     $csv->print($csv_file, $_) for @{ $datev_csv->header };
352     $csv->print($csv_file, $_) for @{ $datev_csv->lines  };
353     $csv_file->close;
354     $self->{warnings} = $datev_csv->warnings;
355
356     $self->_create_xml_and_documents if $self->{documents} && $self->{guids} && %{ $self->{guids} };
357
358     # convert utf-8 to cp1252//translit if set
359     if ($::instance_conf->get_datev_export_format eq 'cp1252-translit') {
360
361       my $filename_translit = "EXTF_DATEV_kivitendo_translit" . $self->from->ymd() . '-' . $self->to->ymd() . ".csv";
362       open my $fh_in,  '<:encoding(UTF-8)',  $self->export_path . '/' . $filename or die "could not open $filename for reading: $!";
363       open my $fh_out, '>', $self->export_path . '/' . $filename_translit         or die "could not open $filename_translit for writing: $!";
364
365       my $converter = SL::Iconv->new("utf-8", "cp1252//translit");
366
367       print $fh_out $converter->convert($_) while <$fh_in>;
368       close $fh_in;
369       close $fh_out;
370
371       unlink $self->export_path . '/' . $filename or warn "Could not unlink $filename: $!";
372       $filename = $filename_translit;
373     }
374
375     return { download_token => $self->download_token, filenames => $filename };
376
377   } else {
378     die 'unrecognized exporttype';
379   }
380
381   return $result;
382 }
383
384 sub fromto {
385   my ($self) = @_;
386
387   return unless $self->from && $self->to;
388
389   return "transdate >= '" . $self->from->to_lxoffice . "' and transdate <= '" . $self->to->to_lxoffice . "'";
390 }
391
392 sub _sign {
393   $_[0] <=> 0;
394 }
395
396 sub locked {
397  my $self = shift;
398
399  if (@_) {
400    $self->{locked} = $_[0];
401  }
402  return $self->{locked};
403 }
404
405 sub imported {
406  my $self = shift;
407
408  if (@_) {
409    $self->{imported} = $_[0];
410  }
411  return $self->{imported};
412 }
413
414 sub documents {
415  my $self = shift;
416
417  if (@_) {
418    $self->{documents} = $_[0];
419  }
420  return $self->{documents};
421 }
422
423 sub _create_xml_and_documents {
424   my $self = shift;
425
426   die "No guids" unless %{ $self->{guids} };
427
428   my $today = DateTime->now_local;
429   my $doc   = XML::LibXML::Document->new('1.0', 'utf-8');
430
431   my $root  = $doc->createElement('archive');
432   #<archive xmlns="http://xml.datev.de/bedi/tps/document/v05.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xml.datev.de/bedi/tps/document/v05.0 Document_v050.xsd" version="5.0" generatingSystem="DATEV-Musterdaten">
433
434   $root->setAttribute('xmlns'              => 'http://xml.datev.de/bedi/tps/document/v05.0');
435   $root->setAttribute('xmlns:xsi'          => 'http://www.w3.org/2001/XMLSchema-instance');
436   $root->setAttribute('xsi:schemaLocation' => 'http://xml.datev.de/bedi/tps/document/v05.0 Document_v050.xsd');
437   $root->setAttribute('version'            => '5.0');
438   $root->setAttribute('generatingSystem'   => 'kivitendo');
439
440   # header with timestamp
441   my $header_tag = $doc->createElement('header');
442   $root->appendChild($header_tag);
443   my $date_tag = $doc->createElement('date');
444   $date_tag->appendTextNode($today);
445   $header_tag->appendChild($date_tag);
446
447
448   # content
449   my $content_node = $doc->createElement('content');
450   $root->appendChild($content_node);
451   # we have n document childs
452   foreach my $guid (keys %{ $self->{guids} }) {
453     # 1. get filename and file location
454     my $file_version = SL::DB::Manager::FileVersion->find_by(guid => $guid);
455     die "Invalid guid $guid" unless ref $file_version eq 'SL::DB::FileVersion';
456     # file_name has to be unique add guid if needed
457     my $filename_for_zip = (exists $self->{files}{$file_version->file_name})
458                            ? $file_version->file_name . '__' . $guid
459                            : $file_version->file_name;
460     $filename_for_zip = $guid . '.pdf';
461     $self->{files}{$filename_for_zip} = $file_version->get_system_location;
462     # create xml metadata for files
463     my $document_node = $doc->createElement('document');
464     # set attr
465     $document_node->setAttribute('guid'      => $guid);
466     $document_node->setAttribute('processID' => '1');
467     $document_node->setAttribute('type'      => '1');
468     $content_node->appendChild($document_node);
469     my $extension_node = $doc->createElement('extension');
470     $extension_node->setAttribute('xsi:type' => 'File');
471     $extension_node->setAttribute('name'     => $filename_for_zip);
472     $document_node->appendChild($extension_node);
473   }
474   $doc->setDocumentElement($root);
475
476   # create Archive::Zip in Export Path
477   my $zip = Archive::Zip->new();
478   # add metadata document
479   $zip->addString($doc->toString(), 'document.xml');
480   # add real files
481   foreach my $filename (keys %{ $self->{files} }) {
482 #    my $enc_filename = encode('Windows-1252', $filename);
483     $zip->addFile($self->{files}{$filename}, $filename);
484   }
485   die "Cannot write Belege-XML.zip" unless ($zip->writeToFileNamed($self->export_path . 'Belege-XML.zip')
486                                             == Archive::Zip::AZ_OK());
487 }
488
489 sub generate_datev_data {
490   $main::lxdebug->enter_sub();
491
492   my ($self, %params)   = @_;
493   my $fromto            = $params{from_to} // '';
494   my $progress_callback = $params{progress_callback} || sub {};
495
496   my $form     =  $main::form;
497
498   my $trans_id_filter = '';
499   my $ar_department_id_filter = '';
500   my $ap_department_id_filter = '';
501   my $gl_department_id_filter = '';
502   if ( $form->{department_id} ) {
503     $ar_department_id_filter = " AND ar.department_id = ? ";
504     $ap_department_id_filter = " AND ap.department_id = ? ";
505     $gl_department_id_filter = " AND gl.department_id = ? ";
506   }
507
508   my ($gl_itime_filter, $ar_itime_filter, $ap_itime_filter);
509   if ( $form->{gldatefrom} ) {
510     $gl_itime_filter = " AND gl.itime >= ? ";
511     $ar_itime_filter = " AND ar.itime >= ? ";
512     $ap_itime_filter = " AND ap.itime >= ? ";
513   } else {
514     $gl_itime_filter = "";
515     $ar_itime_filter = "";
516     $ap_itime_filter = "";
517   }
518
519   if ( $self->{trans_id} ) {
520     # ignore dates when trans_id is passed so that the entire transaction is
521     # checked, not just either the initial bookings or the subsequent payments
522     # (the transdates will likely differ)
523     $fromto = '';
524     $trans_id_filter = 'ac.trans_id = ' . $self->trans_id;
525   } else {
526     $fromto      =~ s/transdate/ac\.transdate/g;
527   };
528
529   my ($notsplitindex);
530
531   my $filter   = '';            # Useful for debugging purposes
532
533   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');
534
535   my $ar_accno = "c.accno";
536   my $ap_accno = "c.accno";
537   if ( $self->use_pk ) {
538     $ar_accno = "CASE WHEN ac.chart_link = 'AR' THEN ct.customernumber ELSE c.accno END as accno";
539     $ap_accno = "CASE WHEN ac.chart_link = 'AP' THEN ct.vendornumber   ELSE c.accno END as accno";
540   }
541   my $gl_imported;
542   if ( !$self->imported ) {
543     $gl_imported = " AND NOT imported";
544   }
545
546   my $query    =
547     qq|SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,ar.id, ac.amount, ac.taxkey, ac.memo,
548          ar.invnumber, ar.duedate, ar.amount as umsatz, COALESCE(ar.tax_point, ar.deliverydate) AS deliverydate, ar.itime::date,
549          ct.name, ct.ustid, ct.customernumber AS vcnumber, ct.id AS customer_id, NULL AS vendor_id,
550          $ar_accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
551          ar.invoice,
552          t.rate AS taxrate, t.taxdescription,
553          'ar' as table,
554          tc.accno AS tax_accno, tc.description AS tax_accname,
555          ar.department_id,
556          ar.notes,
557          project.projectnumber as projectnumber, project.description as projectdescription,
558          department.description as departmentdescription
559        FROM acc_trans ac
560        LEFT JOIN ar          ON (ac.trans_id    = ar.id)
561        LEFT JOIN customer ct ON (ar.customer_id = ct.id)
562        LEFT JOIN chart c     ON (ac.chart_id    = c.id)
563        LEFT JOIN tax t       ON (ac.tax_id      = t.id)
564        LEFT JOIN chart tc    ON (t.chart_id     = tc.id)
565        LEFT JOIN department  ON (department.id  = ar.department_id)
566        LEFT JOIN project     ON (project.id     = ar.globalproject_id)
567        WHERE (ar.id IS NOT NULL)
568          AND $fromto
569          $trans_id_filter
570          $ar_itime_filter
571          $ar_department_id_filter
572          $filter
573
574        UNION ALL
575
576        SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,ap.id, ac.amount, ac.taxkey, ac.memo,
577          ap.invnumber, ap.duedate, ap.amount as umsatz, COALESCE(ap.tax_point, ap.deliverydate) AS deliverydate, ap.itime::date,
578          ct.name, ct.ustid, ct.vendornumber AS vcnumber, NULL AS customer_id, ct.id AS vendor_id,
579          $ap_accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
580          ap.invoice,
581          t.rate AS taxrate, t.taxdescription,
582          'ap' as table,
583          tc.accno AS tax_accno, tc.description AS tax_accname,
584          ap.department_id,
585          ap.notes,
586          project.projectnumber as projectnumber, project.description as projectdescription,
587          department.description as departmentdescription
588        FROM acc_trans ac
589        LEFT JOIN ap        ON (ac.trans_id  = ap.id)
590        LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
591        LEFT JOIN chart c   ON (ac.chart_id  = c.id)
592        LEFT JOIN tax t     ON (ac.tax_id    = t.id)
593        LEFT JOIN chart tc    ON (t.chart_id     = tc.id)
594        LEFT JOIN department  ON (department.id  = ap.department_id)
595        LEFT JOIN project     ON (project.id     = ap.globalproject_id)
596        WHERE (ap.id IS NOT NULL)
597          AND $fromto
598          $trans_id_filter
599          $ap_itime_filter
600          $ap_department_id_filter
601          $filter
602
603        UNION ALL
604
605        SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,gl.id, ac.amount, ac.taxkey, ac.memo,
606          gl.reference AS invnumber, NULL AS duedate, ac.amount as umsatz, COALESCE(gl.tax_point, gl.deliverydate) AS deliverydate, gl.itime::date,
607          gl.description AS name, NULL as ustid, '' AS vcname, NULL AS customer_id, NULL AS vendor_id,
608          c.accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
609          FALSE AS invoice,
610          t.rate AS taxrate, t.taxdescription,
611          'gl' as table,
612          tc.accno AS tax_accno, tc.description AS tax_accname,
613          gl.department_id,
614          gl.notes,
615          '' as projectnumber, '' as projectdescription,
616          department.description as departmentdescription
617        FROM acc_trans ac
618        LEFT JOIN gl      ON (ac.trans_id  = gl.id)
619        LEFT JOIN chart c ON (ac.chart_id  = c.id)
620        LEFT JOIN tax t   ON (ac.tax_id    = t.id)
621        LEFT JOIN chart tc    ON (t.chart_id     = tc.id)
622        LEFT JOIN department  ON (department.id  = gl.department_id)
623        WHERE (gl.id IS NOT NULL)
624          AND $fromto
625          $trans_id_filter
626          $gl_itime_filter
627          $gl_department_id_filter
628          $gl_imported
629          AND NOT EXISTS (SELECT gl_id from ap_gl where gl_id = gl.id)
630          $filter
631
632        ORDER BY trans_id, acc_trans_id|;
633
634   my @query_args;
635   if ( $form->{gldatefrom} or $form->{department_id} ) {
636
637     for ( 1 .. 3 ) {
638       if ( $form->{gldatefrom} ) {
639         my $glfromdate = $::locale->parse_date_to_object($form->{gldatefrom});
640         die "illegal data" unless ref($glfromdate) eq 'DateTime';
641         push(@query_args, $glfromdate);
642       }
643       if ( $form->{department_id} ) {
644         push(@query_args, $form->{department_id});
645       }
646     }
647   }
648
649   my $sth = prepare_execute_query($form, $self->dbh, $query, @query_args);
650   $self->{DATEV} = [];
651
652   my $counter = 0;
653   my $continue = 1; #
654   my $name;
655   while ( $continue && (my $ref = $sth->fetchrow_hashref("NAME_lc")) ) {
656     last unless $ref;  # for single transactions
657     $counter++;
658     if (($counter % 500) == 0) {
659       $progress_callback->($counter);
660     }
661
662     my $trans    = [ $ref ];
663
664     my $count    = $ref->{amount};
665     my $firstrun = 1;
666
667     # if the amount of a booking in a group is smaller than 0.02, any tax
668     # amounts will likely be smaller than 1 cent, so go into subcent mode
669     my $subcent  = abs($count) < 0.02;
670
671     # records from acc_trans are ordered by trans_id and acc_trans_id
672     # first check for unbalanced ledger inside one trans_id
673     # there may be several groups inside a trans_id, e.g. the original booking and the payment
674     # each group individually should be exactly balanced and each group
675     # individually needs its own datev lines
676
677     # keep fetching new acc_trans lines until the end of a balanced group is reached
678     while (abs($count) > 0.01 || $firstrun || ($subcent && abs($count) > 0.005)) {
679       my $ref2 = $sth->fetchrow_hashref("NAME_lc");
680       unless ( $ref2 ) {
681         $continue = 0;
682         last;
683       };
684
685       # check if trans_id of current acc_trans line is still the same as the
686       # trans_id of the first line in group, i.e. we haven't finished a 0-group
687       # before moving on to the next trans_id, error will likely be in the old
688       # trans_id.
689
690       if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
691         require SL::DB::Manager::AccTransaction;
692         if ( $trans->[0]->{trans_id} ) {
693           my $acc_trans_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
694           $self->add_error(t8("Export error in transaction #1: Unbalanced ledger before next transaction (#2)",
695                               $acc_trans_obj->transaction_name, $ref2->{trans_id})
696           );
697         };
698         return;
699       }
700
701       push @{ $trans }, $ref2;
702
703       $count    += $ref2->{amount};
704       $firstrun  = 0;
705     }
706
707     foreach my $i (0 .. scalar(@{ $trans }) - 1) {
708       my $ref        = $trans->[$i];
709       my $prev_ref   = 0 < $i ? $trans->[$i - 1] : undef;
710       if (   $all_taxchart_ids{$ref->{id}}
711           && ($ref->{link} =~ m/(?:AP_tax|AR_tax)/)
712           && (   ($prev_ref && $prev_ref->{taxkey} && (_sign($ref->{amount}) == _sign($prev_ref->{amount})))
713               || $ref->{invoice})) {
714         $ref->{is_tax} = 1;
715       }
716
717       if (   !$ref->{invoice}   # we have a non-invoice booking (=gl)
718           &&  $ref->{is_tax}    # that has "is_tax" set
719           && !($prev_ref->{is_tax})  # previous line wasn't is_tax
720           &&  (_sign($ref->{amount}) == _sign($prev_ref->{amount}))) {  # and sign same as previous sign
721         $trans->[$i - 1]->{tax_amount} = $ref->{amount};
722       }
723     }
724
725     my $absumsatz     = 0;
726     if (scalar(@{$trans}) <= 2) {
727       push @{ $self->{DATEV} }, $trans;
728       next;
729     }
730
731     # determine at which array position the reference value (called absumsatz) is
732     # and which amount it has
733
734     for my $j (0 .. (scalar(@{$trans}) - 1)) {
735
736       # Three cases:
737       # 1: gl transaction (Dialogbuchung), invoice is false, no double split booking allowed
738
739       # 2: sales or vendor invoice (Verkaufs- und Einkaufsrechnung): invoice is
740       # true, instead of absumsatz use link AR/AP (there should only be one
741       # entry)
742
743       # 3. AR/AP transaction (Kreditoren- und Debitorenbuchung): invoice is false,
744       # instead of absumsatz use link AR/AP (there should only be one, so jump
745       # out of search as soon as you find it )
746
747       # case 1 and 2
748       # for gl-bookings no split is allowed and there is no AR/AP account, so we always use the maximum value as a reference
749       # for ap/ar bookings we can always search for AR/AP in link and use that
750       if ( ( not $trans->[$j]->{'invoice'} and abs($trans->[$j]->{'amount'}) > abs($absumsatz) )
751          or ($trans->[$j]->{'invoice'} and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP'))) {
752         $absumsatz     = $trans->[$j]->{'amount'};
753         $notsplitindex = $j;
754       }
755
756       # case 3
757       # Problem: we can't distinguish between AR and AP and normal invoices via boolean "invoice"
758       # for AR and AP transaction exit the loop as soon as an AR or AP account is found
759       # there must be only one AR or AP chart in the booking
760       # since it is possible to do this kind of things with GL too, make sure those don't get aborted in case someone
761       # manually pays an invoice in GL.
762       if ($trans->[$j]->{table} ne 'gl' and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP')) {
763         $notsplitindex = $j;   # position in booking with highest amount
764         $absumsatz     = $trans->[$j]->{'amount'};
765         last;
766       };
767     }
768
769     my $ml             = ($trans->[0]->{'umsatz'} > 0) ? 1 : -1;
770     my $rounding_error = 0;
771     my @taxed;
772
773     # go through each line and determine if it is a tax booking or not
774     # skip all tax lines and notsplitindex line
775     # push all other accounts (e.g. income or expense) with corresponding taxkey
776
777     for my $j (0 .. (scalar(@{$trans}) - 1)) {
778       if (   ($j != $notsplitindex)
779           && !$trans->[$j]->{is_tax}
780           && (   $trans->[$j]->{'taxkey'} eq ""
781               || $trans->[$j]->{'taxkey'} eq "0"
782               || $trans->[$j]->{'taxkey'} eq "1"
783               || $trans->[$j]->{'taxkey'} eq "10"
784               || $trans->[$j]->{'taxkey'} eq "11")) {
785         my %new_trans = ();
786         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
787
788         $absumsatz               += $trans->[$j]->{'amount'};
789         $new_trans{'amount'}      = $trans->[$j]->{'amount'} * (-1);
790         $new_trans{'umsatz'}      = abs($trans->[$j]->{'amount'}) * $ml;
791         $trans->[$j]->{'umsatz'}  = abs($trans->[$j]->{'amount'}) * $ml;
792
793         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
794
795       } elsif (($j != $notsplitindex) && !$trans->[$j]->{is_tax}) {
796
797         my %new_trans = ();
798         map { $new_trans{$_} = $trans->[$notsplitindex]->{$_}; } keys %{ $trans->[$notsplitindex] };
799
800         my $tax_rate              = $trans->[$j]->{'taxrate'};
801         $new_trans{'net_amount'}  = $trans->[$j]->{'amount'} * -1;
802         $new_trans{'tax_rate'}    = 1 + $tax_rate;
803
804         if (!$trans->[$j]->{'invoice'}) {
805           $new_trans{'amount'}      = $form->round_amount(-1 * ($trans->[$j]->{amount} + $trans->[$j]->{tax_amount}), 2);
806           $new_trans{'umsatz'}      = abs($new_trans{'amount'}) * $ml;
807           $trans->[$j]->{'umsatz'}  = $new_trans{'umsatz'};
808           $absumsatz               += -1 * $new_trans{'amount'};
809
810         } else {
811           my $unrounded             = $trans->[$j]->{'amount'} * (1 + $tax_rate) * -1 + $rounding_error;
812           my $rounded               = $form->round_amount($unrounded, 2);
813
814           $rounding_error           = $unrounded - $rounded;
815           $new_trans{'amount'}      = $rounded;
816           $new_trans{'umsatz'}      = abs($rounded) * $ml;
817           $trans->[$j]->{'umsatz'}  = $new_trans{umsatz};
818           $absumsatz               -= $rounded;
819         }
820
821         push @{ $self->{DATEV} }, [ \%new_trans, $trans->[$j] ];
822         push @taxed, $self->{DATEV}->[-1];
823       }
824     }
825
826     my $idx        = 0;
827     my $correction = 0;
828     while ((abs($absumsatz) >= 0.01) && (abs($absumsatz) < 1.00)) {
829       if ($idx >= scalar @taxed) {
830         last if (!$correction);
831
832         $correction = 0;
833         $idx        = 0;
834       }
835
836       my $transaction = $taxed[$idx]->[0];
837
838       my $old_amount     = $transaction->{amount};
839       my $old_correction = $correction;
840       my @possible_diffs;
841
842       if (!$transaction->{diff}) {
843         @possible_diffs = (0.01, -0.01);
844       } else {
845         @possible_diffs = ($transaction->{diff});
846       }
847
848       foreach my $diff (@possible_diffs) {
849         my $net_amount = $form->round_amount(($transaction->{amount} + $diff) / $transaction->{tax_rate}, 2);
850         next if ($net_amount != $transaction->{net_amount});
851
852         $transaction->{diff}    = $diff;
853         $transaction->{amount} += $diff;
854         $transaction->{umsatz} += $diff;
855         $absumsatz             -= $diff;
856         $correction             = 1;
857
858         last;
859       }
860
861       $idx++;
862     }
863
864     $absumsatz = $form->round_amount($absumsatz, 2);
865     if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
866       require SL::DB::Manager::AccTransaction;
867       my $acc_trans_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
868       $self->add_error(t8("Export error in transaction #1: Rounding error too large #2",
869                           $acc_trans_obj->transaction_name, $absumsatz)
870       );
871     } elsif (abs($absumsatz) >= 0.01) {
872       $self->add_net_gross_differences($absumsatz);
873     }
874   }
875
876   $sth->finish();
877
878   $::lxdebug->leave_sub;
879 }
880
881 sub generate_datev_lines {
882   my ($self) = @_;
883
884   my @datev_lines = ();
885
886   foreach my $transaction ( @{ $self->{DATEV} } ) {
887
888     # each $transaction entry contains data from several acc_trans entries
889     # belonging to the same trans_id
890
891     my %datev_data = (); # data for one transaction
892     my $trans_lines = scalar(@{$transaction});
893
894     my $umsatz         = 0;
895     my $gegenkonto     = "";
896     my $konto          = "";
897     my $belegfeld1     = "";
898     my $datum          = "";
899     my $waehrung       = "";
900     my $buchungstext   = "";
901     my $belegfeld2     = "";
902     my $datevautomatik = 0;
903     my $taxkey         = 0;
904     my $charttax       = 0;
905     my $ustid          ="";
906     my ($haben, $soll);
907     for (my $i = 0; $i < $trans_lines; $i++) {
908       if ($trans_lines == 2) {
909         if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
910           $umsatz = $transaction->[$i]->{'amount'};
911         }
912       } else {
913         if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
914           $umsatz = $transaction->[$i]->{'umsatz'};
915         }
916       }
917       if ($transaction->[$i]->{'datevautomatik'}) {
918         $datevautomatik = 1;
919       }
920       if ($transaction->[$i]->{'taxkey'}) {
921         $taxkey = $transaction->[$i]->{'taxkey'};
922         # $taxkey = 0 if $taxkey == 94; # taxbookings are in gl
923       }
924       if ($transaction->[$i]->{'charttax'}) {
925         $charttax = $transaction->[$i]->{'charttax'};
926       }
927       if ($transaction->[$i]->{'amount'} > 0) {
928         $haben = $i;
929       } else {
930         $soll = $i;
931       }
932     }
933
934     if ($trans_lines >= 2) {
935
936       # Personenkontenerweiterung: accno has already been replaced if use_pk was set
937       $datev_data{'gegenkonto'} = $transaction->[$haben]->{'accno'};
938       $datev_data{'konto'}      = $transaction->[$soll]->{'accno'};
939       if ($transaction->[$haben]->{'invnumber'} ne "") {
940         $datev_data{belegfeld1} = $transaction->[$haben]->{'invnumber'};
941       }
942       $datev_data{datum} = $transaction->[$haben]->{'transdate'};
943       $datev_data{waehrung} = 'EUR';
944       $datev_data{kost1} = $transaction->[$haben]->{'departmentdescription'};
945       $datev_data{kost2} = $transaction->[$haben]->{'projectdescription'};
946
947       if ($transaction->[$haben]->{'name'} ne "") {
948         $datev_data{buchungstext} = $transaction->[$haben]->{'name'};
949       }
950       if (($transaction->[$haben]->{'ustid'} // '') ne "") {
951         $datev_data{ustid} = SL::VATIDNr->normalize($transaction->[$haben]->{'ustid'});
952       }
953       if (($transaction->[$haben]->{'duedate'} // '') ne "") {
954         $datev_data{belegfeld2} = $transaction->[$haben]->{'duedate'};
955       }
956
957       # if deliverydate exists, add it to datev export if it is
958       # * an ar/ap booking that is not a payment
959       # * a gl booking
960       if (    ($transaction->[$haben]->{'deliverydate'} // '') ne ''
961            && (
962                 (    $transaction->[$haben]->{'table'} =~ /^(ar|ap)$/
963                   && $transaction->[$haben]->{'link'}  !~ m/_paid/
964                   && $transaction->[$soll]->{'link'}   !~ m/_paid/
965                 )
966                 || $transaction->[$haben]->{'table'} eq 'gl'
967               )
968          ) {
969         $datev_data{leistungsdatum} = $transaction->[$haben]->{'deliverydate'};
970       }
971     }
972     $datev_data{umsatz} = abs($umsatz); # sales invoices without tax have a different sign???
973
974     # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
975     # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
976     # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
977     # Skript angelegt werden.
978     # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
979     # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
980     # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
981     # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
982     # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
983     #
984
985     # only set buchungsschluessel if the following conditions are met:
986     if (   ( $datevautomatik || $taxkey)
987         && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
988       # $datev_data{buchungsschluessel} = !$datevautomatik ? $taxkey : "4";
989       $datev_data{buchungsschluessel} = $taxkey;
990     }
991     # set lock for each transaction
992     $datev_data{locked} = $self->locked;
993     # add guids if datev export with documents is requested
994     if ($self->documents) {
995       # add all document links for the latest created/uploaded document
996       my $latest_document = SL::DB::Manager::File->get_first(query =>
997                                 [
998                                   object_id   => $transaction->[$haben]->{trans_id},
999                                   file_type   => 'document',
1000                                   mime_type   => 'application/pdf',
1001                                   or          => [
1002                                                    object_type => 'gl_transaction',
1003                                                    object_type => 'purchase_invoice',
1004                                                    object_type => 'invoice',
1005                                                    object_type => 'credit_note',
1006                                                  ],
1007                                 ],
1008                                   sort_by   => 'itime DESC');
1009       if (ref $latest_document eq 'SL::DB::File') {
1010       #if (scalar @{ $latest_documents }) {
1011         # if we have a booking document add guid from the latest version
1012         # one record may be referenced to more transaction (credit booking with different accounts)
1013         # therefore collect guids in hash
1014         # not yet implemented -> datev steigt aus, sobald ein komma getrennter wert erscheint
1015         #foreach my $latest_document (@{ $latest_documents }) {
1016           die "No file datatype:" . ref $latest_document unless (ref $latest_document eq 'SL::DB::File');
1017           my $latest_guid = $latest_document->file_versions_sorted->[-1]->guid;
1018
1019           $self->{guids}{$latest_guid} = 1;
1020           $datev_data{document_guid}  .= $datev_data{document_guid} ?  ',' : '';
1021           $datev_data{document_guid}  .= $latest_guid;
1022         # }
1023       }
1024     }
1025
1026     push(@datev_lines, \%datev_data) if $datev_data{umsatz};
1027   }
1028
1029   # example of modifying export data:
1030   # foreach my $datev_line ( @datev_lines ) {
1031   #   if ( $datev_line{"konto"} eq '1234' ) {
1032   #     $datev_line{"konto"} = '9999';
1033   #   }
1034   # }
1035   #
1036
1037   return \@datev_lines;
1038 }
1039
1040 sub check_vcnumbers_are_valid_pk_numbers {
1041   my ($self) = @_;
1042
1043   # better use a class variable and set this in sub new (also needed in DATEV::CSV)
1044   # calculation is also a bit more sane in sub check_valid_length_of_accounts
1045   my $length_of_accounts = length(SL::DB::Manager::Chart->get_first(where => [charttype => 'A'])->accno) // 4;
1046   my $pk_length = $length_of_accounts + 1;
1047   my $query = <<"SQL";
1048    SELECT customernumber AS vcnumber FROM customer WHERE customernumber !~ '^[[:digit:]]{$pk_length}\$'
1049    UNION
1050    SELECT vendornumber   AS vcnumber FROM vendor   WHERE vendornumber   !~ '^[[:digit:]]{$pk_length}\$'
1051    LIMIT 1;
1052 SQL
1053   my ($has_non_pk_accounts)  = selectrow_query($::form, SL::DB->client->dbh, $query);
1054   return defined $has_non_pk_accounts ? 0 : 1;
1055 }
1056
1057
1058 sub check_valid_length_of_accounts {
1059   my ($self) = @_;
1060
1061   my $query = <<"SQL";
1062   SELECT DISTINCT char_length (accno) FROM chart WHERE charttype='A' AND id in (select chart_id from acc_trans);
1063 SQL
1064
1065   my $accno_length = selectall_hashref_query($::form, SL::DB->client->dbh, $query);
1066   if (1 < scalar @$accno_length) {
1067     $::form->error(t8("Invalid combination of ledger account number length." .
1068                       " Mismatch length of #1 with length of #2. Please check your account settings. ",
1069                       $accno_length->[0]->{char_length}, $accno_length->[1]->{char_length}));
1070   }
1071   return 1;
1072 }
1073
1074 sub check_document_export {
1075   my ($self) = @_;
1076
1077   # no dms enabled and works only for type Filesystem
1078   return 0 unless $::instance_conf->get_doc_storage
1079                && $::instance_conf->get_doc_storage_for_documents eq 'Filesystem';
1080
1081   return 1;
1082
1083 }
1084
1085 sub check_all_bookings_have_documents {
1086   my $self   = shift;
1087   my %params = @_;
1088
1089   die "Need from date" unless $params{from};
1090   die "Need to date"   unless $params{to};
1091
1092   $self->from($params{from});
1093   $self->to($params{to});
1094
1095   my $fromto = $self->fromto;
1096   # not all last month ar ap gl booking have an entry -> rent ?
1097   my $query = qq|
1098   select distinct trans_id,object_id from acc_trans
1099   left join files on files.object_id=trans_id
1100   where $fromto
1101   and object_id is null
1102   and trans_id not in (select id from gl)
1103   LIMIT 1|;
1104
1105   my ($booking_has_no_document)  = selectrow_query($::form, SL::DB->client->dbh, $query);
1106   return defined $booking_has_no_document ? 0 : 1;
1107
1108 }
1109
1110
1111
1112 sub _u8 {
1113   my ($value) = @_;
1114   return encode('UTF-8', $value // '');
1115 }
1116
1117
1118 sub DESTROY {
1119   clean_temporary_directories();
1120 }
1121
1122 1;
1123
1124 __END__
1125
1126 =encoding utf-8
1127
1128 =head1 NAME
1129
1130 SL::DATEV - kivitendo DATEV Export module
1131
1132 =head1 SYNOPSIS
1133
1134   use SL::DATEV qw(:CONSTANTS);
1135
1136   my $startdate = DateTime->new(year => 2014, month => 9, day => 1);
1137   my $enddate   = DateTime->new(year => 2014, month => 9, day => 31);
1138   my $datev = SL::DATEV->new(
1139     exporttype => DATEV_ET_BUCHUNGEN,
1140     format     => DATEV_FORMAT_KNE,
1141     from       => $startdate,
1142     to         => $enddate,
1143   );
1144
1145   # To only export transactions from a specific trans_id: (from and to are ignored)
1146   my $invoice = SL::DB::Manager::Invoice->find_by( invnumber => '216' );
1147   my $datev = SL::DATEV->new(
1148     exporttype => DATEV_ET_BUCHUNGEN,
1149     format     => DATEV_FORMAT_KNE,
1150     trans_id   => $invoice->trans_id,
1151   );
1152
1153   my $datev = SL::DATEV->new(
1154     exporttype => DATEV_ET_STAMM,
1155     format     => DATEV_FORMAT_KNE,
1156     accnofrom  => $start_account_number,
1157     accnoto    => $end_account_number,
1158   );
1159
1160   # get or set datev stamm
1161   my $hashref = $datev->get_datev_stamm;
1162   $datev->save_datev_stamm($hashref);
1163
1164   # manually clean up temporary directories older than 8 hours
1165   $datev->clean_temporary_directories;
1166
1167   # export
1168   $datev->export;
1169
1170   if ($datev->errors) {
1171     die join "\n", $datev->error;
1172   }
1173
1174   # get relevant data for saving the export:
1175   my $dl_token = $datev->download_token;
1176   my $path     = $datev->export_path;
1177   my @files    = $datev->filenames;
1178
1179   # retrieving an export at a later time
1180   my $datev = SL::DATEV->new(
1181     download_token => $dl_token_from_user,
1182   );
1183
1184   my $path     = $datev->export_path;
1185   my @files    = glob("$path/*");
1186
1187   # Only test the datev data of a specific trans_id, without generating an
1188   # export file, but filling $datev->errors if errors exist
1189
1190   my $datev = SL::DATEV->new(
1191     trans_id   => $invoice->trans_id,
1192   );
1193   $datev->generate_datev_data;
1194   # if ($datev->errors) { ...
1195
1196
1197 =head1 DESCRIPTION
1198
1199 This module implements the DATEV export standard. For usage see above.
1200
1201 =head1 FUNCTIONS
1202
1203 =over 4
1204
1205 =item new PARAMS
1206
1207 Generic constructor. See section attributes for information about what to pass.
1208
1209 =item generate_datev_data
1210
1211 Fetches all transactions from the database (via a trans_id or a date range),
1212 and does an initial transformation (e.g. filters out tax, determines
1213 the brutto amount, checks split transactions ...) and stores this data in
1214 $self->{DATEV}.
1215
1216 If any errors are found these are collected in $self->errors.
1217
1218 This function is needed for all the exports, but can be also called
1219 independently in order to check transactions for DATEV compatibility.
1220
1221 =item generate_datev_lines
1222
1223 Parse the data in $self->{DATEV} and transform it into a format that can be
1224 used by DATEV, e.g. determines Konto and Gegenkonto, the taxkey, ...
1225
1226 The transformed data is returned as an arrayref, which is ready to be converted
1227 to a DATEV data format, e.g. KNE, OBE, CSV, ...
1228
1229 At this stage the "DATEV rule" has already been applied to the taxkeys, i.e.
1230 entries with datevautomatik have an empty taxkey, as the taxkey is already
1231 determined by the chart.
1232
1233 =item get_datev_stamm
1234
1235 Loads DATEV Stammdaten and returns as hashref.
1236
1237 =item save_datev_stamm HASHREF
1238
1239 Saves DATEV Stammdaten from provided hashref.
1240
1241 =item exporttype
1242
1243 See L<CONSTANTS> for possible values
1244
1245 =item has_exporttype
1246
1247 Returns true if an exporttype has been set. Without exporttype most report functions won't work.
1248
1249 =item format
1250
1251 Specifies the designated format of the export. Currently only KNE export is implemented.
1252
1253 See L<CONSTANTS> for possible values
1254
1255 =item has_format
1256
1257 Returns true if a format has been set. Without format most report functions won't work.
1258
1259 =item download_token
1260
1261 Returns a download token for this DATEV object.
1262
1263 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1264
1265 =item export_path
1266
1267 Returns an export_path for this DATEV object.
1268
1269 Note: If either a download_token or export_path were set at the creation these are infered, otherwise randomly generated.
1270
1271 =item filenames
1272
1273 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.
1274
1275 =item net_gross_differences
1276
1277 If there were any net gross differences during calculation they will be collected here.
1278
1279 =item sum_net_gross_differences
1280
1281 Sum of all differences.
1282
1283 =item clean_temporary_directories
1284
1285 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.
1286
1287 =item errors
1288
1289 Returns a list of errors that occurred. If no errors occurred, the export was a success.
1290
1291 =item export
1292
1293 Exports data. You have to have set L<exporttype> and L<format> or an error will
1294 occur. OBE exports are currently not implemented.
1295
1296 =item csv_export_for_tax_accountant
1297
1298 Generates up to four downloadable csv files containing data about sales and
1299 purchase invoices, and their respective payments:
1300
1301 Example:
1302   my $startdate = DateTime->new(year => 2012, month =>  1, day =>  1);
1303   my $enddate   = DateTime->new(year => 2012, month => 12, day => 31);
1304   SL::DATEV->new(from => $startdate, to => $enddate)->csv_export_for_tax_accountant;
1305   # {
1306   #   'download_token' => '1488551625-815654-22430',
1307   #   'filenames' => [
1308   #                    'Zahlungen Kreditorenbuchungen 2012-01-01 - 2012-12-31.csv',
1309   #                    'Kreditorenbuchungen 2012-01-01 - 2012-12-31.csv',
1310   #                    'Zahlungen Debitorenbuchungen 2012-01-01 - 2012-12-31.csv',
1311   #                    'Debitorenbuchungen 2012-01-01 - 2012-12-31.csv'
1312   #                  ]
1313   # };
1314
1315
1316 =item check_vcnumbers_are_valid_pk_numbers
1317
1318 Returns 1 if all vcnumbers are suitable for the DATEV export, 0 if not.
1319
1320 Finds the default length of charts (e.g. 4), adds 1 for the pk chart length
1321 (e.g. 5), and checks the database for any customers or vendors whose customer-
1322 or vendornumber doesn't consist of only numbers with exactly that length. E.g.
1323 for a chart length of four "10001" would be ok, but not "10001b" or "1000".
1324
1325 All vcnumbers are checked, obsolete customers or vendors aren't exempt.
1326
1327 There is also no check for the typical customer range 10000-69999 and the
1328 typical vendor range 70000-99999.
1329
1330 =item check_valid_length_of_accounts
1331
1332 Returns 1 if all currently booked accounts have only one common number length domain (e.g. 4 or 6).
1333 Will throw an error if more than one distinct size is detected.
1334 The error message gives a short hint with the value of the (at least)
1335 two mismatching number length domains.
1336
1337 =item check_document_export
1338
1339 Returns 1 if DMS feature is enabled and Backend is Filesystem
1340
1341 =item check_all_bookings_have_documents
1342
1343 Returns 1 if all ar and ap transactions for this period have a document entry in files.
1344 Therefore all ar and ap transactions may be exported.
1345 Note: DATEV accepts only PDF and for some gl bookings a document makes no sense
1346
1347
1348 =back
1349
1350 =head1 ATTRIBUTES
1351
1352 This is a list of attributes set in either the C<new> or a method of the same name.
1353
1354 =over 4
1355
1356 =item dbh
1357
1358 Set a database handle to use in the process. This allows for an export to be
1359 done on a transaction in progress without committing first.
1360
1361 Note: If you don't want this code to commit, simply providing a dbh is not
1362 enough enymore. You'll have to wrap the call into a transaction yourself, so
1363 that the internal transaction does not commit.
1364
1365 =item exporttype
1366
1367 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1368
1369 =item format
1370
1371 See L<CONSTANTS> for possible values. This MUST be set before export is called.
1372
1373 =item download_token
1374
1375 Can be set on creation to retrieve a prior export for download.
1376
1377 =item from
1378
1379 =item to
1380
1381 Set boundary dates for the export. Unless a trans_id is passed these MUST be
1382 set for the export to work.
1383
1384 =item trans_id
1385
1386 To check only one gl/ar/ap transaction, pass the trans_id. The attributes
1387 L<from> and L<to> are currently still needed for the query to be assembled
1388 correctly.
1389
1390 =item accnofrom
1391
1392 =item accnoto
1393
1394 Set boundary account numbers for the export. Only useful for a stammdaten export.
1395
1396 =item locked
1397
1398 Boolean if the transactions are locked (read-only in kivitenod) or not.
1399 Default value is false
1400
1401 =back
1402
1403 =head1 CONSTANTS
1404
1405 =head2 Supplied to L<exporttype>
1406
1407 =over 4
1408
1409 =item DATEV_ET_BUCHUNGEN
1410
1411 =item DATEV_ET_STAMM
1412
1413 =back
1414
1415 =head2 Supplied to L<format>.
1416
1417 =over 4
1418
1419 =item DATEV_FORMAT_KNE
1420
1421 =item DATEV_FORMAT_OBE
1422
1423 =back
1424
1425 =head1 ERROR HANDLING
1426
1427 This module will die in the following cases:
1428
1429 =over 4
1430
1431 =item *
1432
1433 No or unrecognized exporttype or format was provided for an export
1434
1435 =item *
1436
1437 OBE export was called, which is not yet implemented.
1438
1439 =item *
1440
1441 general I/O errors
1442
1443 =back
1444
1445 Errors that occur during th actual export will be collected in L<errors>. The following types can occur at the moment:
1446
1447 =over 4
1448
1449 =item *
1450
1451 C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occur.
1452
1453 =item *
1454
1455 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).>  This error occurs if a
1456 transaction could not be reliably sorted out, or had rounding errors above the acceptable threshold.
1457
1458 =back
1459
1460 =head1 BUGS AND CAVEATS
1461
1462 =over 4
1463
1464 =item *
1465
1466 Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to in order to get a working export.
1467
1468 =item *
1469
1470 OBE export is currently not implemented.
1471
1472 =back
1473
1474 =head1 TODO
1475
1476 - handling of export_path and download token is a bit dodgy, clean that up.
1477
1478 =head1 SEE ALSO
1479
1480 L<SL::DATEV::KNEFile>
1481 L<SL::DATEV::CSV>
1482
1483 =head1 AUTHORS
1484
1485 Philip Reetz E<lt>p.reetz@linet-services.deE<gt>,
1486
1487 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
1488
1489 Jan Büren E<lt>jan@lx-office-hosting.deE<gt>,
1490
1491 Geoffrey Richardson E<lt>information@lx-office-hosting.deE<gt>,
1492
1493 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,
1494
1495 Stephan Köhler
1496
1497 =cut