X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FDATEV%2FCSV.pm;h=8b8e8859822facac10eb86eded3cf49e5368bd3c;hb=f3324b5ad66924333bf2a313974f6d4d21932707;hp=45bd5f6fc2fe4f7b6f023122b58707103944198c;hpb=de4257ba28f1d6deba825657aa30d97241fbe3c4;p=kivitendo-erp.git diff --git a/SL/DATEV/CSV.pm b/SL/DATEV/CSV.pm index 45bd5f6fc..8b8e88598 100644 --- a/SL/DATEV/CSV.pm +++ b/SL/DATEV/CSV.pm @@ -1,16 +1,21 @@ package SL::DATEV::CSV; use strict; - -use SL::Locale::String qw(t8); -use SL::DB::Datev; -use SL::Helper::DateTime; - use Carp; use DateTime; -use Encode qw(decode); +use Encode qw(encode); use Scalar::Util qw(looks_like_number); +use SL::DB::Datev; +use SL::DB::Chart; +use SL::Helper::DateTime; +use SL::Locale::String qw(t8); +use SL::Util qw(trim); +use SL::VATIDNr; + +use Rose::Object::MakeMethods::Generic ( + scalar => [ qw(datev_lines from to locked warnings) ], +); my @kivitendo_to_datev = ( { @@ -19,8 +24,8 @@ my @kivitendo_to_datev = ( max_length => 13, type => 'Value', required => 1, - input_check => sub { my ($input) = @_; return (looks_like_number($input) && length($input) <= 13) }, - formatter => sub { my ($input) = @_; return _format_amount($input) }, + input_check => sub { my ($input) = @_; return (looks_like_number($input) && length($input) <= 13 && $input > 0) }, + formatter => \&_format_amount, valid_check => sub { my ($check) = @_; return ($check =~ m/^\d{1,10}(\,\d{1,2})?$/) }, }, { @@ -53,7 +58,7 @@ my @kivitendo_to_datev = ( }, { kivi_datev_name => 'not yet implemented', - sv_header_name => t8('Base Transaction Value'), + csv_header_name => t8('Base Transaction Value'), }, { kivi_datev_name => 'not yet implemented', @@ -99,16 +104,21 @@ my @kivitendo_to_datev = ( max_length => 12, type => 'Text', default => '', - input_check => sub { my ($text) = @_; check_encoding($text); }, + input_check => sub { return 1 unless $::instance_conf->get_datev_export_format eq 'cp1252'; + my ($text) = @_; check_encoding($text); }, + valid_check => sub { return 1 if $::instance_conf->get_datev_export_format eq 'cp1252'; + my ($text) = @_; check_encoding($text); }, formatter => sub { my ($input) = @_; return substr($input, 0, 12) }, }, { - kivi_datev_name => 'not yet implemented', + kivi_datev_name => 'belegfeld2', csv_header_name => t8('Invoice Field 2'), max_length => 12, type => 'Text', default => '', - valid_check => sub { my ($check) = @_; return ($check =~ m/[ -~]{1,12}/) }, + input_check => sub { my ($check) = @_; return 1 unless $check; return (ref (DateTime->from_kivitendo($check)) eq 'DateTime') }, + formatter => sub { my ($input) = @_; return '' 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}$/) }, }, { kivi_datev_name => 'not yet implemented', @@ -116,12 +126,15 @@ my @kivitendo_to_datev = ( type => 'Value', }, { - kivi_datev_name => 'buchungsbes', + kivi_datev_name => 'buchungstext', csv_header_name => t8('Posting Text'), max_length => 60, type => 'Text', default => '', - input_check => sub { my ($text) = @_; return 1 unless $text; check_encoding($text); }, + input_check => sub { return 1 unless $::instance_conf->get_datev_export_format eq 'cp1252'; + my ($text) = @_; check_encoding($text); }, + valid_check => sub { return 1 if $::instance_conf->get_datev_export_format eq 'cp1252'; + my ($text) = @_; check_encoding($text); }, formatter => sub { my ($input) = @_; return substr($input, 0, 60) }, }, # pos 14 { @@ -225,33 +238,282 @@ my @kivitendo_to_datev = ( max_length => 15, type => 'Text', default => '', - input_check => sub { my ($check) = @_; return ($check eq '' || $check =~ m/[A-Z]{2}\w{5,13}/) }, - formatter => sub { my ($input) = @_; return ($input =~ s/\s//g) }, + input_check => sub { + my ($ustid) = @_; + return 1 if ('' eq $ustid); + return SL::VATIDNr->validate($ustid); + }, + formatter => sub { my ($input) = @_; $input =~ s/\s//g; return $input }, valid_check => sub { my ($ustid) = @_; return 1 if ('' eq $ustid); - return ($ustid =~ m/^CH|^[A-Z]{2}\w{5,13}$/); + return SL::VATIDNr->validate($ustid); }, }, # pos 40 + { + 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', + }, # pos 50 + { + 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', + }, # pos 60 + { + 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', + }, # pos 70 + { + 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', + }, # pos 80 + { + 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', + }, # pos 90 + { + 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', + }, # pos 100 + { + 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', + }, # pos 110 + { + kivi_datev_name => 'not yet implemented', + }, + { + kivi_datev_name => 'not yet implemented', + }, + { + kivi_datev_name => 'not yet implemented', + }, + { + kivi_datev_name => 'locked', + csv_header_name => t8('Lock'), + max_length => 1, + type => 'Number', + default => 1, + valid_check => sub { my ($check) = @_; return ($check =~ m/^(0|1)$/) }, + }, # pos 114 + { + kivi_datev_name => 'leistungsdatum', + csv_header_name => t8('Payment Date'), + max_length => 8, + type => 'Date', + default => '', + input_check => sub { my ($check) = @_; return 1 if ('' eq $check); return (ref (DateTime->from_kivitendo($check)) eq 'DateTime') }, + formatter => sub { my ($input) = @_; return '' if ('' eq $input); return DateTime->from_kivitendo($input)->strftime('%d%m%Y') }, + valid_check => sub { my ($check) = @_; return 1 if ('' eq $check); return ($check =~ m/^[0-9]{8}$/) }, + }, # pos 115 + { + 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', + }, # pos 120 ); sub new { my $class = shift; my %data = @_; - my $obj = bless {}, $class; - 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'); - # TODO no params here, better class variables/values - return _csv_buchungsexport(from => $data{from}, - to => $data{to}, - datev_lines => $data{datev_lines}, - locked => $data{locked}, - ); - + my $obj = bless {}, $class; + $obj->$_($data{$_}) for keys %data; $obj; } @@ -259,36 +521,21 @@ sub check_encoding { my ($test) = @_; return undef unless $test; if (eval { - decode('Windows-1252', $test, Encode::FB_CROAK|Encode::LEAVE_SRC); + encode('Windows-1252', $test, Encode::FB_CROAK|Encode::LEAVE_SRC); 1 }) { return 1; } } -sub _kivitendo_to_datev { +sub header { my ($self) = @_; - my $entries = scalar (@kivitendo_to_datev); - push @kivitendo_to_datev, { kivi_datev_name => 'not yet implemented' } for 1 .. (116 - $entries); - return @kivitendo_to_datev; -} - -sub _generate_csv_header { - my %params = @_; - - # we need from and to in YYYYDDMM - croak "Wrong format for from $params{from}" unless $params{from} =~ m/^[0-9]{8}$/; - croak "Wrong format for to $params{to}" unless $params{to} =~ m/^[0-9]{8}$/; - - # who knows if we want locking and when our fiscal year starts - # croak "Wrong state of locking" unless $params{locked} =~ m/^(0|1)$/; - my $locked = defined($params{locked}) ? 1 : 0; - croak "No startdate of fiscal year" unless $params{first_day_of_fiscal_year} =~ m/^[0-9]{8}$/; - + my @header; # we can safely set these defaults - my $today = DateTime->now(time_zone => "local"); + # TODO get length_of_accounts from DATEV.pm + my $today = DateTime->now_local; 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; @@ -308,42 +555,33 @@ sub _generate_csv_header { $meta_datev{$k} = substr $datev->{$k}, 0, $v; } - my @header = ( - "EXTF", "300", 21, "Buchungsstapel", 7, $created_on, "", "ki", + my @header_row_1 = ( + "EXTF", "510", 21, "Buchungsstapel", 7, $created_on, "", "ki", "kivitendo-datev", "", $meta_datev{beraternr}, $meta_datev{mandantennr}, - $params{first_day_of_fiscal_year}, $length_of_accounts, - $params{from}, $params{to}, "", "", 1, "", $locked, + $self->first_day_of_fiscal_year->ymd(''), $length_of_accounts, + $self->from->ymd(''), $self->to->ymd(''), "", "", 1, "", $self->locked, $default_curr, "", "", "","" ); + push @header, [ @header_row_1 ]; - return @header; -} - -sub _csv_buchungsexport { - my %params = @_; + # second header row, just the column names + push @header, [ map { $_->{csv_header_name} } @kivitendo_to_datev ]; - my @csv_columns = _kivitendo_to_datev(); - my @csv_headers = _generate_csv_header( - from => $params{from}->ymd(''), - to => $params{to}->ymd(''), - first_day_of_fiscal_year => $params{to}->year . '0101', - locked => $params{locked} - ); + return \@header; +} - my @array_of_datev; +sub lines { + my ($self) = @_; - # 2 Headers - push @array_of_datev, \@csv_headers; - push @array_of_datev, [ map { $_->{csv_header_name} } @csv_columns ]; + my (@array_of_datev, @warnings); - my @warnings; - foreach my $row (@{ $params{datev_lines} }) { + foreach my $row (@{ $self->datev_lines }) { my @current_datev_row; # 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 - foreach my $column (@csv_columns) { + foreach my $column (@kivitendo_to_datev) { if ($column->{kivi_datev_name} eq 'not yet implemented') { push @current_datev_row, ''; next; @@ -353,14 +591,13 @@ sub _csv_buchungsexport { if (defined $column->{default}) { $data = $column->{default}; } else { - die 'No sensible value or a sensible default found for the entry: ' . $column->{kivi_datev_name}; + die 'No sensible value or a sensible default found for the entry: ' . $column->{kivi_datev_name}; } } # checkpoint a: no undefined data. All strict checks now! - if (exists $column->{input_check}) { + if (exists $column->{input_check} && !$column->{input_check}->($data)) { die t8("Wrong field value '#1' for field '#2' for the transaction with amount '#3'", - $data, $column->{kivi_datev_name}, $row->{umsatz}) - unless $column->{input_check}->($data); + $data, $column->{kivi_datev_name}, $row->{umsatz}); } # checkpoint b: we can safely format the input if ($column->{formatter}) { @@ -375,13 +612,20 @@ sub _csv_buchungsexport { } push @array_of_datev, \@current_datev_row; } - return (\@array_of_datev, \@warnings); + $self->warnings(\@warnings); + return \@array_of_datev; } +# helper + sub _format_amount { $::form->format_amount({ numberformat => '1000,00' }, @_); } +sub first_day_of_fiscal_year { + $_[0]->to->clone->truncate(to => 'year'); +} + 1; __END__ @@ -407,11 +651,25 @@ SL::DATEV::CSV - kivitendo DATEV CSV Specification ); $datev->generate_datev_data; - my $datev_ref = SL::DATEV::CSV->new(datev_lines => $datev->generate_datev_lines, + my $datev_csv = SL::DATEV::CSV->new(datev_lines => $datev->generate_datev_lines, from => $datev->from, to => $datev->to, locked => $datev->locked, ); + $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; + + + =head1 DESCRIPTION @@ -490,8 +748,7 @@ Checks mandantory params as described in section synopsis. Helper function, returns true if a string is not empty and cp1252 encoded For example some arabic utf-8 like ݐ will return false -=item generate_csv_header(from => 'YYYYDDMM', to => 'YYYYDDMM', locked => 0, - first_day_of_fiscal_year => 'YYYYDDMM') +=item header Mostly all other header information are constants or metadata loaded from SL::DB::Datev.pm. @@ -499,13 +756,6 @@ from SL::DB::Datev.pm. Returns the first two entries for the header (see above: File Structure) as an array. -All params are mandatory: -C, C -and C have to be in YYYYDDMM date string -format. -Furthermore C is a perlish boolean. - - =item kivitendo_to_datev Returns the data structure C<@datev_data> as an array @@ -516,7 +766,11 @@ Lightweight wrapper for form->format_amount. Expects a number in kivitendo database format and returns the same number in DATEV format. -=item _csv_buchungsexport +=item first_day_of_fiscal_year + +Takes a look at $self->to to determine the first day of the fiscal year. + +=item lines Generates the CSV-Format data for the CSV DATEV export and returns an 2-dimensional array as an array_ref. @@ -533,5 +787,18 @@ 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. - =back + +=head1 TODO CAVEAT + +One can circumevent the check of the warnings.quite easily, +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);