S:DATEV:CSV: Längenbegrenzung bei buchungstext wieder rein
[kivitendo-erp.git] / SL / DATEV / CSV.pm
index 4a5fb9e..65f6cf0 100644 (file)
@@ -1,16 +1,20 @@
 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 Rose::Object::MakeMethods::Generic (
+  scalar => [ qw(datev_lines from to locked warnings) ],
+);
 
 my @kivitendo_to_datev = (
                             {
@@ -19,7 +23,7 @@ my @kivitendo_to_datev = (
                               max_length      => 13,
                               type            => 'Value',
                               required        => 1,
-                              input_check     => sub { my ($input) = @_; return (looks_like_number($input) && length($input) <= 13) },
+                              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 +57,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 +103,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 +125,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 +237,276 @@ 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);
+                                                       $ustid =~ s{\s+}{}g;
+                                                       return ($ustid =~ m/^CH|^[A-Z]{2}\w{5,13}$/);
+                                                     },
+                              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}$/);
                                                      },
                             }, # 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 => '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 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 +514,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 +548,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 +584,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 +605,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 +644,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 +741,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 +749,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<params{from}>,  C<params{to}>
-and C<params{first_day_of_fiscal_year}> have to be in YYYYDDMM date string
-format.
-Furthermore C<params{locked}> is a perlish boolean.
-
-
 =item kivitendo_to_datev
 
 Returns the data structure C<@datev_data> as an array
@@ -516,7 +759,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 +780,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);