GDPDU: DATEV-ähnlicher Buchungsexport Rohversion
authorSven Schöling <s.schoeling@linet-services.de>
Tue, 27 Oct 2015 17:24:15 +0000 (18:24 +0100)
committerSven Schöling <s.schoeling@linet-services.de>
Fri, 28 Oct 2016 15:11:18 +0000 (17:11 +0200)
SL/Controller/Gdpdu.pm
SL/DATEV.pm
SL/GDPDU.pm
locale/de/all
templates/webpages/gdpdu/filter.html

index 8b0cbb0..871b0ab 100644 (file)
@@ -12,7 +12,7 @@ use SL::Locale::String qw(t8);
 use SL::Helper::Flash;
 
 use Rose::Object::MakeMethods::Generic (
-  'scalar --get_set_init' => [ qw(from to tables) ],
+  'scalar --get_set_init' => [ qw(from to) ],
 );
 
 __PACKAGE__->run_before('check_auth');
@@ -39,8 +39,7 @@ sub action_export {
     location   => $::instance_conf->get_address,
     from       => $self->from,
     to         => $self->to,
-    tables     => $self->tables,
-    all_tables => !@{ $self->tables } && $::form->{all_tables},
+    all_tables => $::form->{all_tables},
   );
 
   my $filename = $gdpdu->generate_export;
@@ -57,19 +56,6 @@ sub check_inputs {
 
   my $error = 0;
 
-  if ($::form->{tables}) {
-    $self->tables([ keys %{ $::form->{tables} } ]);
-    # theese three get inferred
-    push @{ $self->tables }, 'invoice'              if $::form->{tables}{ar} || $::form->{tables}{ap};
-    push @{ $self->tables }, 'orderitems'           if $::form->{tables}{oe};
-    push @{ $self->tables }, 'delivery_order_items' if $::form->{tables}{delivery_orders};
-  }
-
-  if (!@{ $self->tables } && !$::form->{all_tables}) {
-    flash('error', t8('No, I really do need checked tables to export.'));
-    $error = 1;
-  }
-
   if (!$::form->{from}) {
     my $epoch = DateTime->new(day => 1, month => 1, year => 1900);
     flash('info', t8('No start date given, setting to #1', $epoch->to_kivitendo));
@@ -86,6 +72,5 @@ sub check_inputs {
 
 sub init_from { DateTime->from_kivitendo($::form->{from}) }
 sub init_to { DateTime->from_kivitendo($::form->{to}) }
-sub init_tables { [ ] }
 
 1;
index be3ec95..d99bd66 100644 (file)
@@ -32,12 +32,17 @@ use strict;
 use SL::DBUtils;
 use SL::DATEV::KNEFile;
 use SL::DB;
+use SL::HTML::Util ();
 
 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);
 
 {
@@ -45,13 +50,14 @@ use Time::HiRes qw(gettimeofday);
   use constant {
     DATEV_ET_BUCHUNGEN => $i++,
     DATEV_ET_STAMM     => $i++,
+    DATEV_ET_CSV       => $i++,
 
     DATEV_FORMAT_KNE   => $i++,
     DATEV_FORMAT_OBE   => $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);
 our @EXPORT_OK = (@export_constants);
 our %EXPORT_TAGS = (CONSTANTS => [ @export_constants ]);
 
@@ -324,6 +330,8 @@ sub kne_export {
     $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';
   }
@@ -349,9 +357,10 @@ sub _sign {
 
 sub _get_transactions {
   $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;
 
@@ -374,18 +383,21 @@ 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    =
-    qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey,
+    qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey, ac.memo,
          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,
+         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,
          t.rate AS taxrate,
          'ar' as table
+         tc.accno AS tax_accno, tc.description AS tax_accname,
+         ar.notes
        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)
        WHERE (ar.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
@@ -393,18 +405,21 @@ sub _get_transactions {
 
        UNION ALL
 
-       SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey,
+       SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey, ac.memo,
          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,
+         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,
          t.rate AS taxrate,
          'ap' as table
+         tc.accno AS tax_accno, tc.description AS tax_accname,
+         ap.notes
        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)
        WHERE (ap.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
@@ -412,17 +427,20 @@ sub _get_transactions {
 
        UNION ALL
 
-       SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey,
+       SELECT ac.acc_trans_id, ac.transdate, 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.description AS name, NULL as ustid,
-         c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
+         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,
          t.rate AS taxrate,
          'gl' as table
+         tc.accno AS tax_accno, tc.description AS tax_accname,
+         gl.notes
        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)
        WHERE (gl.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
@@ -812,7 +830,7 @@ sub kne_buchungsexport {
 
   my $fromto = $self->fromto;
 
-  $self->_get_transactions($fromto);
+  $self->_get_transactions(from_to => $fromto);
 
   return if $self->errors;
 
@@ -1082,6 +1100,128 @@ sub kne_stammdatenexport {
   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->_get_transactions(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('Invoice Date'), },
+    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 DESTROY {
   clean_temporary_directories();
 }
index 30d0361..0978df9 100644 (file)
@@ -13,15 +13,16 @@ use XML::Writer;
 use Archive::Zip;
 use File::Temp ();
 use File::Spec ();
-use List::UtilsBy qw(partition_by);
+use List::MoreUtils qw(any);
+use List::UtilsBy qw(partition_by sort_by);
 
 use SL::DB::Helper::ALL; # since we work on meta data, we need everything
 use SL::DB::Helper::Mappings;
 use SL::Locale::String qw(t8);
 
 use Rose::Object::MakeMethods::Generic (
-  scalar                  => [ qw(from to tables writer company location) ],
-  'scalar --get_set_init' => [ qw(files tempfiles export_ids) ],
+  scalar                  => [ qw(from to writer company location) ],
+  'scalar --get_set_init' => [ qw(files tempfiles export_ids tables) ],
 );
 
 # in this we find:
@@ -32,19 +33,41 @@ use Rose::Object::MakeMethods::Generic (
 # keep:        arrayref of columns that should be saved for further referencing
 # tables:      arrayref with one column and one or many table.column references that were kept earlier
 my %known_tables = (
-  ar                    => { name => t8('Invoice'),                 description => t8('Sales Invoices and Accounts Receivables'),   keep => [ qw(id customer_id vendor_id) ], transdate => 'transdate', },
-  ap                    => { name => t8('Purchase Invoice'),        description => t8('Purchase Invoices and Accounts Payables'),   keep => [ qw(id customer_id vendor_id) ], transdate => 'transdate', },
-  oe                    => { name => t8('Orders'),                  description => t8('Orders and Quotations, Sales and Purchase'), keep => [ qw(id customer_id vendor_id) ], transdate => 'transdate', },
-  delivery_orders       => { name => t8('Delivery Orders'),         description => t8('Delivery Orders'),                           keep => [ qw(id customer_id vendor_id) ], transdate => 'transdate', },
-  gl                    => { name => t8('General Ledger'),          description => t8('General Ledger Entries'),                    keep => [ qw(id) ],                       transdate => 'transdate', },
-  invoice               => { name => t8('Invoice Positions'),       description => t8('Positions for all Invoices'),                keep => [ qw(parts_id) ], tables => [ trans_id => "ar.id", "ap.id" ] },
-  orderitems            => { name => t8('OrderItems'),              description => t8('Positions for all Orders'),                  keep => [ qw(parts_id) ], tables => [ trans_id => "oe.id" ] },
-  delivery_order_items  => { name => t8('Delivery Order Items'),    description => t8('Positions for all Delivery Orders'),                      keep => [ qw(parts_id) ], tables => [ delivery_order_id => "delivery_orders.id" ] },
-  acc_trans             => { name => t8('Transactions'),            description => t8('All general ledger entries'),                keep => [ qw(chart_id) ], tables => [ trans_id => "ar.id", "ap.id", "oe.id", "delivery_orders.id", "gl.id" ] },
-  chart                 => { name => t8('Charts'),                  description => t8('Chart of Accounts'),                                                   tables => [ id => "acc_trans.chart_id" ] },
-  customer              => { name => t8('Customers'),               description => t8('Customer Master Data'),                                                tables => [ id => "ar.customer_id", "ap.customer_id", "oe.customer_id", "delivery_orders.customer_id" ] },
-  vendor                => { name => t8('Vendors'),                 description => t8('Vendor Master Data'),                                                  tables => [ id => "ar.vendor_id",   "ap.vendor_id",   "oe.vendor_id",   "delivery_orders.vendor_id" ] },
-  parts                 => { name => t8('Parts'),                   description => t8('Parts, Services, and Assemblies'),                                     tables => [ id => "invoice.parts_id", "orderitems.parts_id", "delivery_order_items.parts_id" ] },
+  chart                 => { name => t8('Charts'),                  description => t8('Chart of Accounts'),       primary_key => 'accno'     },
+  customer              => { name => t8('Customers'),               description => t8('Customer Master Data'),                                    },
+  vendor                => { name => t8('Vendors'),                 description => t8('Vendor Master Data'),                                               },
+);
+
+my %datev_column_defs = (
+  acc_trans_id      => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('ID'), primary_key => 1 },
+  amount            => { type => 'Rose::DB::Object::Metadata::Column::Numeric', text => t8('Amount'), },
+  credit_accname    => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Credit Account Name'), },
+  credit_accno      => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Credit Account'), },
+  debit_accname     => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Debit Account Name'), },
+  debit_accno       => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Debit Account'), },
+  invnumber         => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Reference'), },
+  name              => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Name'), },
+  notes             => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Notes'), },
+  tax               => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Tax'), },
+  taxkey            => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Taxkey'), },
+  tax_accname       => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Tax Account Name'), },
+  tax_accno         => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Tax Account'), },
+  transdate         => { type => 'Rose::DB::Object::Metadata::Column::Date',    text => t8('Invoice Date'), },
+  vcnumber          => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Customer/Vendor Number'), },
+  customer_id       => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Customer ID'), },
+  vendor_id         => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Vendor ID'), },
+);
+
+my @datev_columns = qw(
+  acc_trans_id
+  customer_id vendor_id
+  name           vcnumber
+  transdate    invnumber      amount
+  debit_accno  debit_accname
+  credit_accno credit_accname
+  tax
+  tax_accno    tax_accname    taxkey
+  notes
 );
 
 # rows in this listing are tiers.
@@ -108,6 +131,8 @@ sub generate_export {
     $self->do_csv_export($_);
   }
 
+  $self->do_datev_csv_export;
+
   # write xml file
   $self->do_xml_file;
 
@@ -157,6 +182,7 @@ sub do_xml_file {
       for (reverse $self->sorted_tables) { $self  # see CAVEATS for table order
         ->table($_)
       }
+      $self->do_datev_xml_table;
     })
   });
   close($fh);
@@ -194,7 +220,11 @@ sub _table_columns {
   my $package = SL::DB::Helper::Mappings::get_package_for_table($table);
 
   # PrimaryKeys must come before regular columns, so partition first
-  partition_by { 1 * $_->is_primary_key_member } $package->meta->columns;
+  partition_by {
+    $known_tables{$table}{primary_key}
+      ? 1 * ($_ eq $known_tables{$table}{primary_key})
+      : 1 * $_->is_primary_key_member
+  } $package->meta->columns;
 }
 
 sub columns {
@@ -255,6 +285,140 @@ sub foreign_keys {
   }
 }
 
+sub do_datev_xml_table {
+  my ($self) = @_;
+  my $writer = $self->writer;
+
+  $self->tag('Table', sub { $self
+    ->tag('URL', "transaction.csv")
+    ->tag('Name', t8('Transactions'))
+    ->tag('Description', t8('Transactions'))
+    ->tag('Validity', sub { $self
+      ->tag('Range', sub { $self
+        ->tag('From', $self->from->to_kivitendo(dateformat => 'dd.mm.yyyy'))
+        ->tag('To',   $self->to->to_kivitendo(dateformat => 'dd.mm.yyyy'))
+      })
+      ->tag('Format', $date_format)
+    })
+    ->tag('UTF8')
+    ->tag('DecimalSymbol', '.')
+    ->tag('DigitGroupingSymbol', '|')     # see CAVEATS in documentation
+    ->tag('VariableLength', sub { $self
+      ->tag('ColumnDelimiter', ',')       # see CAVEATS for missing RecordDelimiter
+      ->tag('TextEncapsulator', '"')
+      ->datev_columns
+      ->datev_foreign_keys
+    })
+  });
+}
+
+sub datev_columns {
+  my ($self, $table) = @_;
+
+  my %cols_by_primary_key = partition_by { $datev_column_defs{$_}{primary_key} } @datev_columns;
+  $::lxdebug->dump(0,  "cols", \%cols_by_primary_key);
+
+  for my $column (@{ $cols_by_primary_key{1} }) {
+    my $type = $column_types{ $datev_column_defs{$column}{type} };
+
+    die "unknown col type @{[ $column ]}" unless $type;
+
+    $self->tag('VariablePrimaryKey', sub { $self
+      ->tag('Name', $column);
+      $type->($self);
+    })
+  }
+
+  for my $column (@{ $cols_by_primary_key{''} }) {
+    my $type = $column_types{ $datev_column_defs{$column}{type} };
+
+    die "unknown col type @{[ ref $column]}" unless $type;
+
+    $self->tag('VariableColumn', sub { $self
+      ->tag('Name', $column);
+      $type->($self);
+    })
+  }
+
+  $self;
+}
+
+sub datev_foreign_keys {
+  my ($self) = @_;
+  # hard code weeee
+  $self->tag('ForeignKey', sub { $_[0]
+    ->tag('Name', 'customer_id')
+    ->tag('References', 'customer')
+  });
+  $self->tag('ForeignKey', sub { $_[0]
+    ->tag('Name', 'vendor_id')
+    ->tag('References', 'vendor')
+  });
+  $self->tag('ForeignKey', sub { $_[0]
+    ->tag('Name', $_)
+    ->tag('References', 'chart')
+  }) for qw(debit_accno credit_accno tax_accno);
+}
+
+sub do_datev_csv_export {
+  my ($self) = @_;
+
+  my $datev = SL::DATEV->new(from => $self->from, to => $self->to);
+
+  $datev->_get_transactions(from_to => $datev->fromto);
+
+  for my $transaction (@{ $datev->{DATEV} }) {
+    for my $entry (@{ $transaction }) {
+      $entry->{sortkey} = join '-', map { lc } (DateTime->from_kivitendo($entry->{transdate})->strftime('%Y%m%d'), $entry->{name}, $entry->{reference});
+    }
+  }
+
+  my @transactions = sort_by { $_->[0]->{sortkey} } @{ $datev->{DATEV} };
+
+  my $csv = Text::CSV_XS->new({
+    binary   => 1,
+    eol      => "\n",
+    sep_char => ";",
+  });
+
+  my ($fh, $filename) = File::Temp::tempfile();
+  binmode($fh, ':utf8');
+
+  $self->files->{"transactions.csv"} = $filename;
+  push @{ $self->tempfiles }, $filename;
+
+  for my $transaction (@transactions) {
+    my $is_payment     = any { $_->{link} =~ m{A[PR]_paid} } @{ $transaction };
+
+    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 $haben->{memo} || $soll->{memo};
+    $haben->{notes}  //= '';
+    $haben->{notes}    =  SL::HTML::Util->strip($haben->{notes});
+    $haben->{notes}    =~ s{\r}{}g;
+    $haben->{notes}    =~ s{\n+}{ }g;
+
+    my %row            = (
+      customer_id      => $soll->{customer_id} || $haben->{customer_id},
+      vendor_id        => $soll->{vendor_id} || $haben->{vendor_id},
+      amount           => abs($amount->{amount}),
+      debit_accno      => $soll->{accno},
+      debit_accname    => $soll->{accname},
+      credit_accno     => $haben->{accno},
+      credit_accname   => $haben->{accname},
+      tax              => abs($amount->{amount}) - abs($amount->{net_amount}),
+      notes            => $haben->{notes},
+      (map { ($_ => $tax->{$_})                    } qw(taxkey tax_accname tax_accno)),
+      (map { ($_ => ($haben->{$_} // $soll->{$_})) } qw(acc_trans_id invnumber name vcnumber transdate)),
+    );
+
+    $csv->print($fh, [ map { $row{$_} } @datev_columns ]);
+  }
+
+  # and build xml spec for it
+}
+
 sub do_csv_export {
   my ($self, $table) = @_;
 
@@ -382,6 +546,7 @@ sub all_tables {
 sub init_files { +{} }
 sub init_export_ids { +{} }
 sub init_tempfiles { [] }
+sub init_tables { [ grep { $known_tables{$_} } @export_table_order ] }
 
 sub API_VERSION {
   DateTime->new(year => 2002, month => 8, day => 14)->to_kivitendo;
index 5e70cbb..a1b9946 100755 (executable)
@@ -717,6 +717,7 @@ $self->{texts} = {
   'Credit'                      => 'Haben',
   'Credit (one letter abbreviation)' => 'H',
   'Credit Account'              => 'Habenkonto',
+  'Credit Account Name'         => 'Haben-Kontoname',
   'Credit Limit'                => 'Kreditlimit',
   'Credit Limit exceeded!!!'    => 'Kreditlimit überschritten!',
   'Credit Note'                 => 'Gutschrift',
@@ -832,6 +833,7 @@ $self->{texts} = {
   'Debit'                       => 'Soll',
   'Debit (one letter abbreviation)' => 'S',
   'Debit Account'               => 'Sollkonto',
+  'Debit Account Name'          => 'Soll-Kontoname',
   'Debit Starting Balance'      => 'EB Passiva',
   'Debit Tax'                   => 'Vorsteuer',
   'Debit Tax Account'           => 'Vorsteuerkonto',
@@ -1270,6 +1272,7 @@ $self->{texts} = {
   'Export date'                 => 'Exportdatum',
   'Export date from'            => 'Exportdatum von',
   'Export date to'              => 'Exportdatum bis',
+  'Export for tax accountant'   => 'Export für Steuerberater',
   'Extend automatically by n months' => 'Automatische Verlängerung um x Monate',
   'Extended'                    => 'Gesamt',
   'Extended status'             => 'Erweiterter Status',
@@ -2716,6 +2719,8 @@ $self->{texts} = {
   'Task server control'         => 'Task-Server-Steuerung',
   'Task server status'          => 'Task-Server-Status',
   'Tax'                         => 'Steuer',
+  'Tax Account'                 => 'Steuerkonto',
+  'Tax Account Name'            => 'Steuerkontoname',
   'Tax Consultant'              => 'Steuerberater/-in',
   'Tax ID number'               => 'UStID-Nummer',
   'Tax Included'                => 'Steuer im Preis inbegriffen',
index 83c39c7..2f3a855 100644 (file)
     <td>[% 'To Date' | $T8 %]</td>
     <td>[% L.date_tag('to', SELF.to) %]</td>
   </tr>
-  <tr>
-    <td>[% 'Include in Report' | $T8 %]</td>
-    <td>
-      [% L.checkbox_tag('tables.ar', label=LxERP.t8('Invoices'), checked=1) %]
-      [% L.checkbox_tag('tables.ap', label=LxERP.t8('Purchase Invoices'), checked=1) %]
-      [% L.checkbox_tag('tables.gl', label=LxERP.t8('GL Transactions'), checked=1) %]
-      [% L.checkbox_tag('tables.delivery_orders', label=LxERP.t8('Delivery Orders'), checked=1) %]
-      [% L.checkbox_tag('tables.oe', label=LxERP.t8('Quotations and orders'), checked=1) %]
-      [% L.checkbox_tag('tables.customer', label=LxERP.t8('Customers'), checked=1) %]
-      [% L.checkbox_tag('tables.vendor', label=LxERP.t8('Vendors'), checked=1) %]
-      [% L.checkbox_tag('tables.parts', label=LxERP.t8('Parts'), checked=1) %]
-      [% L.checkbox_tag('tables.acc_trans', label=LxERP.t8('Transactions'), checked=1) %]
-      [% L.checkbox_tag('tables.chart', label=LxERP.t8('Charts'), checked=1) %]
-    </td>
-  </tr>
 </table>
 
 [% L.hidden_tag('action', 'Gdpdu/dispatch') %]