WebshopApi: Dokumentation Installations und UPGRADE für Trigram(trgm_pg)
[kivitendo-erp.git] / SL / DATEV.pm
index 88c5fb4..9418c48 100644 (file)
@@ -18,7 +18,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Datev export module
 #======================================================================
 #
 # Datev export module
@@ -31,12 +32,20 @@ use strict;
 
 use SL::DBUtils;
 use SL::DATEV::KNEFile;
 
 use SL::DBUtils;
 use SL::DATEV::KNEFile;
+use SL::DATEV::CSV;
+use SL::DB;
+use SL::HTML::Util ();
+use SL::Locale::String qw(t8);
 
 use Data::Dumper;
 use DateTime;
 use Exporter qw(import);
 use File::Path;
 
 use Data::Dumper;
 use DateTime;
 use Exporter qw(import);
 use File::Path;
-use List::Util qw(max sum);
+use IO::File;
+use List::MoreUtils qw(any);
+use List::Util qw(min max sum);
+use List::UtilsBy qw(partition_by sort_by);
+use Text::CSV_XS;
 use Time::HiRes qw(gettimeofday);
 
 {
 use Time::HiRes qw(gettimeofday);
 
 {
@@ -44,13 +53,15 @@ use Time::HiRes qw(gettimeofday);
   use constant {
     DATEV_ET_BUCHUNGEN => $i++,
     DATEV_ET_STAMM     => $i++,
   use constant {
     DATEV_ET_BUCHUNGEN => $i++,
     DATEV_ET_STAMM     => $i++,
+    DATEV_ET_CSV       => $i++,
 
     DATEV_FORMAT_KNE   => $i++,
     DATEV_FORMAT_OBE   => $i++,
 
     DATEV_FORMAT_KNE   => $i++,
     DATEV_FORMAT_OBE   => $i++,
+    DATEV_FORMAT_CSV   => $i++,
   };
 }
 
   };
 }
 
-my @export_constants = qw(DATEV_ET_BUCHUNGEN DATEV_ET_STAMM DATEV_FORMAT_KNE DATEV_FORMAT_OBE);
+my @export_constants = qw(DATEV_ET_BUCHUNGEN DATEV_ET_STAMM DATEV_ET_CSV DATEV_FORMAT_KNE DATEV_FORMAT_OBE DATEV_FORMAT_CSV);
 our @EXPORT_OK = (@export_constants);
 our %EXPORT_TAGS = (CONSTANTS => [ @export_constants ]);
 
 our @EXPORT_OK = (@export_constants);
 our %EXPORT_TAGS = (CONSTANTS => [ @export_constants ]);
 
@@ -204,6 +215,16 @@ sub trans_id {
   return $self->{trans_id};
 }
 
   return $self->{trans_id};
 }
 
+sub warnings {
+  my $self = shift;
+
+  if (@_) {
+    $self->{warnings} = [@_];
+  } else {
+   return $self->{warnings};
+  }
+}
+
 sub accnofrom {
  my $self = shift;
 
 sub accnofrom {
  my $self = shift;
 
@@ -233,7 +254,7 @@ sub dbh {
     $self->{provided_dbh} = 1;
   }
 
     $self->{provided_dbh} = 1;
   }
 
-  $self->{dbh} ||= $::form->get_standard_dbh;
+  $self->{dbh} ||= SL::DB->client->dbh;
 }
 
 sub provided_dbh {
 }
 
 sub provided_dbh {
@@ -285,14 +306,15 @@ sub get_datev_stamm {
 sub save_datev_stamm {
   my ($self, $data) = @_;
 
 sub save_datev_stamm {
   my ($self, $data) = @_;
 
-  do_query($::form, $self->dbh, 'DELETE FROM datev');
-
-  my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
+  SL::DB->client->with_transaction(sub {
+    do_query($::form, $self->dbh, 'DELETE FROM datev');
 
 
-  my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
-  do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
+    my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
 
 
-  $self->dbh->commit unless $self->provided_dbh;
+    my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
+    do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
+    1;
+  }) or do { die SL::DB->client->error };
 }
 
 sub export {
 }
 
 sub export {
@@ -301,7 +323,9 @@ sub export {
 
   die 'no format set!' unless $self->has_format;
 
 
   die 'no format set!' unless $self->has_format;
 
-  if ($self->format == DATEV_FORMAT_KNE) {
+  if ($self->format == DATEV_FORMAT_CSV) {
+    $result = $self->csv_export;
+  } elsif ($self->format == DATEV_FORMAT_KNE) {
     $result = $self->kne_export;
   } elsif ($self->format == DATEV_FORMAT_OBE) {
     $result = $self->obe_export;
     $result = $self->kne_export;
   } elsif ($self->format == DATEV_FORMAT_OBE) {
     $result = $self->obe_export;
@@ -322,6 +346,8 @@ sub kne_export {
     $result = $self->kne_buchungsexport;
   } elsif ($self->exporttype == DATEV_ET_STAMM) {
     $result = $self->kne_stammdatenexport;
     $result = $self->kne_buchungsexport;
   } elsif ($self->exporttype == DATEV_ET_STAMM) {
     $result = $self->kne_stammdatenexport;
+  } elsif ($self->exporttype == DATEV_ET_CSV) {
+    $result = $self->csv_export_for_tax_accountant;
   } else {
     die 'unrecognized exporttype';
   }
   } else {
     die 'unrecognized exporttype';
   }
@@ -329,6 +355,29 @@ sub kne_export {
   return $result;
 }
 
   return $result;
 }
 
+sub csv_export {
+  my ($self) = @_;
+  my $result;
+
+  die 'no exporttype set!' unless $self->has_exporttype;
+
+  if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
+    _csv_buchungsexport_to_file($self, data => $self->csv_buchungsexport);
+
+  } elsif ($self->exporttype == DATEV_ET_STAMM) {
+    die 'will never be implemented';
+    # 'Background: Export should only contain non
+    #  DATEV-Charts and DATEV import will only
+    #  import new Charts.'
+  } elsif ($self->exporttype == DATEV_ET_CSV) {
+    $result = $self->csv_export_for_tax_accountant;
+  } else {
+    die 'unrecognized exporttype';
+  }
+
+return $result;
+}
+
 sub obe_export {
   die 'not yet implemented';
 }
 sub obe_export {
   die 'not yet implemented';
 }
@@ -345,15 +394,35 @@ sub _sign {
   $_[0] <=> 0;
 }
 
   $_[0] <=> 0;
 }
 
-sub _get_transactions {
+sub generate_datev_data {
   $main::lxdebug->enter_sub();
   $main::lxdebug->enter_sub();
-  my $self     = shift;
-  my $fromto   = shift;
-  my $progress_callback = shift || sub {};
+
+  my ($self, %params)   = @_;
+  my $fromto            = $params{from_to} // '';
+  my $progress_callback = $params{progress_callback} || sub {};
 
   my $form     =  $main::form;
 
   my $trans_id_filter = '';
 
   my $form     =  $main::form;
 
   my $trans_id_filter = '';
+  my $ar_department_id_filter = '';
+  my $ap_department_id_filter = '';
+  my $gl_department_id_filter = '';
+  if ( $form->{department_id} ) {
+    $ar_department_id_filter = " AND ar.department_id = ? ";
+    $ap_department_id_filter = " AND ap.department_id = ? ";
+    $gl_department_id_filter = " AND gl.department_id = ? ";
+  }
+
+  my ($gl_itime_filter, $ar_itime_filter, $ap_itime_filter);
+  if ( $form->{gldatefrom} ) {
+    $gl_itime_filter = " AND gl.itime >= ? ";
+    $ar_itime_filter = " AND ar.itime >= ? ";
+    $ap_itime_filter = " AND ap.itime >= ? ";
+  } else {
+    $gl_itime_filter = "";
+    $ar_itime_filter = "";
+    $ap_itime_filter = "";
+  }
 
   if ( $self->{trans_id} ) {
     # ignore dates when trans_id is passed so that the entire transaction is
 
   if ( $self->{trans_id} ) {
     # ignore dates when trans_id is passed so that the entire transaction is
@@ -372,60 +441,107 @@ sub _get_transactions {
   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');
 
   my $query    =
   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');
 
   my $query    =
-    qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey,
-         ar.invnumber, ar.duedate, ar.amount as umsatz, ar.deliverydate,
-         ct.name, ct.ustid,
-         c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
+    qq|SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,ar.id, ac.amount, ac.taxkey, ac.memo,
+         ar.invnumber, ar.duedate, ar.amount as umsatz, ar.deliverydate, ar.itime::date,
+         ct.name, ct.ustid, ct.customernumber AS vcnumber, ct.id AS customer_id, NULL AS vendor_id,
+         c.accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
          ar.invoice,
          ar.invoice,
-         t.rate AS taxrate
+         t.rate AS taxrate, t.taxdescription,
+         'ar' as table,
+         tc.accno AS tax_accno, tc.description AS tax_accname,
+         ar.department_id,
+         ar.notes,
+         project.projectnumber as projectnumber, project.description as projectdescription,
+         department.description as departmentdescription
        FROM acc_trans ac
        LEFT JOIN ar          ON (ac.trans_id    = ar.id)
        LEFT JOIN customer ct ON (ar.customer_id = ct.id)
        LEFT JOIN chart c     ON (ac.chart_id    = c.id)
        LEFT JOIN tax t       ON (ac.tax_id      = t.id)
        FROM acc_trans ac
        LEFT JOIN ar          ON (ac.trans_id    = ar.id)
        LEFT JOIN customer ct ON (ar.customer_id = ct.id)
        LEFT JOIN chart c     ON (ac.chart_id    = c.id)
        LEFT JOIN tax t       ON (ac.tax_id      = t.id)
+       LEFT JOIN chart tc    ON (t.chart_id     = tc.id)
+       LEFT JOIN department  ON (department.id  = ar.department_id)
+       LEFT JOIN project     ON (project.id     = ar.globalproject_id)
        WHERE (ar.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
        WHERE (ar.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
+         $ar_itime_filter
+         $ar_department_id_filter
          $filter
 
        UNION ALL
 
          $filter
 
        UNION ALL
 
-       SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey,
-         ap.invnumber, ap.duedate, ap.amount as umsatz, ap.deliverydate,
-         ct.name,ct.ustid,
-         c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
+       SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,ap.id, ac.amount, ac.taxkey, ac.memo,
+         ap.invnumber, ap.duedate, ap.amount as umsatz, ap.deliverydate, ap.itime::date,
+         ct.name, ct.ustid, ct.vendornumber AS vcnumber, NULL AS customer_id, ct.id AS vendor_id,
+         c.accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
          ap.invoice,
          ap.invoice,
-         t.rate AS taxrate
+         t.rate AS taxrate, t.taxdescription,
+         'ap' as table,
+         tc.accno AS tax_accno, tc.description AS tax_accname,
+         ap.department_id,
+         ap.notes,
+         project.projectnumber as projectnumber, project.description as projectdescription,
+         department.description as departmentdescription
        FROM acc_trans ac
        LEFT JOIN ap        ON (ac.trans_id  = ap.id)
        LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
        LEFT JOIN chart c   ON (ac.chart_id  = c.id)
        LEFT JOIN tax t     ON (ac.tax_id    = t.id)
        FROM acc_trans ac
        LEFT JOIN ap        ON (ac.trans_id  = ap.id)
        LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
        LEFT JOIN chart c   ON (ac.chart_id  = c.id)
        LEFT JOIN tax t     ON (ac.tax_id    = t.id)
+       LEFT JOIN chart tc    ON (t.chart_id     = tc.id)
+       LEFT JOIN department  ON (department.id  = ap.department_id)
+       LEFT JOIN project     ON (project.id     = ap.globalproject_id)
        WHERE (ap.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
        WHERE (ap.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
+         $ap_itime_filter
+         $ap_department_id_filter
          $filter
 
        UNION ALL
 
          $filter
 
        UNION ALL
 
-       SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey,
-         gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz, NULL as deliverydate,
-         gl.description AS name, NULL as ustid,
-         c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
+       SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,gl.id, ac.amount, ac.taxkey, ac.memo,
+         gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz, NULL as deliverydate, gl.itime::date,
+         gl.description AS name, NULL as ustid, '' AS vcname, NULL AS customer_id, NULL AS vendor_id,
+         c.accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
          FALSE AS invoice,
          FALSE AS invoice,
-         t.rate AS taxrate
+         t.rate AS taxrate, t.taxdescription,
+         'gl' as table,
+         tc.accno AS tax_accno, tc.description AS tax_accname,
+         gl.department_id,
+         gl.notes,
+         '' as projectnumber, '' as projectdescription,
+         department.description as departmentdescription
        FROM acc_trans ac
        LEFT JOIN gl      ON (ac.trans_id  = gl.id)
        LEFT JOIN chart c ON (ac.chart_id  = c.id)
        LEFT JOIN tax t   ON (ac.tax_id    = t.id)
        FROM acc_trans ac
        LEFT JOIN gl      ON (ac.trans_id  = gl.id)
        LEFT JOIN chart c ON (ac.chart_id  = c.id)
        LEFT JOIN tax t   ON (ac.tax_id    = t.id)
+       LEFT JOIN chart tc    ON (t.chart_id     = tc.id)
+       LEFT JOIN department  ON (department.id  = gl.department_id)
        WHERE (gl.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
        WHERE (gl.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
+         $gl_itime_filter
+         $gl_department_id_filter
          $filter
 
        ORDER BY trans_id, acc_trans_id|;
 
          $filter
 
        ORDER BY trans_id, acc_trans_id|;
 
-  my $sth = prepare_execute_query($form, $self->dbh, $query);
+  my @query_args;
+  if ( $form->{gldatefrom} or $form->{department_id} ) {
+
+    for ( 1 .. 3 ) {
+      if ( $form->{gldatefrom} ) {
+        my $glfromdate = $::locale->parse_date_to_object($form->{gldatefrom});
+        die "illegal data" unless ref($glfromdate) eq 'DateTime';
+        push(@query_args, $glfromdate);
+      }
+      if ( $form->{department_id} ) {
+        push(@query_args, $form->{department_id});
+      }
+    }
+  }
+
+  my $sth = prepare_execute_query($form, $self->dbh, $query, @query_args);
   $self->{DATEV} = [];
 
   my $counter = 0;
   $self->{DATEV} = [];
 
   my $counter = 0;
@@ -469,14 +585,11 @@ sub _get_transactions {
       if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
         require SL::DB::Manager::AccTransaction;
         if ( $trans->[0]->{trans_id} ) {
       if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
         require SL::DB::Manager::AccTransaction;
         if ( $trans->[0]->{trans_id} ) {
-          my $acc_trans_old_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
-          $self->add_error("Unbalanced ledger! Old: " . $acc_trans_old_obj->transaction_name) if ref($acc_trans_old_obj);
+          my $acc_trans_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
+          $self->add_error(t8("Export error in transaction #1: Unbalanced ledger before next transaction (#2)",
+                              $acc_trans_obj->transaction_name, $ref2->{trans_id})
+          );
         };
         };
-        if ( $ref2->{trans_id} ) {
-          my $acc_trans_curr_obj = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $ref2->{trans_id} ]);
-          $self->add_error("Unbalanced ledger! New:" . $acc_trans_curr_obj->transaction_name) if ref($acc_trans_curr_obj);
-        };
-        $self->add_error("count: $count");
         return;
       }
 
         return;
       }
 
@@ -539,7 +652,9 @@ sub _get_transactions {
       # Problem: we can't distinguish between AR and AP and normal invoices via boolean "invoice"
       # for AR and AP transaction exit the loop as soon as an AR or AP account is found
       # there must be only one AR or AP chart in the booking
       # Problem: we can't distinguish between AR and AP and normal invoices via boolean "invoice"
       # for AR and AP transaction exit the loop as soon as an AR or AP account is found
       # there must be only one AR or AP chart in the booking
-      if ( $trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP') {
+      # since it is possible to do this kind of things with GL too, make sure those don't get aborted in case someone
+      # manually pays an invoice in GL.
+      if ($trans->[$j]->{table} ne 'gl' and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP')) {
         $notsplitindex = $j;   # position in booking with highest amount
         $absumsatz     = $trans->[$j]->{'amount'};
         last;
         $notsplitindex = $j;   # position in booking with highest amount
         $absumsatz     = $trans->[$j]->{'amount'};
         last;
@@ -645,8 +760,9 @@ sub _get_transactions {
     if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
       require SL::DB::Manager::AccTransaction;
       my $acc_trans_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
     if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
       require SL::DB::Manager::AccTransaction;
       my $acc_trans_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
-      $self->add_error("Datev-Export fehlgeschlagen! Bei Transaktion " . $acc_trans_obj->transaction_name . " ($absumsatz)");
-
+      $self->add_error(t8("Export error in transaction #1: Rounding error too large #2",
+                          $acc_trans_obj->transaction_name, $absumsatz)
+      );
     } elsif (abs($absumsatz) >= 0.01) {
       $self->add_net_gross_differences($absumsatz);
     }
     } elsif (abs($absumsatz) >= 0.01) {
       $self->add_net_gross_differences($absumsatz);
     }
@@ -714,9 +830,6 @@ sub datetofour {
 
   my ($day, $month, $year) = split(/\./, $date);
 
 
   my ($day, $month, $year) = split(/\./, $date);
 
-  if ($day =~ /^0/) {
-    $day = substr($day, 1, 1);
-  }
   if (length($month) < 2) {
     $month = "0" . $month;
   }
   if (length($month) < 2) {
     $month = "0" . $month;
   }
@@ -789,6 +902,116 @@ sub make_ev_header {
   return $ev_header;
 }
 
   return $ev_header;
 }
 
+sub generate_datev_lines {
+  my ($self) = @_;
+
+  my @datev_lines = ();
+
+  foreach my $transaction ( @{ $self->{DATEV} } ) {
+
+    # each $transaction entry contains data from several acc_trans entries
+    # belonging to the same trans_id
+
+    my %datev_data = (); # data for one transaction
+    my $trans_lines = scalar(@{$transaction});
+
+    my $umsatz         = 0;
+    my $gegenkonto     = "";
+    my $konto          = "";
+    my $belegfeld1     = "";
+    my $datum          = "";
+    my $waehrung       = "";
+    my $buchungstext   = "";
+    my $belegfeld2     = "";
+    my $datevautomatik = 0;
+    my $taxkey         = 0;
+    my $charttax       = 0;
+    my $ustid          ="";
+    my ($haben, $soll);
+    for (my $i = 0; $i < $trans_lines; $i++) {
+      if ($trans_lines == 2) {
+        if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
+          $umsatz = $transaction->[$i]->{'amount'};
+        }
+      } else {
+        if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
+          $umsatz = $transaction->[$i]->{'umsatz'};
+        }
+      }
+      if ($transaction->[$i]->{'datevautomatik'}) {
+        $datevautomatik = 1;
+      }
+      if ($transaction->[$i]->{'taxkey'}) {
+        $taxkey = $transaction->[$i]->{'taxkey'};
+      }
+      if ($transaction->[$i]->{'charttax'}) {
+        $charttax = $transaction->[$i]->{'charttax'};
+      }
+      if ($transaction->[$i]->{'amount'} > 0) {
+        $haben = $i;
+      } else {
+        $soll = $i;
+      }
+    }
+
+    if ($trans_lines >= 2) {
+
+      $datev_data{'gegenkonto'} = $transaction->[$haben]->{'accno'};
+      $datev_data{'konto'}      = $transaction->[$soll]->{'accno'};
+      if ($transaction->[$haben]->{'invnumber'} ne "") {
+        $datev_data{belegfeld1} = $transaction->[$haben]->{'invnumber'};
+      }
+      $datev_data{datum} = $transaction->[$haben]->{'transdate'};
+      $datev_data{waehrung} = 'EUR';
+      $datev_data{kost1} = $transaction->[$haben]->{'departmentdescription'};
+      $datev_data{kost2} = $transaction->[$haben]->{'projectdescription'};
+
+      if ($transaction->[$haben]->{'name'} ne "") {
+        $datev_data{buchungstext} = $transaction->[$haben]->{'name'};
+      }
+      if (($transaction->[$haben]->{'ustid'} // '') ne "") {
+        $datev_data{ustid} = $transaction->[$haben]->{'ustid'};
+      }
+      if (($transaction->[$haben]->{'duedate'} // '') ne "") {
+        $datev_data{belegfeld2} = $transaction->[$haben]->{'duedate'};
+      }
+    }
+    $datev_data{soll_haben_kennzeichen} = (0 < $umsatz) ? 'H' : 'S';
+    $datev_data{umsatz} = abs($umsatz); # sales invoices without tax have a different sign???
+
+    # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
+    # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
+    # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
+    # Skript angelegt werden.
+    # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
+    # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
+    # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
+    # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
+    # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
+    #
+
+    # only set buchungsschluessel if the following conditions are met:
+    if (   ( $datevautomatik || $taxkey)
+        && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
+      # $datev_data{buchungsschluessel} = !$datevautomatik ? $taxkey : "4";
+      $datev_data{buchungsschluessel} = $taxkey;
+    }
+
+    push(@datev_lines, \%datev_data);
+  }
+
+  # example of modifying export data:
+  # foreach my $datev_line ( @datev_lines ) {
+  #   if ( $datev_line{"konto"} eq '1234' ) {
+  #     $datev_line{"konto"} = '9999';
+  #   }
+  # }
+  #
+
+  return \@datev_lines;
+}
+
+
 sub kne_buchungsexport {
   $main::lxdebug->enter_sub();
 
 sub kne_buchungsexport {
   $main::lxdebug->enter_sub();
 
@@ -798,160 +1021,92 @@ sub kne_buchungsexport {
 
   my @filenames;
 
 
   my @filenames;
 
-  my $filename    = "ED00000";
+  my $filename    = "ED00001";
   my $evfile      = "EV01";
   my @ed_versionset;
   my $evfile      = "EV01";
   my @ed_versionset;
-  my $fileno = 0;
+  my $fileno      = 1;
+  my $ed_filename = $self->export_path . $filename;
 
   my $fromto = $self->fromto;
 
 
   my $fromto = $self->fromto;
 
-  $self->_get_transactions($fromto);
-
+  $self->generate_datev_data(from_to => $self->fromto); # fetches data from db, transforms data and fills $self->{DATEV}
   return if $self->errors;
 
   return if $self->errors;
 
-  my $counter = 0;
+  my @datev_lines = @{ $self->generate_datev_lines };
 
 
-  while (scalar(@{ $self->{DATEV} || [] })) {
-    my $umsatzsumme = 0;
-    $filename++;
-    my $ed_filename = $self->export_path . $filename;
-    push(@filenames, $filename);
-    my $header = $self->make_kne_data_header($form);
-
-    my $kne_file = SL::DATEV::KNEFile->new();
-    $kne_file->add_block($header);
-
-    while (scalar(@{ $self->{DATEV} }) > 0) {
-      my $transaction = shift @{ $self->{DATEV} };
-      my $trans_lines = scalar(@{$transaction});
-      $counter++;
-
-      my $umsatz         = 0;
-      my $gegenkonto     = "";
-      my $konto          = "";
-      my $belegfeld1     = "";
-      my $datum          = "";
-      my $waehrung       = "";
-      my $buchungstext   = "";
-      my $belegfeld2     = "";
-      my $datevautomatik = 0;
-      my $taxkey         = 0;
-      my $charttax       = 0;
-      my $ustid          ="";
-      my ($haben, $soll);
-      my $iconv          = $::locale->{iconv_utf8};
-      my %umlaute = ($iconv->convert('ä') => 'ae',
-                     $iconv->convert('ö') => 'oe',
-                     $iconv->convert('ü') => 'ue',
-                     $iconv->convert('Ä') => 'Ae',
-                     $iconv->convert('Ö') => 'Oe',
-                     $iconv->convert('Ü') => 'Ue',
-                     $iconv->convert('ß') => 'sz');
-      for (my $i = 0; $i < $trans_lines; $i++) {
-        if ($trans_lines == 2) {
-          if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
-            $umsatz = $transaction->[$i]->{'amount'};
-          }
-        } else {
-          if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
-            $umsatz = $transaction->[$i]->{'umsatz'};
-          }
-        }
-        if ($transaction->[$i]->{'datevautomatik'}) {
-          $datevautomatik = 1;
-        }
-        if ($transaction->[$i]->{'taxkey'}) {
-          $taxkey = $transaction->[$i]->{'taxkey'};
-        }
-        if ($transaction->[$i]->{'charttax'}) {
-          $charttax = $transaction->[$i]->{'charttax'};
-        }
-        if ($transaction->[$i]->{'amount'} > 0) {
-          $haben = $i;
-        } else {
-          $soll = $i;
-        }
-      }
-      # Umwandlung von Umlauten und Sonderzeichen in erlaubte Zeichen bei Textfeldern
+
+  my $umsatzsumme = sum map { $_->{umsatz} } @datev_lines;
+
+  # prepare kne file, everything gets stored in ED00001
+  my $header = $self->make_kne_data_header($form);
+  my $kne_file = SL::DATEV::KNEFile->new();
+  $kne_file->add_block($header);
+
+  my $iconv   = $::locale->{iconv_utf8};
+  my %umlaute = ($iconv->convert('ä') => 'ae',
+                 $iconv->convert('ö') => 'oe',
+                 $iconv->convert('ü') => 'ue',
+                 $iconv->convert('Ä') => 'Ae',
+                 $iconv->convert('Ö') => 'Oe',
+                 $iconv->convert('Ü') => 'Ue',
+                 $iconv->convert('ß') => 'sz');
+
+  # add the data from @datev_lines to the kne_file, formatting as needed
+  foreach my $kne ( @datev_lines ) {
+    $kne_file->add_block("+" . $kne_file->format_amount(abs($kne->{umsatz}), 0));
+
+    # only add buchungsschluessel if it was previously defined
+    $kne_file->add_block("\x6C" . $kne->{buchungsschluessel}) if defined $kne->{buchungsschluessel};
+
+    # ($kne->{gegenkonto}) = $kne->{gegenkonto} =~ /^(\d+)/;
+    $kne_file->add_block("a" . trim_leading_zeroes($kne->{gegenkonto}));
+
+    if ( $kne->{belegfeld1} ) {
+      my $invnumber = $kne->{belegfeld1};
       foreach my $umlaut (keys(%umlaute)) {
       foreach my $umlaut (keys(%umlaute)) {
-        $transaction->[$haben]->{'invnumber'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
-        $transaction->[$haben]->{'name'}      =~ s/${umlaut}/${umlaute{$umlaut}}/g;
+        $invnumber =~ s/${umlaut}/${umlaute{$umlaut}}/g;
       }
       }
+      $invnumber =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
+      $invnumber =  substr($invnumber, 0, 12);
+      $invnumber =~ s/\ *$//;
+      $kne_file->add_block("\xBD" . $invnumber . "\x1C");
+    }
 
 
-      $transaction->[$haben]->{'invnumber'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
-      $transaction->[$haben]->{'name'}      =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
+    $kne_file->add_block("\xBE" . &datetofour($kne->{belegfeld2},1) . "\x1C");
 
 
-      $transaction->[$haben]->{'invnumber'} =  substr($transaction->[$haben]->{'invnumber'}, 0, 12);
-      $transaction->[$haben]->{'name'}      =  substr($transaction->[$haben]->{'name'}, 0, 30);
-      $transaction->[$haben]->{'invnumber'} =~ s/\ *$//;
-      $transaction->[$haben]->{'name'}      =~ s/\ *$//;
+    $kne_file->add_block("d" . &datetofour($kne->{datum},0));
 
 
-      if ($trans_lines >= 2) {
+    # ($kne->{konto}) = $kne->{konto} =~ /^(\d+)/;
+    $kne_file->add_block("e" . trim_leading_zeroes($kne->{konto}));
 
 
-        $gegenkonto = "a" . trim_leading_zeroes($transaction->[$haben]->{'accno'});
-        $konto      = "e" . trim_leading_zeroes($transaction->[$soll]->{'accno'});
-        if ($transaction->[$haben]->{'invnumber'} ne "") {
-          $belegfeld1 = "\xBD" . $transaction->[$haben]->{'invnumber'} . "\x1C";
-        }
-        $datum = "d";
-        $datum .= &datetofour($transaction->[$haben]->{'transdate'}, 0);
-        $waehrung = "\xB3" . "EUR" . "\x1C";
-        if ($transaction->[$haben]->{'name'} ne "") {
-          $buchungstext = "\x1E" . $transaction->[$haben]->{'name'} . "\x1C";
-        }
-        if (($transaction->[$haben]->{'ustid'} // '') ne "") {
-          $ustid = "\xBA" . $transaction->[$haben]->{'ustid'} . "\x1C";
-        }
-        if (($transaction->[$haben]->{'duedate'} // '') ne "") {
-          $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
-        }
-      }
+    my $name = $kne->{buchungstext};
+    foreach my $umlaut (keys(%umlaute)) {
+      $name =~ s/${umlaut}/${umlaute{$umlaut}}/g;
+    }
+    $name =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
+    $name =  substr($name, 0, 30);
+    $name =~ s/\ *$//;
+    $kne_file->add_block("\x1E" . $name . "\x1C");
 
 
-      $umsatz       = $kne_file->format_amount(abs($umsatz), 0);
-      $umsatzsumme += $umsatz;
-      $kne_file->add_block("+" . $umsatz);
-
-      # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
-      # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
-      # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
-      # Skript angelegt werden.
-      # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
-      # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
-      # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
-      # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
-      # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
-      #
-      if (   ( $datevautomatik || $taxkey)
-          && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
-#         $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
-        $kne_file->add_block("\x6C${taxkey}");
-      }
+    $kne_file->add_block("\xBA" . $kne->{'ustid'}    . "\x1C") if $kne->{'ustid'};
 
 
-      $kne_file->add_block($gegenkonto);
-      $kne_file->add_block($belegfeld1);
-      $kne_file->add_block($belegfeld2);
-      $kne_file->add_block($datum);
-      $kne_file->add_block($konto);
-      $kne_file->add_block($buchungstext);
-      $kne_file->add_block($ustid);
-      $kne_file->add_block($waehrung . "\x79");
-    }
+    $kne_file->add_block("\xB3" . $kne->{'waehrung'} . "\x1C" . "\x79");
+  };
 
 
-    my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
+  $umsatzsumme          = $kne_file->format_amount(abs($umsatzsumme), 0);
+  my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
 
 
-    $kne_file->add_block($mandantenendsumme);
-    $kne_file->flush();
+  $kne_file->add_block($mandantenendsumme);
+  $kne_file->flush();
 
 
-    open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
-    print(ED $kne_file->get_data());
-    close(ED);
+  open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
+  print(ED $kne_file->get_data());
+  close(ED);
 
 
-    $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
-    $fileno++;
-  }
+  $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
 
   #Make EV Verwaltungsdatei
 
   #Make EV Verwaltungsdatei
-  my $ev_header = $self->make_ev_header($form, $fileno);
+  my $ev_header   = $self->make_ev_header($form, $fileno);
   my $ev_filename = $self->export_path . $evfile;
   push(@filenames, $evfile);
   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
   my $ev_filename = $self->export_path . $evfile;
   push(@filenames, $evfile);
   open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
@@ -1075,6 +1230,218 @@ sub kne_stammdatenexport {
   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 }
 
   return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
 }
 
+sub _format_accno {
+  my ($accno) = @_;
+  return $accno . ('0' x (6 - min(length($accno), 6)));
+}
+
+sub csv_export_for_tax_accountant {
+  my ($self) = @_;
+
+  $self->generate_datev_data(from_to => $self->fromto);
+
+  foreach my $transaction (@{ $self->{DATEV} }) {
+    foreach my $entry (@{ $transaction }) {
+      $entry->{sortkey} = join '-', map { lc } (DateTime->from_kivitendo($entry->{transdate})->strftime('%Y%m%d'), $entry->{name}, $entry->{reference});
+    }
+  }
+
+  my %transactions =
+    partition_by { $_->[0]->{table} }
+    sort_by      { $_->[0]->{sortkey} }
+    grep         { 2 == scalar(@{ $_ }) }
+    @{ $self->{DATEV} };
+
+  my %column_defs = (
+    acc_trans_id      => { 'text' => $::locale->text('ID'), },
+    amount            => { 'text' => $::locale->text('Amount'), },
+    credit_accname    => { 'text' => $::locale->text('Credit Account Name'), },
+    credit_accno      => { 'text' => $::locale->text('Credit Account'), },
+    debit_accname     => { 'text' => $::locale->text('Debit Account Name'), },
+    debit_accno       => { 'text' => $::locale->text('Debit Account'), },
+    invnumber         => { 'text' => $::locale->text('Reference'), },
+    name              => { 'text' => $::locale->text('Name'), },
+    notes             => { 'text' => $::locale->text('Notes'), },
+    tax               => { 'text' => $::locale->text('Tax'), },
+    taxkey            => { 'text' => $::locale->text('Taxkey'), },
+    tax_accname       => { 'text' => $::locale->text('Tax Account Name'), },
+    tax_accno         => { 'text' => $::locale->text('Tax Account'), },
+    transdate         => { 'text' => $::locale->text('Transdate'), },
+    vcnumber          => { 'text' => $::locale->text('Customer/Vendor Number'), },
+  );
+
+  my @columns = qw(
+    acc_trans_id name           vcnumber
+    transdate    invnumber      amount
+    debit_accno  debit_accname
+    credit_accno credit_accname
+    tax
+    tax_accno    tax_accname    taxkey
+    notes
+  );
+
+  my %filenames_by_type = (
+    ar => $::locale->text('AR Transactions'),
+    ap => $::locale->text('AP Transactions'),
+    gl => $::locale->text('GL Transactions'),
+  );
+
+  my @filenames;
+  foreach my $type (qw(ap ar)) {
+    my %csvs = (
+      invoices   => {
+        content  => '',
+        filename => sprintf('%s %s - %s.csv', $filenames_by_type{$type}, $self->from->to_kivitendo, $self->to->to_kivitendo),
+        csv      => Text::CSV_XS->new({
+          binary   => 1,
+          eol      => "\n",
+          sep_char => ";",
+        }),
+      },
+      payments   => {
+        content  => '',
+        filename => sprintf('Zahlungen %s %s - %s.csv', $filenames_by_type{$type}, $self->from->to_kivitendo, $self->to->to_kivitendo),
+        csv      => Text::CSV_XS->new({
+          binary   => 1,
+          eol      => "\n",
+          sep_char => ";",
+        }),
+      },
+    );
+
+    foreach my $csv (values %csvs) {
+      $csv->{out} = IO::File->new($self->export_path . '/' . $csv->{filename}, '>:encoding(utf8)') ;
+      $csv->{csv}->print($csv->{out}, [ map { $column_defs{$_}->{text} } @columns ]);
+
+      push @filenames, $csv->{filename};
+    }
+
+    foreach my $transaction (@{ $transactions{$type} }) {
+      my $is_payment     = any { $_->{link} =~ m{A[PR]_paid} } @{ $transaction };
+      my $csv            = $is_payment ? $csvs{payments} : $csvs{invoices};
+
+      my ($soll, $haben) = map { $transaction->[$_] } ($transaction->[0]->{amount} > 0 ? (1, 0) : (0, 1));
+      my $tax            = defined($soll->{tax_accno})  ? $soll : $haben;
+      my $amount         = defined($soll->{net_amount}) ? $soll : $haben;
+      $haben->{notes}    = ($haben->{memo} || $soll->{memo}) if $is_payment;
+      $haben->{notes}  //= '';
+      $haben->{notes}    =  SL::HTML::Util->strip($haben->{notes});
+      $haben->{notes}    =~ s{\r}{}g;
+      $haben->{notes}    =~ s{\n+}{ }g;
+
+      my %row            = (
+        amount           => $::form->format_amount({ numberformat => '1000,00' }, abs($amount->{amount}), 2),
+        debit_accno      => _format_accno($soll->{accno}),
+        debit_accname    => $soll->{accname},
+        credit_accno     => _format_accno($haben->{accno}),
+        credit_accname   => $haben->{accname},
+        tax              => $::form->format_amount({ numberformat => '1000,00' }, abs($amount->{amount}) - abs($amount->{net_amount}), 2),
+        notes            => $haben->{notes},
+        (map { ($_ => $tax->{$_})                    } qw(taxkey tax_accname tax_accno)),
+        (map { ($_ => ($haben->{$_} // $soll->{$_})) } qw(acc_trans_id invnumber name vcnumber transdate)),
+      );
+
+      $csv->{csv}->print($csv->{out}, [ map { $row{$_} } @columns ]);
+    }
+
+    $_->{out}->close for values %csvs;
+  }
+
+  $self->add_filenames(@filenames);
+
+  return { download_token => $self->download_token, filenames => \@filenames };
+}
+
+sub csv_buchungsexport {
+  my $self = shift;
+  my %params = @_;
+
+  $self->generate_datev_data(from_to => $self->fromto);
+  return if $self->errors;
+
+  my @datev_lines = @{ $self->generate_datev_lines };
+
+  my @csv_columns = SL::DATEV::CSV->kivitendo_to_datev();
+  my @csv_headers = SL::DATEV::CSV->generate_csv_header(
+                      from                     => $self->from->ymd(''),
+                      to                       => $self->to->ymd(''),
+                      first_day_of_fiscal_year => $self->to->year . '0101',
+                      locked                   => 0
+                    );
+
+  my @array_of_datev;
+
+  # 2 Headers
+  push @array_of_datev, \@csv_headers;
+  push @array_of_datev, [ map { $_->{csv_header_name} } @csv_columns ];
+
+  my @warnings;
+  foreach my $row ( @datev_lines ) {
+    my @current_datev_row;
+
+    # shorten strings
+    if ($row->{belegfeld1}) {
+      $row->{buchungsbes} = $row->{belegfeld1} if $row->{belegfeld1};
+      $row->{belegfeld1}  = substr($row->{belegfeld1}, 0, 12);
+      $row->{buchungsbes} = substr($row->{buchungsbes}, 0, 60);
+    }
+
+    $row->{datum}       = datetofour($row->{datum}, 0);
+    $row->{kost1}       = substr($row->{kost1}, 0, 8) if $row->{kost1};
+    $row->{kost2}       = substr($row->{kost2}, 0, 8) if $row->{kost2};
+
+    # , as decimal point and trim for UstID
+    $row->{umsatz}      =~ s/\./,/;
+    $row->{ustid}       =~ s/\s//g if $row->{ustid}; # trim whitespace
+
+    foreach my $column (@csv_columns) {
+      if (exists $column->{max_length} && $column->{kivi_datev_name} ne 'not yet implemented') {
+        # check max length
+        die "Incorrect lenght of field" if length($row->{ $column->{kivi_datev_name} }) > $column->{max_length};
+      }
+      if (exists $column->{valid_check} && $column->{kivi_datev_name} ne 'not yet implemented') {
+        # more checks, listed as user warnings
+        push @warnings, t8("Wrong field value '#1' for field '#2' for the transaction" .
+                            " with amount '#3'",$row->{ $column->{kivi_datev_name} },
+                            $column->{kivi_datev_name},$row->{umsatz})
+          unless ($column->{valid_check}->($row->{ $column->{kivi_datev_name} }));
+      }
+      push @current_datev_row, $row->{ $column->{kivi_datev_name} };
+    }
+    push @array_of_datev, \@current_datev_row;
+  }
+  $self->warnings(@warnings) if @warnings;
+  return \@array_of_datev;
+}
+
+sub _csv_buchungsexport_to_file {
+  my $self   = shift;
+  my %params = @_;
+
+  # we can definitely deny shorter data structures
+  croak ("Need at least 2 rows for header info") unless scalar @{ $params{data} } > 1;
+
+  my $filename = "EXTF_DATEV_kivitendo" . $self->from->ymd() . '-' . $self->to->ymd() . ".csv";
+  my @data = \$params{data};
+
+  my $csv = Text::CSV_XS->new({
+              binary       => 1,
+              sep_char     => ";",
+              always_quote => 1,
+              eol          => "\r\n",
+            }) or die "Cannot use CSV: ".Text::CSV_XS->error_diag();
+
+  if ($csv->version >= 1.18) {
+    # get rid of stupid datev warnings in "Validity program"
+    $csv->quote_empty(1);
+  }
+
+  my $csv_file = IO::File->new($self->export_path . '/' . $filename, '>:encoding(cp1252)') or die "Can't open: $!";
+  $csv->print($csv_file, $_) for @{ $params{data} };
+  $csv_file->close;
+
+  return { download_token => $self->download_token, filenames => $params{filename} };
+}
 sub DESTROY {
   clean_temporary_directories();
 }
 sub DESTROY {
   clean_temporary_directories();
 }
@@ -1144,6 +1511,16 @@ SL::DATEV - kivitendo DATEV Export module
   my $path     = $datev->export_path;
   my @files    = glob("$path/*");
 
   my $path     = $datev->export_path;
   my @files    = glob("$path/*");
 
+  # Only test the datev data of a specific trans_id, without generating an
+  # export file, but filling $datev->errors if errors exist
+
+  my $datev = SL::DATEV->new(
+    trans_id   => $invoice->trans_id,
+  );
+  $datev->generate_datev_data;
+  # if ($datev->errors) { ...
+
+
 =head1 DESCRIPTION
 
 This module implements the DATEV export standard. For usage see above.
 =head1 DESCRIPTION
 
 This module implements the DATEV export standard. For usage see above.
@@ -1156,6 +1533,30 @@ This module implements the DATEV export standard. For usage see above.
 
 Generic constructor. See section attributes for information about what to pass.
 
 
 Generic constructor. See section attributes for information about what to pass.
 
+=item generate_datev_data
+
+Fetches all transactions from the database (via a trans_id or a date range),
+and does an initial transformation (e.g. filters out tax, determines
+the brutto amount, checks split transactions ...) and stores this data in
+$self->{DATEV}.
+
+If any errors are found these are collected in $self->errors.
+
+This function is needed for all the exports, but can be also called
+independently in order to check transactions for DATEV compatibility.
+
+=item generate_datev_lines
+
+Parse the data in $self->{DATEV} and transform it into a format that can be
+used by DATEV, e.g. determines Konto and Gegenkonto, the taxkey, ...
+
+The transformed data is returned as an arrayref, which is ready to be converted
+to a DATEV data format, e.g. KNE, OBE, CSV, ...
+
+At this stage the "DATEV rule" has already been applied to the taxkeys, i.e.
+entries with datevautomatik have an empty taxkey, as the taxkey is already
+determined by the chart.
+
 =item get_datev_stamm
 
 Loads DATEV Stammdaten and returns as hashref.
 =item get_datev_stamm
 
 Loads DATEV Stammdaten and returns as hashref.
@@ -1219,6 +1620,54 @@ Returns a list of errors that occured. If no errors occured, the export was a su
 Exports data. You have to have set L<exporttype> and L<format> or an error will
 occur. OBE exports are currently not implemented.
 
 Exports data. You have to have set L<exporttype> and L<format> or an error will
 occur. OBE exports are currently not implemented.
 
+=item csv_export_for_tax_accountant
+
+Generates up to four downloadable csv files containing data about sales and
+purchase invoices, and their respective payments:
+
+Example:
+  my $startdate = DateTime->new(year => 2012, month =>  1, day =>  1);
+  my $enddate   = DateTime->new(year => 2012, month => 12, day => 31);
+  SL::DATEV->new(from => $startdate, to => $enddate)->csv_export_for_tax_accountant;
+  # {
+  #   'download_token' => '1488551625-815654-22430',
+  #   'filenames' => [
+  #                    'Zahlungen Kreditorenbuchungen 2012-01-01 - 2012-12-31.csv',
+  #                    'Kreditorenbuchungen 2012-01-01 - 2012-12-31.csv',
+  #                    'Zahlungen Debitorenbuchungen 2012-01-01 - 2012-12-31.csv',
+  #                    'Debitorenbuchungen 2012-01-01 - 2012-12-31.csv'
+  #                  ]
+  # };
+
+
+=item csv_buchungsexport
+
+Generates the CSV-Format data for the CSV DATEV export and returns
+an 2-dimensional array as an array_ref.
+
+Requires $self->fromto for a valid DATEV header.
+
+Furthermore we assume that the first day of the fiscal year is
+the first of January and we cannot guarantee that our data in kivitendo
+is locked, that means a booking cannot be modified after a defined (vat tax)
+period.
+Some validity checks (max_length and regex) will be done if the
+data structure contains them and the field is defined.
+
+To add or alter the structure of the data take a look at SL::DATEV::CSV.pm
+
+=item _csv_buchungsexport_to_file
+
+Generates one downloadable csv file wrapped in a zip archive.
+Basically this method is just a thin wrapper for TEXT::CSV_XS.pm
+
+Generates a CSV-file with the same encodings as defined in DATEV Format CSV 2015:
+ $ file
+ $ EXTF_Buchungsstapel.csv: ISO-8859 text, with very long lines, with CRLF line terminators
+
+Usage: _csv_buchungsexport_to_file($self, data => $self->csv_buchungsexport);
+
+
 =back
 
 =head1 ATTRIBUTES
 =back
 
 =head1 ATTRIBUTES
@@ -1232,6 +1681,10 @@ This is a list of attributes set in either the C<new> or a method of the same na
 Set a database handle to use in the process. This allows for an export to be
 done on a transaction in progress without committing first.
 
 Set a database handle to use in the process. This allows for an export to be
 done on a transaction in progress without committing first.
 
+Note: If you don't want this code to commit, simply providing a dbh is not
+enough enymore. You'll have to wrap the call into a transaction yourself, so
+that the internal transaction does not commit.
+
 =item exporttype
 
 See L<CONSTANTS> for possible values. This MUST be set before export is called.
 =item exporttype
 
 See L<CONSTANTS> for possible values. This MUST be set before export is called.
@@ -1343,6 +1796,7 @@ OBE export is currently not implemented.
 =head1 SEE ALSO
 
 L<SL::DATEV::KNEFile>
 =head1 SEE ALSO
 
 L<SL::DATEV::KNEFile>
+L<SL::DATEV::CSV>
 
 =head1 AUTHORS
 
 
 =head1 AUTHORS