CSS/JS: Git-Revision von HEAD als GET-Parameter verwenden
[kivitendo-erp.git] / SL / DATEV.pm
index d33692e..af188f7 100644 (file)
@@ -32,9 +32,12 @@ use strict;
 
 use SL::DBUtils;
 use SL::DATEV::KNEFile;
+use SL::DATEV::CSV;
 use SL::DB;
 use SL::HTML::Util ();
+use SL::Iconv;
 use SL::Locale::String qw(t8);
+use SL::VATIDNr;
 
 use Data::Dumper;
 use DateTime;
@@ -214,6 +217,26 @@ sub trans_id {
   return $self->{trans_id};
 }
 
+sub warnings {
+  my $self = shift;
+
+  if (@_) {
+    $self->{warnings} = [@_];
+  } else {
+   return $self->{warnings};
+  }
+}
+
+sub use_pk {
+ my $self = shift;
+
+ if (@_) {
+   $self->{use_pk} = $_[0];
+ }
+
+ return $self->{use_pk};
+}
+
 sub accnofrom {
  my $self = shift;
 
@@ -345,7 +368,73 @@ sub kne_export {
 }
 
 sub csv_export {
-  die 'not yet implemented';
+  my ($self) = @_;
+  my $result;
+
+  die 'no exporttype set!' unless $self->has_exporttype;
+
+  if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
+
+    $self->generate_datev_data(from_to => $self->fromto);
+    return if $self->errors;
+
+    my $datev_csv = SL::DATEV::CSV->new(
+      datev_lines  => $self->generate_datev_lines,
+      from         => $self->from,
+      to           => $self->to,
+      locked       => $self->locked,
+    );
+
+
+    my $filename = "EXTF_DATEV_kivitendo" . $self->from->ymd() . '-' . $self->to->ymd() . ".csv";
+
+    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();
+
+    # get encoding from defaults - use cp1252 if DATEV strict export is used
+    my $enc = ($::instance_conf->get_datev_export_format eq 'cp1252') ? 'cp1252' : 'utf-8';
+    my $csv_file = IO::File->new($self->export_path . '/' . $filename, ">:encoding($enc)") or die "Can't open: $!";
+
+    $csv->print($csv_file, $_) for @{ $datev_csv->header };
+    $csv->print($csv_file, $_) for @{ $datev_csv->lines  };
+    $csv_file->close;
+    $self->{warnings} = $datev_csv->warnings;
+
+    # convert utf-8 to cp1252//translit if set
+    if ($::instance_conf->get_datev_export_format eq 'cp1252-translit') {
+
+      my $filename_translit = "EXTF_DATEV_kivitendo_translit" . $self->from->ymd() . '-' . $self->to->ymd() . ".csv";
+      open my $fh_in,  '<:encoding(UTF-8)',  $self->export_path . '/' . $filename or die "could not open $filename for reading: $!";
+      open my $fh_out, '>', $self->export_path . '/' . $filename_translit         or die "could not open $filename_translit for writing: $!";
+
+      my $converter = SL::Iconv->new("utf-8", "cp1252//translit");
+
+      print $fh_out $converter->convert($_) while <$fh_in>;
+      close $fh_in;
+      close $fh_out;
+
+      unlink $self->export_path . '/' . $filename or warn "Could not unlink $filename: $!";
+      $filename = $filename_translit;
+    }
+
+    return { download_token => $self->download_token, filenames => $filename };
+
+  } 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 {
@@ -364,6 +453,15 @@ sub _sign {
   $_[0] <=> 0;
 }
 
+sub locked {
+ my $self = shift;
+
+ if (@_) {
+   $self->{locked} = $_[0];
+ }
+ return $self->{locked};
+}
+
 sub generate_datev_data {
   $main::lxdebug->enter_sub();
 
@@ -410,11 +508,18 @@ sub generate_datev_data {
 
   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 $ar_accno = "c.accno";
+  my $ap_accno = "c.accno";
+  if ( $self->use_pk ) {
+    $ar_accno = "CASE WHEN ac.chart_link = 'AR' THEN ct.customernumber ELSE c.accno END as accno";
+    $ap_accno = "CASE WHEN ac.chart_link = 'AP' THEN ct.vendornumber   ELSE c.accno END as accno";
+  }
+
   my $query    =
     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_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, t.taxdescription,
          'ar' as table,
@@ -443,7 +548,7 @@ sub generate_datev_data {
        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_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, t.taxdescription,
          'ap' as table,
@@ -470,7 +575,7 @@ sub generate_datev_data {
        UNION ALL
 
        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.reference AS invnumber, NULL AS duedate, ac.amount as umsatz, gl.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,
@@ -800,9 +905,6 @@ sub datetofour {
 
   my ($day, $month, $year) = split(/\./, $date);
 
-  if ($day =~ /^0/) {
-    $day = substr($day, 1, 1);
-  }
   if (length($month) < 2) {
     $month = "0" . $month;
   }
@@ -929,6 +1031,7 @@ sub generate_datev_lines {
 
     if ($trans_lines >= 2) {
 
+      # Personenkontenerweiterung: accno has already been replaced if use_pk was set
       $datev_data{'gegenkonto'} = $transaction->[$haben]->{'accno'};
       $datev_data{'konto'}      = $transaction->[$soll]->{'accno'};
       if ($transaction->[$haben]->{'invnumber'} ne "") {
@@ -943,13 +1046,27 @@ sub generate_datev_lines {
         $datev_data{buchungstext} = $transaction->[$haben]->{'name'};
       }
       if (($transaction->[$haben]->{'ustid'} // '') ne "") {
-        $datev_data{ustid} = $transaction->[$haben]->{'ustid'};
+        $datev_data{ustid} = SL::VATIDNr->normalize($transaction->[$haben]->{'ustid'});
       }
       if (($transaction->[$haben]->{'duedate'} // '') ne "") {
         $datev_data{belegfeld2} = $transaction->[$haben]->{'duedate'};
       }
-    }
 
+      # if deliverydate exists, add it to datev export if it is
+      # * an ar/ap booking that is not a payment
+      # * a gl booking
+      if (    ($transaction->[$haben]->{'deliverydate'} // '') ne ''
+           && (
+                (    $transaction->[$haben]->{'table'} =~ /^(ar|ap)$/
+                  && $transaction->[$haben]->{'link'}  !~ m/_paid/
+                  && $transaction->[$soll]->{'link'}   !~ m/_paid/
+                )
+                || $transaction->[$haben]->{'table'} eq 'gl'
+              )
+         ) {
+        $datev_data{leistungsdatum} = $transaction->[$haben]->{'deliverydate'};
+      }
+    }
     $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?
@@ -969,8 +1086,10 @@ sub generate_datev_lines {
       # $datev_data{buchungsschluessel} = !$datevautomatik ? $taxkey : "4";
       $datev_data{buchungsschluessel} = $taxkey;
     }
+    # set lock for each transaction
+    $datev_data{locked} = $self->locked;
 
-    push(@datev_lines, \%datev_data);
+    push(@datev_lines, \%datev_data) if $datev_data{umsatz};
   }
 
   # example of modifying export data:
@@ -1061,7 +1180,7 @@ sub kne_buchungsexport {
     $name =~ s/\ *$//;
     $kne_file->add_block("\x1E" . $name . "\x1C");
 
-    $kne_file->add_block("\xBA" . $kne->{'ustid'}    . "\x1C") if $kne->{'ustid'};
+    $kne_file->add_block("\xBA" . SL::VATIDNr->normalize($kne->{'ustid'}) . "\x1C") if $kne->{'ustid'};
 
     $kne_file->add_block("\xB3" . $kne->{'waehrung'} . "\x1C" . "\x79");
   };
@@ -1325,6 +1444,40 @@ sub csv_export_for_tax_accountant {
   return { download_token => $self->download_token, filenames => \@filenames };
 }
 
+sub check_vcnumbers_are_valid_pk_numbers {
+  my ($self) = @_;
+
+  # better use a class variable and set this in sub new (also needed in DATEV::CSV)
+  # calculation is also a bit more sane in sub check_valid_length_of_accounts
+  my $length_of_accounts = length(SL::DB::Manager::Chart->get_first(where => [charttype => 'A'])->accno) // 4;
+  my $pk_length = $length_of_accounts + 1;
+  my $query = <<"SQL";
+   SELECT customernumber AS vcnumber FROM customer WHERE customernumber !~ '^[[:digit:]]{$pk_length}\$'
+   UNION
+   SELECT vendornumber   AS vcnumber FROM vendor   WHERE vendornumber   !~ '^[[:digit:]]{$pk_length}\$'
+   LIMIT 1;
+SQL
+  my ($has_non_pk_accounts)  = selectrow_query($::form, SL::DB->client->dbh, $query);
+  return defined $has_non_pk_accounts ? 0 : 1;
+}
+
+
+sub check_valid_length_of_accounts {
+  my ($self) = @_;
+
+  my $query = <<"SQL";
+  SELECT DISTINCT char_length (accno) FROM chart WHERE charttype='A' AND id in (select chart_id from acc_trans);
+SQL
+
+  my $accno_length = selectall_hashref_query($::form, SL::DB->client->dbh, $query);
+  if (1 < scalar @$accno_length) {
+    $::form->error(t8("Invalid combination of ledger account number length." .
+                      " Mismatch length of #1 with length of #2. Please check your account settings. ",
+                      $accno_length->[0]->{char_length}, $accno_length->[1]->{char_length}));
+  }
+  return 1;
+}
+
 sub DESTROY {
   clean_temporary_directories();
 }
@@ -1496,7 +1649,7 @@ Forces a garbage collection on previous exports which will delete all exports th
 
 =item errors
 
-Returns a list of errors that occured. If no errors occured, the export was a success.
+Returns a list of errors that occurred. If no errors occurred, the export was a success.
 
 =item export
 
@@ -1522,6 +1675,28 @@ Example:
   #                  ]
   # };
 
+
+=item check_vcnumbers_are_valid_pk_numbers
+
+Returns 1 if all vcnumbers are suitable for the DATEV export, 0 if not.
+
+Finds the default length of charts (e.g. 4), adds 1 for the pk chart length
+(e.g. 5), and checks the database for any customers or vendors whose customer-
+or vendornumber doesn't consist of only numbers with exactly that length. E.g.
+for a chart length of four "10001" would be ok, but not "10001b" or "1000".
+
+All vcnumbers are checked, obsolete customers or vendors aren't exempt.
+
+There is also no check for the typical customer range 10000-69999 and the
+typical vendor range 70000-99999.
+
+=item check_valid_length_of_accounts
+
+Returns 1 if all currently booked accounts have only one common number length domain (e.g. 4 or 6).
+Will throw an error if more than one distinct size is detected.
+The error message gives a short hint with the value of the (at least)
+two mismatching number length domains.
+
 =back
 
 =head1 ATTRIBUTES
@@ -1570,6 +1745,11 @@ correctly.
 
 Set boundary account numbers for the export. Only useful for a stammdaten export.
 
+=item locked
+
+Boolean if the transactions are locked (read-only in kivitenod) or not.
+Default value is false
+
 =back
 
 =head1 CONSTANTS
@@ -1650,6 +1830,7 @@ OBE export is currently not implemented.
 =head1 SEE ALSO
 
 L<SL::DATEV::KNEFile>
+L<SL::DATEV::CSV>
 
 =head1 AUTHORS