Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 1d559eff

Von Jan Büren vor etwa 7 Jahren hinzugefügt

  • ID 1d559eff1e8a7efba3d21704d93b8cb62749de75
  • Vorgänger 6d831130
  • Nachfolger b55d5bb4

DATEV: csv_buchungsexport nach DATEV::CSV.pm ausgelagert

Testfälle angepasst. POD angepasst.
Details:

DATEV.pm
- Klassenvariable locked hinzugefügt.
- Aufruf der CSV-Klasse anstatt der internen Methode

CSV.pm
- Konstruktor wie in DATEV.pm ergänzt und um minimale
Pflichtfeldprüfung ergänzt.
- datetofour durch SL::Helper::DateTime ersetzt
- Helper _format_amount auch aufrufen
- Routinen umbenannt (pseudoprivat mit Unterstrich)
- Prüfung auf locked als perlish boolean
- _csv_buchungsexport um zweiten return array_ref mit warnungen ergänzt

t/datev/*
- Testfälle enstprechend dem neuen API-Call umgeschrieben
- Einen Testfall zur Überprüfung von keiner Warnung ergänzt

Unterschiede anzeigen:

SL/DATEV.pm
372 372
  die 'no exporttype set!' unless $self->has_exporttype;
373 373

  
374 374
  if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
375
    _csv_buchungsexport_to_file($self, data => $self->csv_buchungsexport);
375

  
376
  $self->generate_datev_data(from_to => $self->fromto);
377
  return if $self->errors;
378

  
379
  my $datev_ref = SL::DATEV::CSV->new(datev_lines  => $self->generate_datev_lines,
380
                                      from         => $self->from,
381
                                      to           => $self->to,
382
                                      locked       => $self->locked,
383
                                     );
384

  
385
  my $filename = "EXTF_DATEV_kivitendo" . $self->from->ymd() . '-' . $self->to->ymd() . ".csv";
386

  
387
  my $csv = Text::CSV_XS->new({
388
              binary       => 1,
389
              sep_char     => ";",
390
              always_quote => 1,
391
              eol          => "\r\n",
392
            }) or die "Cannot use CSV: ".Text::CSV_XS->error_diag();
393

  
394
  my $csv_file = IO::File->new($self->export_path . '/' . $filename, '>:encoding(cp1252)') or die "Can't open: $!";
395
  $csv->print($csv_file, $_) for @{ $datev_ref };
396
  $csv_file->close;
397

  
398
  return { download_token => $self->download_token, filenames => $filename };
376 399

  
377 400
  } elsif ($self->exporttype == DATEV_ET_STAMM) {
378 401
    die 'will never be implemented';
......
404 427
  $_[0] <=> 0;
405 428
}
406 429

  
430
sub locked {
431
 my $self = shift;
432

  
433
 if (@_) {
434
   $self->{locked} = $_[0];
435
 }
436
 return $self->{locked};
437
}
438

  
407 439
sub generate_datev_data {
408 440
  $main::lxdebug->enter_sub();
409 441

  
......
1370 1402
  return { download_token => $self->download_token, filenames => \@filenames };
1371 1403
}
1372 1404

  
1373
sub csv_buchungsexport {
1374
  my $self = shift;
1375
  my %params = @_;
1376

  
1377
  $self->generate_datev_data(from_to => $self->fromto);
1378
  return if $self->errors;
1379

  
1380
  my @datev_lines = @{ $self->generate_datev_lines };
1381

  
1382
  my @csv_columns = SL::DATEV::CSV->kivitendo_to_datev();
1383
  my @csv_headers = SL::DATEV::CSV->generate_csv_header(
1384
                      from                     => $self->from->ymd(''),
1385
                      to                       => $self->to->ymd(''),
1386
                      first_day_of_fiscal_year => $self->to->year . '0101',
1387
                      locked                   => 0
1388
                    );
1389

  
1390
  my @array_of_datev;
1391

  
1392
  # 2 Headers
1393
  push @array_of_datev, \@csv_headers;
1394
  push @array_of_datev, [ map { $_->{csv_header_name} } @csv_columns ];
1395

  
1396
  my @warnings;
1397
  foreach my $row ( @datev_lines ) {
1398
    my @current_datev_row;
1399

  
1400
    # shorten strings
1401
    if ($row->{belegfeld1}) {
1402
      $row->{buchungsbes} = $row->{belegfeld1} if $row->{belegfeld1};
1403
      $row->{belegfeld1}  = substr($row->{belegfeld1}, 0, 12);
1404
      $row->{buchungsbes} = substr($row->{buchungsbes}, 0, 60);
1405
    }
1406

  
1407
    $row->{datum}       = datetofour($row->{datum}, 0);
1408
    $row->{kost1}       = substr($row->{kost1}, 0, 8) if $row->{kost1};
1409
    $row->{kost2}       = substr($row->{kost2}, 0, 8) if $row->{kost2};
1410

  
1411
    # , as decimal point and trim for UstID
1412
    $row->{umsatz}      =~ s/\./,/;
1413
    $row->{ustid}       =~ s/\s//g if $row->{ustid}; # trim whitespace
1414

  
1415
    foreach my $column (@csv_columns) {
1416
      if (exists $column->{max_length} && $column->{kivi_datev_name} ne 'not yet implemented') {
1417
        # check max length
1418
        die "Incorrect length of field" if length($row->{ $column->{kivi_datev_name} }) > $column->{max_length};
1419
      }
1420
      if (exists $column->{valid_check} && $column->{kivi_datev_name} ne 'not yet implemented') {
1421
        # more checks, listed as user warnings
1422
        push @warnings, t8("Wrong field value '#1' for field '#2' for the transaction" .
1423
                            " with amount '#3'",$row->{ $column->{kivi_datev_name} },
1424
                            $column->{kivi_datev_name},$row->{umsatz})
1425
          unless ($column->{valid_check}->($row->{ $column->{kivi_datev_name} }));
1426
      }
1427
      push @current_datev_row, $row->{ $column->{kivi_datev_name} };
1428
    }
1429
    push @array_of_datev, \@current_datev_row;
1430
  }
1431
  $self->warnings(@warnings) if @warnings;
1432
  return \@array_of_datev;
1433
}
1434

  
1435
sub _csv_buchungsexport_to_file {
1436
  my $self   = shift;
1437
  my %params = @_;
1438

  
1439
  # we can definitely deny shorter data structures
1440
  croak ("Need at least 2 rows for header info") unless scalar @{ $params{data} } > 1;
1441

  
1442
  my $filename = "EXTF_DATEV_kivitendo" . $self->from->ymd() . '-' . $self->to->ymd() . ".csv";
1443
  my @data = \$params{data};
1444

  
1445
  my $csv = Text::CSV_XS->new({
1446
              binary       => 1,
1447
              sep_char     => ";",
1448
              always_quote => 1,
1449
              eol          => "\r\n",
1450
            }) or die "Cannot use CSV: ".Text::CSV_XS->error_diag();
1451

  
1452
  if ($csv->version >= 1.18) {
1453
    # get rid of stupid datev warnings in "Validity program"
1454
    $csv->quote_empty(1);
1455
  }
1456

  
1457
  my $csv_file = IO::File->new($self->export_path . '/' . $filename, '>:encoding(cp1252)') or die "Can't open: $!";
1458
  $csv->print($csv_file, $_) for @{ $params{data} };
1459
  $csv_file->close;
1460

  
1461
  return { download_token => $self->download_token, filenames => $params{filename} };
1462
}
1463

  
1464 1405
sub check_vcnumbers_are_valid_pk_numbers {
1465 1406
  my ($self) = @_;
1466 1407

  
......
1763 1704

  
1764 1705
Set boundary account numbers for the export. Only useful for a stammdaten export.
1765 1706

  
1707
=item locked
1708

  
1709
Boolean if the transactions are locked (read-only in kivitenod) or not.
1710
Default value is false
1711

  
1766 1712
=back
1767 1713

  
1768 1714
=head1 CONSTANTS
SL/DATEV/CSV.pm
4 4

  
5 5
use SL::Locale::String qw(t8);
6 6
use SL::DB::Datev;
7
use SL::Helper::DateTime;
7 8

  
8 9
use Carp;
9 10
use DateTime;
......
205 206
                            }, # pos 40
206 207
  );
207 208

  
209
sub new {
210
  my $class = shift;
211
  my %data  = @_;
212

  
213
  my $obj = bless {}, $class;
214

  
215
  croak(t8('We need a valid from date'))      unless (ref $data{from} eq 'DateTime');
216
  croak(t8('We need a valid to date'))        unless (ref $data{to}   eq 'DateTime');
217
  croak(t8('We need a array of datev_lines')) unless (ref $data{datev_lines} eq 'ARRAY');
218

  
219
  # TODO no params here, better class variables/values
220
  return _csv_buchungsexport(from        => $data{from},
221
                             to          => $data{to},
222
                             datev_lines => $data{datev_lines},
223
                             locked      => $data{locked},
224
                            );
225

  
226
  $obj;
227
}
228

  
208 229
sub check_encoding {
209 230
  my ($test) = @_;
210 231
  return undef unless $test;
......
216 237
  }
217 238
}
218 239

  
219
sub kivitendo_to_datev {
240
sub _kivitendo_to_datev {
220 241
  my ($self) = @_;
221 242

  
222 243
  my $entries = scalar (@kivitendo_to_datev);
......
224 245
  return @kivitendo_to_datev;
225 246
}
226 247

  
227
sub generate_csv_header {
228
  my ($self, %params)   = @_;
248
sub _generate_csv_header {
249
  my %params  = @_;
229 250

  
230 251
  # we need from and to in YYYYDDMM
231
  croak "Wrong format for from" unless $params{from} =~ m/^[0-9]{8}$/;
232
  croak "Wrong format for to"   unless $params{to} =~ m/^[0-9]{8}$/;
252
  croak "Wrong format for from $params{from}" unless $params{from} =~ m/^[0-9]{8}$/;
253
  croak "Wrong format for to $params{to}"   unless $params{to} =~ m/^[0-9]{8}$/;
233 254

  
234 255
  # who knows if we want locking and when our fiscal year starts
235
  croak "Wrong state of locking"      unless $params{locked} =~ m/(0|1)/;
256
  # croak "Wrong state of locking"      unless $params{locked} =~ m/^(0|1)$/;
257
  my $locked = defined($params{locked}) ? 1 : 0;
236 258
  croak "No startdate of fiscal year" unless $params{first_day_of_fiscal_year} =~ m/^[0-9]{8}$/;
237 259

  
238 260

  
......
260 282
    "EXTF", "300", 21, "Buchungsstapel", 7, $created_on, "", "ki",
261 283
    "kivitendo-datev", "", $meta_datev{beraternr}, $meta_datev{mandantennr},
262 284
    $params{first_day_of_fiscal_year}, $length_of_accounts,
263
    $params{from}, $params{to}, "", "", 1, "", $params{locked},
285
    $params{from}, $params{to}, "", "", 1, "", $locked,
264 286
    $default_curr, "", "", "",""
265 287
  );
266 288

  
267 289
  return @header;
268 290
}
269 291

  
292
sub _csv_buchungsexport {
293
  my %params = @_;
294

  
295
  my @csv_columns = _kivitendo_to_datev();
296
  my @csv_headers = _generate_csv_header(
297
                      from                     => $params{from}->ymd(''),
298
                      to                       => $params{to}->ymd(''),
299
                      first_day_of_fiscal_year => $params{to}->year . '0101',
300
                      locked                   => $params{locked}
301
                    );
302

  
303
  my @array_of_datev;
304

  
305
  # 2 Headers
306
  push @array_of_datev, \@csv_headers;
307
  push @array_of_datev, [ map { $_->{csv_header_name} } @csv_columns ];
308

  
309
  my @warnings;
310
  foreach my $row (@{ $params{datev_lines} }) {
311
    my @current_datev_row;
312

  
313
    # shorten strings
314
    if ($row->{belegfeld1}) {
315
      $row->{buchungsbes} = $row->{belegfeld1} if $row->{belegfeld1};
316
      $row->{belegfeld1}  = substr($row->{belegfeld1}, 0, 12);
317
      $row->{buchungsbes} = substr($row->{buchungsbes}, 0, 60);
318
    }
319

  
320
    $row->{datum} = DateTime->from_kivitendo($row->{datum})->strftime('%d%m');
321

  
322
    $row->{kost1}       = substr($row->{kost1}, 0, 8) if $row->{kost1};
323
    $row->{kost2}       = substr($row->{kost2}, 0, 8) if $row->{kost2};
324

  
325
    # , as decimal point and trim for UstID
326
    $row->{umsatz}      = _format_amount($row->{umsatz});
327
    $row->{ustid}       =~ s/\s//g if $row->{ustid}; # trim whitespace
328

  
329
    foreach my $column (@csv_columns) {
330
      if (exists $column->{max_length} && $column->{kivi_datev_name} ne 'not yet implemented') {
331
        # check max length
332
        die "Incorrect length of field" if length($row->{ $column->{kivi_datev_name} }) > $column->{max_length};
333
      }
334
      if (exists $column->{valid_check} && $column->{kivi_datev_name} ne 'not yet implemented') {
335
        # more checks, listed as user warnings
336
        push @warnings, t8("Wrong field value '#1' for field '#2' for the transaction" .
337
                            " with amount '#3'",$row->{ $column->{kivi_datev_name} },
338
                            $column->{kivi_datev_name},$row->{umsatz})
339
          unless ($column->{valid_check}->($row->{ $column->{kivi_datev_name} }));
340
      }
341
      push @current_datev_row, $row->{ $column->{kivi_datev_name} };
342
    }
343
    push @array_of_datev, \@current_datev_row;
344
  }
345
  return (\@array_of_datev, \@warnings);
346
}
347

  
270 348
sub _format_amount {
271 349
  $::form->format_amount({ numberformat => '1000,00' }, @_);
272 350
}
......
283 361

  
284 362
=head1 SYNOPSIS
285 363

  
364
  use SL::DATEV qw(:CONSTANTS);
365
  use SL::DATEV::CSV;
366

  
367
  my $startdate = DateTime->new(year => 2014, month => 9, day => 1);
368
  my $enddate   = DateTime->new(year => 2014, month => 9, day => 31);
369
  my $datev = SL::DATEV->new(
370
    exporttype => DATEV_ET_BUCHUNGEN,
371
    format     => DATEV_FORMAT_CSV,
372
    from       => $startdate,
373
    to         => $enddate,
374
  );
375
  $datev->generate_datev_data;
376

  
377
  my $datev_ref = SL::DATEV::CSV->new(datev_lines  => $datev->generate_datev_lines,
378
                                      from         => $datev->from,
379
                                      to           => $datev->to,
380
                                      locked       => $datev->locked,
381
                                     );
382

  
383
=head1 DESCRIPTION
384

  
286 385
The parsing of the DATEV CSV is index based, therefore the correct
287 386
column must be present at the corresponding index, i.e.:
288 387
 Index 2
......
348 447

  
349 448
=over 4
350 449

  
450
=item new PARAMS
451

  
452
Constructor for CSV-DATEV export.
453
Checks mandantory params as described in section synopsis.
454

  
351 455
=item check_encoding
352 456

  
353 457
Helper function, returns true if a string is not empty and cp1252 encoded
......
366 470
C<params{from}>,  C<params{to}>
367 471
and C<params{first_day_of_fiscal_year}> have to be in YYYYDDMM date string
368 472
format.
369
Furthermore C<params{locked}> needs to be a boolean in number format (0|1).
473
Furthermore C<params{locked}> is a perlish boolean.
370 474

  
371 475

  
372 476
=item kivitendo_to_datev
......
379 483
Expects a number in kivitendo database format and returns the same number
380 484
in DATEV format.
381 485

  
486
=item _csv_buchungsexport
487

  
488
Generates the CSV-Format data for the CSV DATEV export and returns
489
an 2-dimensional array as an array_ref.
490
May additionally return a second array_ref with warnings.
491

  
492
Requires the same date fields as the constructor for a valid DATEV header.
493

  
494
Furthermore we assume that the first day of the fiscal year is
495
the first of January and we cannot guarantee that our data in kivitendo
496
is locked, that means a booking cannot be modified after a defined (vat tax)
497
period.
498
Some validity checks (max_length and regex) will be done if the
499
data structure contains them and the field is defined.
500

  
501
To add or alter the structure of the data take a look at the C<@kivitendo_to_datev> structure.
502

  
503

  
382 504
=back
t/datev/datev_format_2018.t
68 68
# check conversion to csv
69 69
$datev1->from($startdate);
70 70
$datev1->to($enddate);
71
$datev1->csv_buchungsexport();
72
my @warnings = $datev1->warnings;
71
my ($datev_ref, $warnings_ref) = SL::DATEV::CSV->new(datev_lines  => $datev1->generate_datev_lines,
72
                                                     from         => $startdate,
73
                                                     to           => $enddate,
74
                                                     locked       => $datev1->locked,
75
                                                    );
76
my @warnings = $warnings_ref;
73 77
is($warnings[0]->[0]->{untranslated},
74 78
  'Wrong field value \'#1\' for field \'#2\' for the transaction with amount \'#3\'', 'wrong_encoding');
75 79

  
......
87 91
$datev3->to($enddate);
88 92
$datev3->generate_datev_data;
89 93
$datev3->generate_datev_lines;
90
$datev3->csv_buchungsexport;
94
my ($datev_ref2, $warnings_ref2) = SL::DATEV::CSV->new(datev_lines  => $datev3->generate_datev_lines,
95
                                                       from         => $startdate,
96
                                                       to           => $enddate,
97
                                                       locked       => $datev3->locked,
98
                                                      );
99

  
100

  
101

  
91 102
@warnings = [];
92
@warnings = $datev3->warnings;
103
@warnings = $warnings_ref2;
93 104
is($warnings[0]->[0]->{untranslated},
94 105
  'Wrong field value \'#1\' for field \'#2\' for the transaction with amount \'#3\'', 'mixed_wrong_encoding');
95 106

  
......
157 168
$datev2->generate_datev_data;
158 169
$datev2->generate_datev_lines;
159 170

  
160
my @data_csv = splice @{ $datev2->csv_buchungsexport() }, 2, 5;
161
@data_csv    = sort { $a->[0] <=> $b->[0] } @data_csv;
171
my ($datev_ref3, $warnings_ref3) = SL::DATEV::CSV->new(datev_lines  => $datev2->generate_datev_lines,
172
                                                       from         => $startdate,
173
                                                       to           => $enddate,
174
                                                       locked       => $datev2->locked,
175
                                                      );
162 176

  
177
my @data_csv = splice @{ $datev_ref3 }, 2, 5;
178
@data_csv    = sort { $a->[0] cmp $b->[0] } @data_csv;
163 179

  
164 180
my $cp1252_posting_text   = SL::Iconv::convert("UTF-8", "CP1252", 'Reisekosten März 2018');
165 181
cmp_bag($data_csv[0], [ 100, 'H', 'EUR', undef, undef, undef, '4660', '1000', 9, '1703', 'Reisekosten ',
t/datev/invoices.t
91 91
                                         },
92 92
                                         {
93 93
                                           'belegfeld1'   => "\x{de} sales \x{a5}& inv\x{f6}ice",
94
                                           'buchungstext' => 'Testcustomer',
94

  
95

  
96
'buchungstext' => 'Testcustomer',
95 97
                                           'buchungstext' => 'Testcustomer',
96 98
                                           'datum'        => '05.01.2017',
97 99
                                           'gegenkonto'   => '1400',
......
152 154
# check conversion to csv
153 155
$datev1->from($startdate);
154 156
$datev1->to($enddate);
155
$datev1->use_pk(0); # reset use_pk for csv_buchungsexport
157
# reset use_pk for csv_buchungsexport
158
$datev1->use_pk(0);
159
$datev1->generate_datev_data;
160

  
161

  
162
my ($datev_ref, $w_ref) = SL::DATEV::CSV->new(datev_lines  => $datev1->generate_datev_lines,
163
                                              from         => $startdate,
164
                                              to           => $enddate,
165
                                              locked       => $datev1->locked,
166
                                   );
167
# warnings should be undef -> no array elements at all
168
is(scalar @{ $w_ref }, 0);
156 169

  
157 170
# splice away the header, because sort won't do
158 171
# we need sort, because pay_invoice is not acc_trans_id order safe
159
my @data_csv = splice @{ $datev1->csv_buchungsexport() }, 2, 5;
172
my @data_csv = splice @{ $datev_ref }, 2, 5;
160 173
@data_csv    = sort { $a->[0] cmp $b->[0] } @data_csv;
161 174

  
162 175
my $cp1252_belegfeld1   = SL::Iconv::convert("UTF-8", "CP1252", 'Þ sales ¥& i');

Auch abrufbar als: Unified diff