Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 476d2c57

Von Jan Büren vor mehr als 3 Jahren hinzugefügt

  • ID 476d2c57a3d2a344009ae3a38fb0e5c84f91914d
  • Vorgänger 996eeac7
  • Nachfolger dfecc325

Redmine: #317 KNE-Export für DATEV entfernen

Unterschiede anzeigen:

SL/DATEV.pm
31 31
use strict;
32 32

  
33 33
use SL::DBUtils;
34
use SL::DATEV::KNEFile;
35 34
use SL::DATEV::CSV;
36 35
use SL::DB;
37 36
use SL::HTML::Util ();
......
288 287
  $::lxdebug->leave_sub;
289 288
}
290 289

  
291
sub _fill {
292
  $main::lxdebug->enter_sub();
293

  
294
  my $text      = shift // '';
295
  my $field_len = shift;
296
  my $fill_char = shift;
297
  my $alignment = shift || 'right';
298

  
299
  my $text_len  = length $text;
300

  
301
  if ($field_len < $text_len) {
302
    $text = substr $text, 0, $field_len;
303

  
304
  } elsif ($field_len > $text_len) {
305
    my $filler = ($fill_char) x ($field_len - $text_len);
306
    $text      = $alignment eq 'right' ? $filler . $text : $text . $filler;
307
  }
308

  
309
  $main::lxdebug->leave_sub();
310

  
311
  return $text;
312
}
313

  
314 290
sub get_datev_stamm {
315 291
  return $_[0]{stamm} ||= selectfirst_hashref_query($::form, $_[0]->dbh, 'SELECT * FROM datev');
316 292
}
......
331 307

  
332 308
sub export {
333 309
  my ($self) = @_;
334
  my $result;
335 310

  
336
  die 'no format set!' unless $self->has_format;
337

  
338
  if ($self->format == DATEV_FORMAT_CSV) {
339
    $result = $self->csv_export;
340
  } elsif ($self->format == DATEV_FORMAT_KNE) {
341
    $result = $self->kne_export;
342
  } elsif ($self->format == DATEV_FORMAT_OBE) {
343
    $result = $self->obe_export;
344
  } else {
345
    die 'unrecognized export format';
346
  }
347

  
348
  return $result;
349
}
350

  
351
sub kne_export {
352
  my ($self) = @_;
353
  my $result;
354

  
355
  die 'no exporttype set!' unless $self->has_exporttype;
356

  
357
  if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
358
    $result = $self->kne_buchungsexport;
359
  } elsif ($self->exporttype == DATEV_ET_STAMM) {
360
    $result = $self->kne_stammdatenexport;
361
  } elsif ($self->exporttype == DATEV_ET_CSV) {
362
    $result = $self->csv_export_for_tax_accountant;
363
  } else {
364
    die 'unrecognized exporttype';
365
  }
366

  
367
  return $result;
311
  return $self->csv_export;
368 312
}
369 313

  
370 314
sub csv_export {
......
423 367

  
424 368
    return { download_token => $self->download_token, filenames => $filename };
425 369

  
426
  } elsif ($self->exporttype == DATEV_ET_STAMM) {
427
    die 'will never be implemented';
428
    # 'Background: Export should only contain non
429
    #  DATEV-Charts and DATEV import will only
430
    #  import new Charts.'
431
  } elsif ($self->exporttype == DATEV_ET_CSV) {
432
    $result = $self->csv_export_for_tax_accountant;
433 370
  } else {
434 371
    die 'unrecognized exporttype';
435 372
  }
......
437 374
  return $result;
438 375
}
439 376

  
440
sub obe_export {
441
  die 'not yet implemented';
442
}
443

  
444 377
sub fromto {
445 378
  my ($self) = @_;
446 379

  
......
861 794
  $::lxdebug->leave_sub;
862 795
}
863 796

  
864
sub make_kne_data_header {
865
  $main::lxdebug->enter_sub();
866

  
867
  my ($self, $form) = @_;
868
  my ($primanota);
869

  
870
  my $stamm = $self->get_datev_stamm;
871

  
872
  my $jahr = $self->from ? $self->from->year : DateTime->today->year;
873

  
874
  #Header
875
  my $header  = "\x1D\x181";
876
  $header    .= _fill($stamm->{datentraegernr}, 3, ' ', 'left');
877
  $header    .= ($self->fromto) ? "11" : "13"; # Anwendungsnummer
878
  $header    .= _fill($stamm->{dfvkz}, 2, '0');
879
  $header    .= _fill($stamm->{beraternr}, 7, '0');
880
  $header    .= _fill($stamm->{mandantennr}, 5, '0');
881
  $header    .= _fill(($stamm->{abrechnungsnr} // '') . $jahr, 6, '0');
882

  
883
  $header .= $self->from ? $self->from->strftime('%d%m%y') : '';
884
  $header .= $self->to   ? $self->to->strftime('%d%m%y')   : '';
885

  
886
  if ($self->fromto) {
887
    $primanota = "001";
888
    $header .= $primanota;
889
  }
890

  
891
  $header .= _fill($stamm->{passwort}, 4, '0');
892
  $header .= " " x 16;       # Anwendungsinfo
893
  $header .= " " x 16;       # Inputinfo
894
  $header .= "\x79";
895

  
896
  #Versionssatz
897
  my $versionssatz  = $self->exporttype == DATEV_ET_BUCHUNGEN ? "\xB5" . "1," : "\xB6" . "1,";
898

  
899
  my $query         = qq|SELECT accno FROM chart LIMIT 1|;
900
  my $ref           = selectfirst_hashref_query($form, $self->dbh, $query);
901

  
902
  $versionssatz    .= length $ref->{accno};
903
  $versionssatz    .= ",";
904
  $versionssatz    .= length $ref->{accno};
905
  $versionssatz    .= ",SELF" . "\x1C\x79";
906

  
907
  $header          .= $versionssatz;
908

  
909
  $main::lxdebug->leave_sub();
910

  
911
  return $header;
912
}
913

  
914
sub datetofour {
915
  $main::lxdebug->enter_sub();
916

  
917
  my ($date, $six) = @_;
918

  
919
  my ($day, $month, $year) = split(/\./, $date);
920

  
921
  if (length($month) < 2) {
922
    $month = "0" . $month;
923
  }
924
  if (length($year) > 2) {
925
    $year = substr($year, -2, 2);
926
  }
927

  
928
  if ($six) {
929
    $date = $day . $month . $year;
930
  } else {
931
    $date = $day . $month;
932
  }
933

  
934
  $main::lxdebug->leave_sub();
935

  
936
  return $date;
937
}
938

  
939
sub trim_leading_zeroes {
940
  my $str = shift;
941

  
942
  $str =~ s/^0+//g;
943

  
944
  return $str;
945
}
946

  
947
sub make_ed_versionset {
948
  $main::lxdebug->enter_sub();
949

  
950
  my ($self, $header, $filename, $blockcount) = @_;
951

  
952
  my $versionset  = "V" . substr($filename, 2, 5);
953
  $versionset    .= substr($header, 6, 22);
954

  
955
  if ($self->fromto) {
956
    $versionset .= "0000" . substr($header, 28, 19);
957
  } else {
958
    my $datum = " " x 16;
959
    $versionset .= $datum . "001" . substr($header, 28, 4);
960
  }
961

  
962
  $versionset .= _fill($blockcount, 5, '0');
963
  $versionset .= "001";
964
  $versionset .= " 1";
965
  $versionset .= substr($header, -12, 10) . "    ";
966
  $versionset .= " " x 53;
967

  
968
  $main::lxdebug->leave_sub();
969

  
970
  return $versionset;
971
}
972

  
973
sub make_ev_header {
974
  $main::lxdebug->enter_sub();
975

  
976
  my ($self, $form, $fileno) = @_;
977

  
978
  my $stamm = $self->get_datev_stamm;
979

  
980
  my $ev_header  = _fill($stamm->{datentraegernr}, 3, ' ', 'left');
981
  $ev_header    .= "   ";
982
  $ev_header    .= _fill($stamm->{beraternr}, 7, ' ', 'left');
983
  $ev_header    .= _fill($stamm->{beratername}, 9, ' ', 'left');
984
  $ev_header    .= " ";
985
  $ev_header    .= (_fill($fileno, 5, '0')) x 2;
986
  $ev_header    .= " " x 95;
987

  
988
  $main::lxdebug->leave_sub();
989

  
990
  return $ev_header;
991
}
992

  
993 797
sub generate_datev_lines {
994 798
  my ($self) = @_;
995 799

  
......
1116 920
  return \@datev_lines;
1117 921
}
1118 922

  
1119

  
1120
sub kne_buchungsexport {
1121
  $main::lxdebug->enter_sub();
1122

  
1123
  my ($self) = @_;
1124

  
1125
  my $form = $::form;
1126

  
1127
  my @filenames;
1128

  
1129
  my $filename    = "ED00001";
1130
  my $evfile      = "EV01";
1131
  my @ed_versionset;
1132
  my $fileno      = 1;
1133
  my $ed_filename = $self->export_path . $filename;
1134

  
1135
  my $fromto = $self->fromto;
1136

  
1137
  $self->generate_datev_data(from_to => $self->fromto); # fetches data from db, transforms data and fills $self->{DATEV}
1138
  return if $self->errors;
1139

  
1140
  my @datev_lines = @{ $self->generate_datev_lines };
1141

  
1142

  
1143
  my $umsatzsumme = sum map { $_->{umsatz} } @datev_lines;
1144

  
1145
  # prepare kne file, everything gets stored in ED00001
1146
  my $header = $self->make_kne_data_header($form);
1147
  my $kne_file = SL::DATEV::KNEFile->new();
1148
  $kne_file->add_block($header);
1149

  
1150
  my $iconv   = $::locale->{iconv_utf8};
1151
  my %umlaute = ($iconv->convert('ä') => 'ae',
1152
                 $iconv->convert('ö') => 'oe',
1153
                 $iconv->convert('ü') => 'ue',
1154
                 $iconv->convert('Ä') => 'Ae',
1155
                 $iconv->convert('Ö') => 'Oe',
1156
                 $iconv->convert('Ü') => 'Ue',
1157
                 $iconv->convert('ß') => 'sz');
1158

  
1159
  # add the data from @datev_lines to the kne_file, formatting as needed
1160
  foreach my $kne ( @datev_lines ) {
1161
    $kne_file->add_block("+" . $kne_file->format_amount(abs($kne->{umsatz}), 0));
1162

  
1163
    # only add buchungsschluessel if it was previously defined
1164
    $kne_file->add_block("\x6C" . $kne->{buchungsschluessel}) if defined $kne->{buchungsschluessel};
1165

  
1166
    # ($kne->{gegenkonto}) = $kne->{gegenkonto} =~ /^(\d+)/;
1167
    $kne_file->add_block("a" . trim_leading_zeroes($kne->{gegenkonto}));
1168

  
1169
    if ( $kne->{belegfeld1} ) {
1170
      my $invnumber = $kne->{belegfeld1};
1171
      foreach my $umlaut (keys(%umlaute)) {
1172
        $invnumber =~ s/${umlaut}/${umlaute{$umlaut}}/g;
1173
      }
1174
      $invnumber =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
1175
      $invnumber =  substr($invnumber, 0, 12);
1176
      $invnumber =~ s/\ *$//;
1177
      $kne_file->add_block("\xBD" . $invnumber . "\x1C");
1178
    }
1179

  
1180
    $kne_file->add_block("\xBE" . &datetofour($kne->{belegfeld2},1) . "\x1C");
1181

  
1182
    $kne_file->add_block("d" . &datetofour($kne->{datum},0));
1183

  
1184
    # ($kne->{konto}) = $kne->{konto} =~ /^(\d+)/;
1185
    $kne_file->add_block("e" . trim_leading_zeroes($kne->{konto}));
1186

  
1187
    my $name = $kne->{buchungstext};
1188
    foreach my $umlaut (keys(%umlaute)) {
1189
      $name =~ s/${umlaut}/${umlaute{$umlaut}}/g;
1190
    }
1191
    $name =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
1192
    $name =  substr($name, 0, 30);
1193
    $name =~ s/\ *$//;
1194
    $kne_file->add_block("\x1E" . $name . "\x1C");
1195

  
1196
    $kne_file->add_block("\xBA" . SL::VATIDNr->normalize($kne->{'ustid'}) . "\x1C") if $kne->{'ustid'};
1197

  
1198
    $kne_file->add_block("\xB3" . $kne->{'waehrung'} . "\x1C" . "\x79");
1199
  };
1200

  
1201
  $umsatzsumme          = $kne_file->format_amount(abs($umsatzsumme), 0);
1202
  my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
1203

  
1204
  $kne_file->add_block($mandantenendsumme);
1205
  $kne_file->flush();
1206

  
1207
  open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
1208
  print(ED $kne_file->get_data());
1209
  close(ED);
1210

  
1211
  $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
1212

  
1213
  #Make EV Verwaltungsdatei
1214
  my $ev_header   = $self->make_ev_header($form, $fileno);
1215
  my $ev_filename = $self->export_path . $evfile;
1216
  push(@filenames, $evfile);
1217
  open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
1218
  print(EV $ev_header);
1219

  
1220
  foreach my $file (@ed_versionset) {
1221
    print(EV $file);
1222
  }
1223
  close(EV);
1224
  ###
1225

  
1226
  $self->add_filenames(@filenames);
1227

  
1228
  $main::lxdebug->leave_sub();
1229

  
1230
  return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
1231
}
1232

  
1233
sub kne_stammdatenexport {
1234
  $main::lxdebug->enter_sub();
1235

  
1236
  my ($self) = @_;
1237
  my $form = $::form;
1238

  
1239
  $self->get_datev_stamm->{abrechnungsnr} = "99";
1240

  
1241
  my @filenames;
1242

  
1243
  my $filename    = "ED00000";
1244
  my $evfile      = "EV01";
1245
  my @ed_versionset;
1246
  my $fileno          = 1;
1247
  my $i               = 0;
1248
  my $blockcount      = 1;
1249
  my $remaining_bytes = 256;
1250
  my $total_bytes     = 256;
1251
  my $buchungssatz    = "";
1252
  $filename++;
1253
  my $ed_filename = $self->export_path . $filename;
1254
  push(@filenames, $filename);
1255
  open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
1256
  my $header = $self->make_kne_data_header($form);
1257
  $remaining_bytes -= length($header);
1258

  
1259
  my $fuellzeichen;
1260

  
1261
  my (@where, @values) = ((), ());
1262
  if ($self->accnofrom) {
1263
    push @where, 'c.accno >= ?';
1264
    push @values, $self->accnofrom;
1265
  }
1266
  if ($self->accnoto) {
1267
    push @where, 'c.accno <= ?';
1268
    push @values, $self->accnoto;
1269
  }
1270

  
1271
  my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
1272

  
1273
  my $query     = qq|SELECT c.accno, c.description
1274
                     FROM chart c
1275
                     $where_str
1276
                     ORDER BY c.accno|;
1277

  
1278
  my $sth = $self->dbh->prepare($query);
1279
  $sth->execute(@values) || $form->dberror($query);
1280

  
1281
  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
1282
    if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
1283
      $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
1284
      $buchungssatz .= "\x00" x $fuellzeichen;
1285
      $blockcount++;
1286
      $total_bytes = ($blockcount) * 256;
1287
    }
1288
    $buchungssatz .= "t" . $ref->{'accno'};
1289
    $remaining_bytes = $total_bytes - length($buchungssatz . $header);
1290
    $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
1291
    $ref->{'description'} = substr($ref->{'description'}, 0, 40);
1292
    $ref->{'description'} =~ s/\ *$//;
1293

  
1294
    if (
1295
        ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
1296
        ) <= 6
1297
      ) {
1298
      $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
1299
      $buchungssatz .= "\x00" x $fuellzeichen;
1300
      $blockcount++;
1301
      $total_bytes = ($blockcount) * 256;
1302
    }
1303
    $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
1304
    $remaining_bytes = $total_bytes - length($buchungssatz . $header);
1305
  }
1306

  
1307
  $sth->finish;
1308
  print(ED $header);
1309
  print(ED $buchungssatz);
1310
  $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
1311
  my $dateiende = "\x00" x $fuellzeichen;
1312
  print(ED "z");
1313
  print(ED $dateiende);
1314
  close(ED);
1315

  
1316
  #Make EV Verwaltungsdatei
1317
  $ed_versionset[0] =
1318
    $self->make_ed_versionset($header, $filename, $blockcount);
1319

  
1320
  my $ev_header = $self->make_ev_header($form, $fileno);
1321
  my $ev_filename = $self->export_path . $evfile;
1322
  push(@filenames, $evfile);
1323
  open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
1324
  print(EV $ev_header);
1325

  
1326
  foreach my $file (@ed_versionset) {
1327
    print(EV $ed_versionset[$file]);
1328
  }
1329
  close(EV);
1330

  
1331
  $self->add_filenames(@filenames);
1332

  
1333
  $main::lxdebug->leave_sub();
1334

  
1335
  return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
1336
}
1337

  
1338
sub _format_accno {
1339
  my ($accno) = @_;
1340
  return $accno . ('0' x (6 - min(length($accno), 6)));
1341
}
1342

  
1343
sub csv_export_for_tax_accountant {
1344
  my ($self) = @_;
1345

  
1346
  $self->generate_datev_data(from_to => $self->fromto);
1347

  
1348
  foreach my $transaction (@{ $self->{DATEV} }) {
1349
    foreach my $entry (@{ $transaction }) {
1350
      $entry->{sortkey} = join '-', map { lc } (DateTime->from_kivitendo($entry->{transdate})->strftime('%Y%m%d'), $entry->{name}, $entry->{reference});
1351
    }
1352
  }
1353

  
1354
  my %transactions =
1355
    partition_by { $_->[0]->{table} }
1356
    sort_by      { $_->[0]->{sortkey} }
1357
    grep         { 2 == scalar(@{ $_ }) }
1358
    @{ $self->{DATEV} };
1359

  
1360
  my %column_defs = (
1361
    acc_trans_id      => { 'text' => $::locale->text('ID'), },
1362
    amount            => { 'text' => $::locale->text('Amount'), },
1363
    credit_accname    => { 'text' => $::locale->text('Credit Account Name'), },
1364
    credit_accno      => { 'text' => $::locale->text('Credit Account'), },
1365
    debit_accname     => { 'text' => $::locale->text('Debit Account Name'), },
1366
    debit_accno       => { 'text' => $::locale->text('Debit Account'), },
1367
    invnumber         => { 'text' => $::locale->text('Reference'), },
1368
    name              => { 'text' => $::locale->text('Name'), },
1369
    notes             => { 'text' => $::locale->text('Notes'), },
1370
    tax               => { 'text' => $::locale->text('Tax'), },
1371
    taxkey            => { 'text' => $::locale->text('Taxkey'), },
1372
    tax_accname       => { 'text' => $::locale->text('Tax Account Name'), },
1373
    tax_accno         => { 'text' => $::locale->text('Tax Account'), },
1374
    transdate         => { 'text' => $::locale->text('Transdate'), },
1375
    vcnumber          => { 'text' => $::locale->text('Customer/Vendor Number'), },
1376
  );
1377

  
1378
  my @columns = qw(
1379
    acc_trans_id name           vcnumber
1380
    transdate    invnumber      amount
1381
    debit_accno  debit_accname
1382
    credit_accno credit_accname
1383
    tax
1384
    tax_accno    tax_accname    taxkey
1385
    notes
1386
  );
1387

  
1388
  my %filenames_by_type = (
1389
    ar => $::locale->text('AR Transactions'),
1390
    ap => $::locale->text('AP Transactions'),
1391
    gl => $::locale->text('GL Transactions'),
1392
  );
1393

  
1394
  my @filenames;
1395
  foreach my $type (qw(ap ar)) {
1396
    my %csvs = (
1397
      invoices   => {
1398
        content  => '',
1399
        filename => sprintf('%s %s - %s.csv', $filenames_by_type{$type}, $self->from->to_kivitendo, $self->to->to_kivitendo),
1400
        csv      => Text::CSV_XS->new({
1401
          binary   => 1,
1402
          eol      => "\n",
1403
          sep_char => ";",
1404
        }),
1405
      },
1406
      payments   => {
1407
        content  => '',
1408
        filename => sprintf('Zahlungen %s %s - %s.csv', $filenames_by_type{$type}, $self->from->to_kivitendo, $self->to->to_kivitendo),
1409
        csv      => Text::CSV_XS->new({
1410
          binary   => 1,
1411
          eol      => "\n",
1412
          sep_char => ";",
1413
        }),
1414
      },
1415
    );
1416

  
1417
    foreach my $csv (values %csvs) {
1418
      $csv->{out} = IO::File->new($self->export_path . '/' . $csv->{filename}, '>:encoding(utf8)') ;
1419
      $csv->{csv}->print($csv->{out}, [ map { $column_defs{$_}->{text} } @columns ]);
1420

  
1421
      push @filenames, $csv->{filename};
1422
    }
1423

  
1424
    foreach my $transaction (@{ $transactions{$type} }) {
1425
      my $is_payment     = any { $_->{link} =~ m{A[PR]_paid} } @{ $transaction };
1426
      my $csv            = $is_payment ? $csvs{payments} : $csvs{invoices};
1427

  
1428
      my ($soll, $haben) = map { $transaction->[$_] } ($transaction->[0]->{amount} > 0 ? (1, 0) : (0, 1));
1429
      my $tax            = defined($soll->{tax_accno})  ? $soll : $haben;
1430
      my $amount         = defined($soll->{net_amount}) ? $soll : $haben;
1431
      $haben->{notes}    = ($haben->{memo} || $soll->{memo}) if $is_payment;
1432
      $haben->{notes}  //= '';
1433
      $haben->{notes}    =  SL::HTML::Util->strip($haben->{notes});
1434
      $haben->{notes}    =~ s{\r}{}g;
1435
      $haben->{notes}    =~ s{\n+}{ }g;
1436

  
1437
      my %row            = (
1438
        amount           => $::form->format_amount({ numberformat => '1000,00' }, abs($amount->{amount}), 2),
1439
        debit_accno      => _format_accno($soll->{accno}),
1440
        debit_accname    => $soll->{accname},
1441
        credit_accno     => _format_accno($haben->{accno}),
1442
        credit_accname   => $haben->{accname},
1443
        tax              => $::form->format_amount({ numberformat => '1000,00' }, abs($amount->{amount}) - abs($amount->{net_amount}), 2),
1444
        notes            => $haben->{notes},
1445
        (map { ($_ => $tax->{$_})                    } qw(taxkey tax_accname tax_accno)),
1446
        (map { ($_ => ($haben->{$_} // $soll->{$_})) } qw(acc_trans_id invnumber name vcnumber transdate)),
1447
      );
1448

  
1449
      $csv->{csv}->print($csv->{out}, [ map { $row{$_} } @columns ]);
1450
    }
1451

  
1452
    $_->{out}->close for values %csvs;
1453
  }
1454

  
1455
  $self->add_filenames(@filenames);
1456

  
1457
  return { download_token => $self->download_token, filenames => \@filenames };
1458
}
1459

  
1460 923
sub check_vcnumbers_are_valid_pk_numbers {
1461 924
  my ($self) = @_;
1462 925

  

Auch abrufbar als: Unified diff