+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 length 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} };
+}