Projekt

Allgemein

Profil

Herunterladen (22,7 KB) Statistiken
| Zweig: | Markierung: | Revision:
ef5b4b39 Jan Büren
package SL::DATEV::CSV;

use strict;
use Carp;
cf8942d3 Sven Schöling
use DateTime;
f2b3e089 Jan Büren
use Encode qw(encode);
8bf25ad5 Jan Büren
use Scalar::Util qw(looks_like_number);
ef5b4b39 Jan Büren
cf8942d3 Sven Schöling
use SL::DB::Datev;
use SL::DB::Chart;
use SL::Helper::DateTime;
use SL::Locale::String qw(t8);
cdd2f4c8 Bernd Bleßmann
use SL::Util qw(trim);
cf8942d3 Sven Schöling
7130c4c1 Jan Büren
use Rose::Object::MakeMethods::Generic (
scalar => [ qw(datev_lines from to locked warnings) ],
);
ef5b4b39 Jan Büren
my @kivitendo_to_datev = (
{
kivi_datev_name => 'umsatz',
csv_header_name => t8('Transaction Value'),
max_length => 13,
type => 'Value',
8bf25ad5 Jan Büren
required => 1,
28550377 Moritz Bunkus
input_check => sub { my ($input) = @_; return (looks_like_number($input) && length($input) <= 13 && $input > 0) },
51d553bd Jan Büren
formatter => \&_format_amount,
850cb6b4 Jan Büren
valid_check => sub { my ($check) = @_; return ($check =~ m/^\d{1,10}(\,\d{1,2})?$/) },
ef5b4b39 Jan Büren
},
{
kivi_datev_name => 'soll_haben_kennzeichen',
csv_header_name => t8('Debit/Credit Label'),
max_length => 1,
type => 'Text',
8bf25ad5 Jan Büren
required => 1,
default => 'S',
input_check => sub { my ($check) = @_; return ($check =~ m/^(S|H)$/) },
formatter => sub { my ($input) = @_; return $input eq 'H' ? 'H' : 'S' },
850cb6b4 Jan Büren
valid_check => sub { my ($check) = @_; return ($check =~ m/^(S|H)$/) },
ef5b4b39 Jan Büren
},
{
kivi_datev_name => 'waehrung',
csv_header_name => t8('Transaction Value Currency Code'),
max_length => 3,
type => 'Text',
8bf25ad5 Jan Büren
default => '',
input_check => sub { my ($check) = @_; return ($check eq '' || $check =~ m/^[A-Z]{3}$/) },
850cb6b4 Jan Büren
valid_check => sub { my ($check) = @_; return ($check =~ m/^[A-Z]{3}$/) },
ef5b4b39 Jan Büren
},
{
kivi_datev_name => 'wechselkurs',
csv_header_name => t8('Exchange Rate'),
max_length => 11,
type => 'Number',
8bf25ad5 Jan Büren
default => '',
850cb6b4 Jan Büren
valid_check => sub { my ($check) = @_; return ($check =~ m/^[0-9]*\.?[0-9]*$/) },
ef5b4b39 Jan Büren
},
{
kivi_datev_name => 'not yet implemented',
a9bbf9c6 Jan Büren
csv_header_name => t8('Base Transaction Value'),
ef5b4b39 Jan Büren
},
{
kivi_datev_name => 'not yet implemented',
csv_header_name => t8('Base Transaction Value Currency Code'),
},
{
kivi_datev_name => 'konto',
csv_header_name => t8('Account'),
8bf25ad5 Jan Büren
max_length => 9,
ef5b4b39 Jan Büren
type => 'Account',
8bf25ad5 Jan Büren
required => 1,
input_check => sub { my ($check) = @_; return ($check =~ m/^[0-9]{4,9}$/) },
ef5b4b39 Jan Büren
},
{
kivi_datev_name => 'gegenkonto',
csv_header_name => t8('Contra Account'),
8bf25ad5 Jan Büren
max_length => 9,
ef5b4b39 Jan Büren
type => 'Account',
8bf25ad5 Jan Büren
required => 1,
input_check => sub { my ($check) = @_; return ($check =~ m/^[0-9]{4,9}$/) },
ef5b4b39 Jan Büren
},
{
kivi_datev_name => 'buchungsschluessel',
csv_header_name => t8('Posting Key'),
max_length => 2,
type => 'Text',
8bf25ad5 Jan Büren
default => '',
input_check => sub { my ($check) = @_; return ($check =~ m/^[0-9]{0,2}$/) },
ef5b4b39 Jan Büren
},
{
kivi_datev_name => 'datum',
csv_header_name => t8('Invoice Date'),
max_length => 4,
type => 'Date',
8bf25ad5 Jan Büren
required => 1,
input_check => sub { my ($check) = @_; return (ref (DateTime->from_kivitendo($check)) eq 'DateTime') },
formatter => sub { my ($input) = @_; return DateTime->from_kivitendo($input)->strftime('%d%m') },
850cb6b4 Jan Büren
valid_check => sub { my ($check) = @_; return ($check =~ m/^[0-9]{4}$/) },
ef5b4b39 Jan Büren
},
{
kivi_datev_name => 'belegfeld1',
csv_header_name => t8('Invoice Field 1'),
max_length => 12,
type => 'Text',
8bf25ad5 Jan Büren
default => '',
input_check => sub { my ($text) = @_; check_encoding($text); },
formatter => sub { my ($input) = @_; return substr($input, 0, 12) },
ef5b4b39 Jan Büren
},
{
0ac8d01c Jan Büren
kivi_datev_name => 'belegfeld2',
ef5b4b39 Jan Büren
csv_header_name => t8('Invoice Field 2'),
8bf25ad5 Jan Büren
max_length => 12,
ef5b4b39 Jan Büren
type => 'Text',
8bf25ad5 Jan Büren
default => '',
0ac8d01c Jan Büren
input_check => sub { my ($check) = @_; return 1 unless $check; return (ref (DateTime->from_kivitendo($check)) eq 'DateTime') },
da836211 Jan Büren
formatter => sub { my ($input) = @_; return undef unless $input; return trim(DateTime->from_kivitendo($input)->strftime('%e%m%y')) },
valid_check => sub { my ($check) = @_; return 1 unless $check; return ($check =~ m/^[0-9]{5,6}$/) },
ef5b4b39 Jan Büren
},
{
kivi_datev_name => 'not yet implemented',
csv_header_name => t8('Discount'),
type => 'Value',
},
{
0d082a48 Sven Schöling
kivi_datev_name => 'buchungstext',
ef5b4b39 Jan Büren
csv_header_name => t8('Posting Text'),
max_length => 60,
type => 'Text',
8bf25ad5 Jan Büren
default => '',
input_check => sub { my ($text) = @_; return 1 unless $text; check_encoding($text); },
formatter => sub { my ($input) = @_; return substr($input, 0, 60) },
ef5b4b39 Jan Büren
}, # pos 14
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
csv_header_name => t8('Link to invoice'),
max_length => 210, # DMS Application shortcut and GUID
# Example: "BEDI"
# "8DB85C02-4CC3-FF3E-06D7-7F87EEECCF3A".
}, # pos 20
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'not yet implemented',
},
{
kivi_datev_name => 'kost1',
csv_header_name => t8('Cost Center'),
max_length => 8,
type => 'Text',
8bf25ad5 Jan Büren
default => '',
input_check => sub { my ($text) = @_; return 1 unless $text; check_encoding($text); },
formatter => sub { my ($input) = @_; return substr($input, 0, 8) },
ef5b4b39 Jan Büren
}, # pos 37
{
kivi_datev_name => 'kost2',
csv_header_name => t8('Cost Center'),
max_length => 8,
type => 'Text',
8bf25ad5 Jan Büren
default => '',
input_check => sub { my ($text) = @_; return 1 unless $text; check_encoding($text); },
formatter => sub { my ($input) = @_; return substr($input, 0, 8) },
ef5b4b39 Jan Büren
}, # pos 38
{
kivi_datev_name => 'not yet implemented',
csv_header_name => t8('KOST Quantity'),
max_length => 9,
type => 'Number',
850cb6b4 Jan Büren
valid_check => sub { my ($check) = @_; return ($check =~ m/^[0-9]{0,9}$/) },
ef5b4b39 Jan Büren
}, # pos 39
{
kivi_datev_name => 'ustid',
csv_header_name => t8('EU Member State and VAT ID Number'),
max_length => 15,
type => 'Text',
8bf25ad5 Jan Büren
default => '',
4be32a52 Moritz Bunkus
input_check => sub {
my ($ustid) = @_;
return 1 if ('' eq $ustid);
3800c37e Moritz Bunkus
$ustid =~ s{\s+}{}g;
4be32a52 Moritz Bunkus
return ($ustid =~ m/^CH|^[A-Z]{2}\w{5,13}$/);
},
3800c37e Moritz Bunkus
formatter => sub { my ($input) = @_; $input =~ s/\s//g; return $input },
ef5b4b39 Jan Büren
valid_check => sub {
850cb6b4 Jan Büren
my ($ustid) = @_;
8bf25ad5 Jan Büren
return 1 if ('' eq $ustid);
ef5b4b39 Jan Büren
return ($ustid =~ m/^CH|^[A-Z]{2}\w{5,13}$/);
},
}, # pos 40
);

1d559eff Jan Büren
sub new {
my $class = shift;
my %data = @_;

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

7130c4c1 Jan Büren
my $obj = bless {}, $class;
$obj->$_($data{$_}) for keys %data;
1d559eff Jan Büren
$obj;
}

ef5b4b39 Jan Büren
sub check_encoding {
2f2a0f41 Jan Büren
my ($test) = @_;
ef5b4b39 Jan Büren
return undef unless $test;
if (eval {
f2b3e089 Jan Büren
encode('Windows-1252', $test, Encode::FB_CROAK|Encode::LEAVE_SRC);
ef5b4b39 Jan Büren
1
}) {
return 1;
}
}

1d559eff Jan Büren
sub _kivitendo_to_datev {
cf5fa654 Sven Schöling
@kivitendo_to_datev, ({ kivi_datev_name => 'not yet implemented' }) x (116 - @kivitendo_to_datev);
ef5b4b39 Jan Büren
}

7130c4c1 Jan Büren
sub header {
my ($self) = @_;
ef5b4b39 Jan Büren
7130c4c1 Jan Büren
my @header;
ef5b4b39 Jan Büren
# we can safely set these defaults
991d36cb Sven Schöling
# TODO get length_of_accounts from DATEV.pm
my $today = DateTime->now_local;
ef5b4b39 Jan Büren
my $created_on = $today->ymd('') . $today->hms('') . '000';
my $length_of_accounts = length(SL::DB::Manager::Chart->get_first(where => [charttype => 'A'])->accno) // 4;
my $default_curr = SL::DB::Default->get_default_currency;

2deb38c5 Geoffrey Richardson
# datev metadata and the string length limits
ef5b4b39 Jan Büren
my %meta_datev;
my %meta_datev_to_valid_length = (
beraternr => 7,
beratername => 25,
mandantennr => 5,
);

my $datev = SL::DB::Manager::Datev->get_first();

while (my ($k, $v) = each %meta_datev_to_valid_length) {
de4257ba Jan Büren
next unless $datev->{$k};
ef5b4b39 Jan Büren
$meta_datev{$k} = substr $datev->{$k}, 0, $v;
}

7130c4c1 Jan Büren
my @header_row_1 = (
153719df Jan Büren
"EXTF", "510", 21, "Buchungsstapel", 7, $created_on, "", "ki",
ef5b4b39 Jan Büren
"kivitendo-datev", "", $meta_datev{beraternr}, $meta_datev{mandantennr},
7130c4c1 Jan Büren
$self->first_day_of_fiscal_year->ymd(''), $length_of_accounts,
$self->from->ymd(''), $self->to->ymd(''), "", "", 1, "", $self->locked,
ef5b4b39 Jan Büren
$default_curr, "", "", "",""
);
7130c4c1 Jan Büren
push @header, [ @header_row_1 ];

# second header row, just the column names
push @header, [ map { $_->{csv_header_name} } _kivitendo_to_datev() ];
ef5b4b39 Jan Büren
7130c4c1 Jan Büren
return \@header;
ef5b4b39 Jan Büren
}
850cb6b4 Jan Büren
7130c4c1 Jan Büren
sub lines {
my ($self) = @_;
1d559eff Jan Büren
7130c4c1 Jan Büren
my (@array_of_datev, @warnings);
1d559eff Jan Büren
my @csv_columns = _kivitendo_to_datev();

7130c4c1 Jan Büren
foreach my $row (@{ $self->datev_lines }) {
1d559eff Jan Büren
my @current_datev_row;

8bf25ad5 Jan Büren
# 1. check all datev_lines and see if we have a defined value
# 2. if we don't have a defined value set a default if exists
# 3. otherwise die
1d559eff Jan Büren
foreach my $column (@csv_columns) {
8bf25ad5 Jan Büren
if ($column->{kivi_datev_name} eq 'not yet implemented') {
push @current_datev_row, '';
next;
}
my $data = $row->{$column->{kivi_datev_name}};
if (!defined $data) {
if (defined $column->{default}) {
$data = $column->{default};
} else {
7bee3eca Jan Büren
die 'No sensible value or a sensible default found for the entry: ' . $column->{kivi_datev_name};
8bf25ad5 Jan Büren
}
}
# checkpoint a: no undefined data. All strict checks now!
7130c4c1 Jan Büren
if (exists $column->{input_check} && !$column->{input_check}->($data)) {
8bf25ad5 Jan Büren
die t8("Wrong field value '#1' for field '#2' for the transaction with amount '#3'",
7130c4c1 Jan Büren
$data, $column->{kivi_datev_name}, $row->{umsatz});
8bf25ad5 Jan Büren
}
# checkpoint b: we can safely format the input
if ($column->{formatter}) {
$data = $column->{formatter}->($data);
1d559eff Jan Büren
}
8bf25ad5 Jan Büren
# checkpoint c: all soft checks now, will pop up as a user warning
if (exists $column->{valid_check} && !$column->{valid_check}->($data)) {
1d559eff Jan Büren
push @warnings, t8("Wrong field value '#1' for field '#2' for the transaction" .
8bf25ad5 Jan Büren
" with amount '#3'", $data, $column->{kivi_datev_name}, $row->{umsatz});
1d559eff Jan Büren
}
8bf25ad5 Jan Büren
push @current_datev_row, $data;
1d559eff Jan Büren
}
push @array_of_datev, \@current_datev_row;
}
7130c4c1 Jan Büren
$self->warnings(\@warnings);
return \@array_of_datev;
1d559eff Jan Büren
}

7130c4c1 Jan Büren
# helper

850cb6b4 Jan Büren
sub _format_amount {
$::form->format_amount({ numberformat => '1000,00' }, @_);
}

7130c4c1 Jan Büren
sub first_day_of_fiscal_year {
$_[0]->to->clone->truncate(to => 'year');
}

ef5b4b39 Jan Büren
1;

__END__

=encoding utf-8

=head1 NAME

SL::DATEV::CSV - kivitendo DATEV CSV Specification

=head1 SYNOPSIS

1d559eff Jan Büren
use SL::DATEV qw(:CONSTANTS);
use SL::DATEV::CSV;

my $startdate = DateTime->new(year => 2014, month => 9, day => 1);
my $enddate = DateTime->new(year => 2014, month => 9, day => 31);
my $datev = SL::DATEV->new(
exporttype => DATEV_ET_BUCHUNGEN,
format => DATEV_FORMAT_CSV,
from => $startdate,
to => $enddate,
);
$datev->generate_datev_data;

7130c4c1 Jan Büren
my $datev_csv = SL::DATEV::CSV->new(datev_lines => $datev->generate_datev_lines,
1d559eff Jan Büren
from => $datev->from,
to => $datev->to,
locked => $datev->locked,
);
7130c4c1 Jan Büren
$datev_csv->header; # returns the required 2 rows of header ($aref = [ ["row1" ..], [ "row2" .. ] ]) as array of array
$datev_csv->lines; # returns an array_ref of rows of array_refs soll uns die ein Arrayref von Zeilen zurückgeben, die jeweils Arrayrefs sind
$datev_csv->warnings; # returns warnings


# The above object methods can be directly chained to a CSV export function, like this:
my $csv_file = IO::File->new($somewhere_in_filesystem)') 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;



1d559eff Jan Büren
=head1 DESCRIPTION

ef5b4b39 Jan Büren
The parsing of the DATEV CSV is index based, therefore the correct
column must be present at the corresponding index, i.e.:
Index 2
Field Name : Debit/Credit Label
Valid Values : 'S' or 'H'
Length: : 1

The columns in C<@kivi_datev> are in the correct order and the
specific attributes are defined as a key value hash list for each entry.

The key names are the english translation according to the DATEV specs
(Leitfaden DATEV englisch).

The two attributes C<max_length> and C<type> are also set as specified
by the DATEV specs.

To link the structure to kivitendo data, each entry has the attribute C<kivi_datev_name>
which is by convention the key name as generated by DATEV->generate_datev_data.
A value of C<'not yet implemented'> indicates that this field has no
corresponding kivitendo data and will be given an empty value by DATEV->csv_buchungsexport.


=head1 SPECIFICATION

This is an excerpt of the DATEV Format 2015 Specification for CSV-Header
and CSV-Data lines.

=head2 FILENAME

The filename is subject to the following restrictions:
1. The filename must begin with the prefix DTVF_ or EXTF_.
2. The filename must end with .csv.

When exporting from or importing into DATEV applications, the filename is
marked with the prefix "DTVF_" (DATEV Format).
The prefix "DTVF_" is reserved for DATEV applications.
If you are using a third-party application to create a file in the DATEV format
that you want to import using batch processing, use the prefix "EXTF_"
(External Format).

=head2 File Structure

The file structure of the text file exported/imported is defined as follows

Line 1: Header (serves to assist in the interpretation of the following data)

Line 2: Headline (headline of the user data)

Line 3 – n: Records (user data)

For an valid example file take a look at doc/DATEV-2015/EXTF_Buchungsstapel.csv


=head2 Detailed Description

Line 1 must contain 11 fields.

Line 2 must contain 26 fields.

Line 3 - n: must contain 116 fields, a smaller subset is mandatory.

=head1 FUNCTIONS

=over 4

1d559eff Jan Büren
=item new PARAMS

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

ef5b4b39 Jan Büren
=item check_encoding

Helper function, returns true if a string is not empty and cp1252 encoded
2f2a0f41 Jan Büren
For example some arabic utf-8 like ݐ will return false
ef5b4b39 Jan Büren
7130c4c1 Jan Büren
=item header
ef5b4b39 Jan Büren
Mostly all other header information are constants or metadata loaded
from SL::DB::Datev.pm.

Returns the first two entries for the header (see above: File Structure)
as an array.

=item kivitendo_to_datev

Returns the data structure C<@datev_data> as an array

850cb6b4 Jan Büren
=item _format_amount

Lightweight wrapper for form->format_amount.
Expects a number in kivitendo database format and returns the same number
in DATEV format.

7130c4c1 Jan Büren
=item first_day_of_fiscal_year

Takes a look at $self->to to determine the first day of the fiscal year.

=item lines
1d559eff Jan Büren
Generates the CSV-Format data for the CSV DATEV export and returns
an 2-dimensional array as an array_ref.
May additionally return a second array_ref with warnings.

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

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

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

ef5b4b39 Jan Büren
=back
7130c4c1 Jan Büren
=head1 TODO CAVEAT

cf5fa654 Sven Schöling
One can circumevent the check of the warnings.quite easily,
7130c4c1 Jan Büren
becaus warnings are generated after the call to lines:

# WRONG usage
die if @{ $datev_csv->warnings };
somethin_with($datev_csv->lines);

# safe usage
my $lines = $datev_csv->lines;
die if @{ $datev_csv->warnings };
somethin_with($lines);