]> wagnertech.de Git - mfinanz.git/blobdiff - SL/BackgroundJob/ConvertTimeRecordings.pm
Zeiterfassung: Konvertierung: POD-Update und Kosmetik
[mfinanz.git] / SL / BackgroundJob / ConvertTimeRecordings.pm
index f39ac1ab0adc50892772cb53513476d105acf682..75c09ad342efe5292b8d842ab1cae5a7c7f80bf5 100644 (file)
@@ -13,27 +13,14 @@ use SL::Locale::String qw(t8);
 
 use DateTime;
 use List::Util qw(any);
-use Try::Tiny;
 
 sub create_job {
   $_[0]->create_standard_job('7 3 1 * *'); # every first day of month at 03:07
 }
 use Rose::Object::MakeMethods::Generic (
- 'scalar'                => [ qw(data) ],
- 'scalar --get_set_init' => [ qw(rounding link_order) ],
+ 'scalar'                => [ qw(params) ],
 );
 
-# valid parameters -> better as class members with rose generic set/get
-my %valid_params = (
-              from_date => '',
-              to_date   => '',
-              customernumbers => '',
-              part_id => '',
-              rounding => 1,
-              link_order => 0,
-              project_id => '',
-             );
-
 #
 # If job does not throw an error,
 # success in background_job_histories is 'success'.
@@ -44,52 +31,24 @@ my %valid_params = (
 sub run {
   my ($self, $db_obj) = @_;
 
-  $self->data($db_obj->data_as_hash) if $db_obj;
+  $self->initialize_params($db_obj->data_as_hash) if $db_obj;
 
   $self->{$_} = [] for qw(job_errors);
 
-  # check user input param names
-  foreach my $param (keys %{ $self->data }) {
-    die "Not a valid parameter: $param" unless exists $valid_params{$param};
-  }
-
-  # TODO check user input param values - (defaults are assigned later)
-  # 1- If there are any customer numbers check if they refer to valid customers
-  #    otherwise croak and do nothing
-  # 2 .. n Same applies for other params if used at all (rounding -> 0|1  link_order -> 0|1)
-
-  # from/to date from data. Defaults to begining and end of last month.
-  # TODO get/set see above
-  my $from_date;
-  my $to_date;
-  # handle errors with a catch handler
-  try {
-    $from_date   = DateTime->from_kivitendo($self->data->{from_date}) if $self->data->{from_date};
-    $to_date     = DateTime->from_kivitendo($self->data->{to_date})   if $self->data->{to_date};
-  } catch {
-    die "Cannot convert date from string $self->data->{from_date} $self->data->{to_date}\n Details :\n $_"; # not $@
-  };
-  $from_date ||= DateTime->new( day => 1,    month => DateTime->today_local->month, year => DateTime->today_local->year)->subtract(months => 1);
-  $to_date   ||= DateTime->last_day_of_month(month => DateTime->today_local->month, year => DateTime->today_local->year)->subtract(months => 1);
-
-  $to_date->add(days => 1); # to get all from the to_date, because of the time part (15.12.2020 23.59 > 15.12.2020)
-
   my %customer_where;
-  %customer_where = ('customer.customernumber' => $self->data->{customernumbers}) if 'ARRAY' eq ref $self->data->{customernumbers};
+  %customer_where = ('customer_id' => $self->params->{customer_ids}) if scalar @{ $self->params->{customer_ids} };
 
-  my $time_recordings = SL::DB::Manager::TimeRecording->get_all(where        => [date => { ge_lt => [ $from_date, $to_date ]},
-                                                                                 or   => [booked => 0, booked => undef],
-                                                                                 '!duration' => 0,
-                                                                                 '!duration' => undef,
-                                                                                 %customer_where],
-                                                                with_objects => ['customer']);
+  my $time_recordings = SL::DB::Manager::TimeRecording->get_all(where => [date        => { ge_lt => [ $self->params->{from_date}, $self->params->{to_date} ]},
+                                                                          or          => [booked => 0, booked => undef],
+                                                                          '!duration' => 0,
+                                                                          '!duration' => undef,
+                                                                          %customer_where]);
 
-  # no time recordings at all ? -> better exit here before iterating a empty hash
-  # return undef or message unless ref $time_recordings->[0] eq SL::DB::Manager::TimeRecording;
+  return t8('No time recordings to convert') if scalar @$time_recordings == 0;
 
   my @donumbers;
 
-  if ($self->data->{link_order}) {
+  if ($self->params->{link_order}) {
     my %time_recordings_by_order_id;
     my %orders_by_order_id;
     foreach my $tr (@$time_recordings) {
@@ -120,25 +79,88 @@ sub run {
   return $msg;
 }
 
-# inits
+# helper
+sub initialize_params {
+  my ($self, $data) = @_;
 
-sub init_rounding {
-  1
-}
+  # valid parameters with default values
+  my %valid_params = (
+    from_date       => DateTime->new( day => 1,    month => DateTime->today_local->month, year => DateTime->today_local->year)->subtract(months => 1)->to_kivitendo,
+    to_date         => DateTime->last_day_of_month(month => DateTime->today_local->month, year => DateTime->today_local->year)->subtract(months => 1)->to_kivitendo,
+    customernumbers => [],
+    part_id         => undef,
+    project_id      => undef,
+    rounding        => 1,
+    link_order      => 0,
+  );
+
+
+  # check user input param names
+  foreach my $param (keys %$data) {
+    die "Not a valid parameter: $param" unless exists $valid_params{$param};
+  }
+
+  # set defaults
+  $self->params(
+    { map { ($_ => $data->{$_} // $valid_params{$_}) } keys %valid_params }
+  );
+
+
+  # convert date from string to object
+  my $from_date;
+  my $to_date;
+  $from_date = DateTime->from_kivitendo($self->params->{from_date});
+  $to_date   = DateTime->from_kivitendo($self->params->{to_date});
+  # DateTime->from_kivitendo returns undef if the string cannot be parsed. Therefore test the result.
+  die 'Cannot convert date from string "' . $self->params->{from_date} . '"' if !$from_date;
+  die 'Cannot convert date to string "'   . $self->params->{to_date}   . '"' if !$to_date;
+
+  $to_date->add(days => 1); # to get all from the to_date, because of the time part (15.12.2020 23.59 > 15.12.2020)
+
+  $self->params->{from_date} = $from_date;
+  $self->params->{to_date}   = $to_date;
+
+
+  # check if customernumbers are valid
+  die 'Customer numbers must be given in an array' if 'ARRAY' ne ref $self->params->{customernumbers};
+
+  my $customers = [];
+  if (scalar @{ $self->params->{customernumbers} }) {
+    $customers = SL::DB::Manager::Customer->get_all(where => [ customernumber => $self->params->{customernumbers},
+                                                               or             => [obsolete => undef, obsolete => 0] ]);
+  }
+  die 'Not all customer numbers are valid' if scalar @$customers != scalar @{ $self->params->{customernumbers} };
 
-sub init_link_order {
-  0
+  # return customer ids
+  $self->params->{customer_ids} = [ map { $_->id } @$customers ];
+
+
+  # check part
+  if ($self->params->{part_id} && !SL::DB::Manager::Part->find_by(id => $self->params->{part_id},
+                                                                  or => [obsolete => undef, obsolete => 0])) {
+    die 'No valid part found by given part id';
+  }
+
+
+  # check project
+  if ($self->params->{project_id} && !SL::DB::Manager::Project->find_by(id => $self->params->{project_id},
+                                                                        active => 1, valid => 1)) {
+    die 'No valid project found by given project id';
+  }
+
+  return $self->params;
 }
 
-# helper
 sub convert_without_linking {
   my ($self, $time_recordings) = @_;
 
   my %time_recordings_by_customer_id;
   push @{ $time_recordings_by_customer_id{$_->customer_id} }, $_ for @$time_recordings;
 
-  my %convert_params = map { $_ => $self->data->{$_} } qw(rounding link_order project_id);
-  $convert_params{default_part_id} = $self->data->{part_id};
+  my %convert_params = (
+    rounding        => $self->params->{rounding},
+    default_part_id => $self->params->{part_id},
+  );
 
   my @donumbers;
   foreach my $customer_id (keys %time_recordings_by_customer_id) {
@@ -147,10 +169,7 @@ sub convert_without_linking {
       $do = SL::DB::DeliveryOrder->new_from_time_recordings($time_recordings_by_customer_id{$customer_id}, %convert_params);
       1;
     }) {
-      $::lxdebug->message(LXDebug->WARN(),
-                          "ConvertTimeRecordings: creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}});
-      push @{ $self->{job_errors} }, "ConvertTimeRecordings: creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}};
-
+      $self->log_error("creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}});
     }
 
     if ($do) {
@@ -159,9 +178,7 @@ sub convert_without_linking {
         $_->update_attributes(booked => 1) for @{$time_recordings_by_customer_id{$customer_id}};
         1;
       })) {
-        $::lxdebug->message(LXDebug->WARN(),
-                            "ConvertTimeRecordings: saving delivery order failed for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}});
-        push @{ $self->{job_errors} }, "ConvertTimeRecordings: saving delivery order failed for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}};
+        $self->log_error('saving delivery order failed for time recording ids ' . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}});
       } else {
         push @donumbers, $do->donumber;
       }
@@ -174,8 +191,10 @@ sub convert_without_linking {
 sub convert_with_linking {
   my ($self, $time_recordings_by_order_id, $orders_by_order_id) = @_;
 
-  my %convert_params = map { $_ => $self->data->{$_} } qw(rounding link_order project_id);
-  $convert_params{default_part_id} = $self->data->{part_id};
+  my %convert_params = (
+    rounding        => $self->params->{rounding},
+    default_part_id => $self->params->{part_id},
+  );
 
   my @donumbers;
   foreach my $related_order_id (keys %$time_recordings_by_order_id) {
@@ -185,9 +204,7 @@ sub convert_with_linking {
       $do = SL::DB::DeliveryOrder->new_from_time_recordings($time_recordings_by_order_id->{$related_order_id}, related_order => $related_order, %convert_params);
       1;
     }) {
-      $::lxdebug->message(LXDebug->WARN(),
-                          "ConvertTimeRecordings: creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}});
-      push @{ $self->{job_errors} }, "ConvertTimeRecordings: creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}};
+      $self->log_error("creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}});
     }
 
     if ($do) {
@@ -221,9 +238,8 @@ sub convert_with_linking {
 
         1;
       })) {
-        $::lxdebug->message(LXDebug->WARN(),
-                            "ConvertTimeRecordings: saving delivery order failed for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}});
-        push @{ $self->{job_errors} }, "ConvertTimeRecordings: saving delivery order failed for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}};
+        $self->log_error('saving delivery order failed for time recording ids ' . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}});
+
       } else {
         push @donumbers, $do->donumber;
       }
@@ -241,36 +257,28 @@ sub get_order_for_time_recording {
   if (!$tr->order_id) {
     # check project
     my $project_id;
-    #$project_id   = $self->overide_project_id;
-    $project_id   = $self->data->{project_id};
+    #$project_id   = $self->override_project_id;
+    $project_id   = $self->params->{project_id};
     $project_id ||= $tr->project_id;
     #$project_id ||= $self->default_project_id;
 
     if (!$project_id) {
-      my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : no project id';
-      $::lxdebug->message(LXDebug->WARN(), $err_msg);
-      push @{ $self->{job_errors} }, $err_msg;
+      $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no project id');
       return;
     }
 
     my $project = SL::DB::Project->load_cached($project_id);
 
     if (!$project) {
-      my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : project not found';
-      $::lxdebug->message(LXDebug->WARN(), $err_msg);
-      push @{ $self->{job_errors} }, $err_msg;
+      $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project not found');
       return;
     }
     if (!$project->active || !$project->valid) {
-      my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : project not active or not valid';
-      $::lxdebug->message(LXDebug->WARN(), $err_msg);
-      push @{ $self->{job_errors} }, $err_msg;
+      $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project not active or not valid');
       return;
     }
     if ($project->customer_id && $project->customer_id != $tr->customer_id) {
-      my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : project customer does not match customer of time recording';
-      $::lxdebug->message(LXDebug->WARN(), $err_msg);
-      push @{ $self->{job_errors} }, $err_msg;
+      $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project customer does not match customer of time recording');
       return;
     }
 
@@ -286,30 +294,24 @@ sub get_order_for_time_recording {
   }
 
   if (!scalar @$orders) {
-    my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : no order found';
-    $::lxdebug->message(LXDebug->WARN(), $err_msg);
-    push @{ $self->{job_errors} }, $err_msg;
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no order found');
     return;
   }
 
   # check part
   my $part_id;
-  #$part_id   = $self->overide_part_id;
+  #$part_id   = $self->override_part_id;
   $part_id ||= $tr->part_id;
   #$part_id ||= $self->default_part_id;
-  $part_id ||= $self->data->{part_id};
+  $part_id ||= $self->params->{part_id};
 
   if (!$part_id) {
-    my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : no part id';
-    $::lxdebug->message(LXDebug->WARN(), $err_msg);
-    push @{ $self->{job_errors} }, $err_msg;
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no part id');
     return;
   }
   my $part = SL::DB::Part->load_cached($part_id);
   if (!$part->unit_obj->is_time_based) {
-    my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : part unit is not time based';
-    $::lxdebug->message(LXDebug->WARN(), $err_msg);
-    push @{ $self->{job_errors} }, $err_msg;
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : part unit is not time based');
     return;
   }
 
@@ -321,44 +323,41 @@ sub get_order_for_time_recording {
   }
 
   if (1 != scalar @matching_orders) {
-    my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : no or more than one orders do match';
-    $::lxdebug->message(LXDebug->WARN(), $err_msg);
-    push @{ $self->{job_errors} }, $err_msg;
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no or more than one orders do match');
     return;
   }
 
   my $matching_order = $matching_orders[0];
 
   if (!$matching_order->is_sales) {
-    my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : found order is not a sales order';
-    $::lxdebug->message(LXDebug->WARN(), $err_msg);
-    push @{ $self->{job_errors} }, $err_msg;
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : found order is not a sales order');
     return;
   }
 
   if ($matching_order->customer_id != $tr->customer_id) {
-    my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : customer of order does not match customer of time recording';
-    $::lxdebug->message(LXDebug->WARN(), $err_msg);
-    push @{ $self->{job_errors} }, $err_msg;
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : customer of order does not match customer of time recording');
     return;
   }
 
   if ($tr->project_id && $tr->project_id != ($matching_order->globalproject_id || 0)) {
-    my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : project of order does not match project of time recording';
-    $::lxdebug->message(LXDebug->WARN(), $err_msg);
-    push @{ $self->{job_errors} }, $err_msg;
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project of order does not match project of time recording');
     return;
   }
 
   return $matching_order;
 }
 
+sub log_error {
+  my ($self, $msg) = @_;
+
+  my $dbg = 0;
+
+  push @{ $self->{job_errors} }, $msg;
+  $::lxdebug->message(LXDebug->WARN(), 'ConvertTimeRecordings: ' . $msg) if $dbg;
+}
+
 1;
 
-# possible data
-# from_date: 01.12.2020
-# to_date: 15.12.2020
-# customernumbers: [1,2,3]
 __END__
 
 =pod
@@ -380,7 +379,13 @@ C<SL::DB::DeliveryOrder-E<gt>new_from_time_recordings>).
 
 Some data can be provided to configure this backgroung job.
 If there is user data and it cannot be validated the background job
-returns a error messages.
+fails.
+
+Example:
+
+  from_date: 01.12.2020
+  to_date: 15.12.2020
+  customernumbers: [1,2,3]
 
 =over 4
 
@@ -405,29 +410,29 @@ to_date: 15.12.2020
 =item C<customernumbers>
 
 An array with the customer numbers for which time recordings should
-be collected. If not given, time recordings for customers are
-collected. This is the default.
+be collected. If not given, time recordings for all customers are
+collected.
 
 customernumbers: [c1,22332,334343]
 
 =item C<part_id>
 
 The part id of a time based service which should be used to
-book the times. If not set the clients config defaults is used.
+book the times if no part is set in the time recording entry.
 
 =item C<rounding>
 
 If set the 0 no rounding of the times will be done otherwise
-the times will be rounded up to th full quarters of an hour,
+the times will be rounded up to the full quarters of an hour,
 ie. 0.25h 0.5h 0.75h 1.25h ...
 Defaults to rounding true (1).
 
 =item C<link_order>
 
-If set the job links the created delivery order with with the order
+If set the job links the created delivery order with the order
 given in the time recording entry. If there is no order given, then
-it tries to find an order with with the current customer and project
-number and tries to do as much automatic workflow processing as the
+it tries to find an order with the current customer and project
+number. It tries to do as much automatic workflow processing as the
 UI.
 Defaults to off. If set to true (1) the job will fail if there
 is no sales order which qualifies as a predecessor.
@@ -447,11 +452,34 @@ the services are not yet fully delivered (simple case: 'Payment in advance').
 Hint: take a look or extend the job CloseProjectsBelongingToClosedSalesOrder for
 further automatisation of your organisational needs.
 
-
 =item C<project_id>
 
 Use this project_id instead of the project_id in the time recordings.
 
+=back
+
+=head1 TODO
+
+=over 4
+
+=item * part and project parameters as numbers
+
+Add parameters to give part and project not with their ids, but with their
+numbers. E.g. (default_/override_)part_number,
+(default_/override_)project_number.
+
+=item * part and project parameters override and default
+
+In the moment, the part id given as parameter is used as the default value.
+This means, it will be used if there is no part in the time recvording entry.
+
+The project id given is used as override parameter. It overrides the project
+given in the time recording entry.
+
+To solve this, there should be parameters named override_part_id,
+default_part_id, override_project_id and default_project_id.
+
+
 =back
 
 =head1 AUTHOR