Zeiterfassung: Konvertierung: angegebenen Auftrag als Vorgänger verwenden können
authorBernd Bleßmann <bernd@kivitendo-premium.de>
Tue, 4 May 2021 10:47:56 +0000 (12:47 +0200)
committerBernd Bleßmann <bernd@kivitendo-premium.de>
Wed, 5 May 2021 15:25:03 +0000 (17:25 +0200)
SL/BackgroundJob/ConvertTimeRecordings.pm
t/background_job/convert_time_recordings.t

index f44e711..f39ac1a 100644 (file)
@@ -20,7 +20,7 @@ sub create_job {
 }
 use Rose::Object::MakeMethods::Generic (
  'scalar'                => [ qw(data) ],
- 'scalar --get_set_init' => [ qw(rounding link_project) ],
+ 'scalar --get_set_init' => [ qw(rounding link_order) ],
 );
 
 # valid parameters -> better as class members with rose generic set/get
@@ -30,7 +30,7 @@ my %valid_params = (
               customernumbers => '',
               part_id => '',
               rounding => 1,
-              link_project => 0,
+              link_order => 0,
               project_id => '',
              );
 
@@ -56,7 +56,7 @@ sub run {
   # 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_project -> 0|1)
+  # 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
@@ -89,7 +89,7 @@ sub run {
 
   my @donumbers;
 
-  if ($self->data->{link_project}) {
+  if ($self->data->{link_order}) {
     my %time_recordings_by_order_id;
     my %orders_by_order_id;
     foreach my $tr (@$time_recordings) {
@@ -126,7 +126,7 @@ sub init_rounding {
   1
 }
 
-sub init_link_project {
+sub init_link_order {
   0
 }
 
@@ -137,7 +137,7 @@ sub convert_without_linking {
   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_project project_id);
+  my %convert_params = map { $_ => $self->data->{$_} } qw(rounding link_order project_id);
   $convert_params{default_part_id} = $self->data->{part_id};
 
   my @donumbers;
@@ -174,7 +174,7 @@ 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_project project_id);
+  my %convert_params = map { $_ => $self->data->{$_} } qw(rounding link_order project_id);
   $convert_params{default_part_id} = $self->data->{part_id};
 
   my @donumbers;
@@ -236,36 +236,57 @@ sub convert_with_linking {
 sub get_order_for_time_recording {
   my ($self, $tr) = @_;
 
-  # check project
-  my $project_id;
-  #$project_id   = $self->overide_project_id;
-  $project_id   = $self->data->{project_id};
-  $project_id ||= $tr->project_id;
-  #$project_id ||= $self->default_project_id;
+  my $orders;
+
+  if (!$tr->order_id) {
+    # check project
+    my $project_id;
+    #$project_id   = $self->overide_project_id;
+    $project_id   = $self->data->{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;
+      return;
+    }
 
-  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;
-    return;
-  }
+    my $project = SL::DB::Project->load_cached($project_id);
 
-  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;
+      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;
+      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;
+      return;
+    }
 
-  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;
-    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;
-    return;
+    $orders = SL::DB::Manager::Order->get_all(where        => [customer_id      => $tr->customer_id,
+                                                               or               => [quotation => undef, quotation => 0],
+                                                               globalproject_id => $project_id, ],
+                                              with_objects => ['orderitems']);
+
+  } else {
+    # order_id given
+    my $order = SL::DB::Manager::Order->find_by(id => $tr->order_id);
+    push @$orders, $order if $order;
   }
-  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';
+
+  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;
     return;
@@ -292,10 +313,6 @@ sub get_order_for_time_recording {
     return;
   }
 
-  my $orders = SL::DB::Manager::Order->get_all(where        => [customer_id      => $tr->customer_id,
-                                                                or               => [quotation => undef, quotation => 0],
-                                                                globalproject_id => $project_id, ],
-                                               with_objects => ['orderitems']);
   my @matching_orders;
   foreach my $order (@$orders) {
     if (any { $_->parts_id == $part_id } @{ $order->items_sorted }) {
@@ -310,7 +327,30 @@ sub get_order_for_time_recording {
     return;
   }
 
-  return $matching_orders[0];
+  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;
+    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;
+    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;
+    return;
+  }
+
+  return $matching_order;
 }
 
 1;
@@ -382,21 +422,24 @@ the times will be rounded up to th full quarters of an hour,
 ie. 0.25h 0.5h 0.75h 1.25h ...
 Defaults to rounding true (1).
 
-=item C<link_project>
+=item C<link_order>
 
-If set the job tries to find a previous Order with the current
-customer and project number and tries to do as much automatic
-workflow processing as the UI.
+If set the job links the created delivery order with 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
+UI.
 Defaults to off. If set to true (1) the job will fail if there
-is no Sales Orders which qualifies as a predecessor.
+is no sales order which qualifies as a predecessor.
 Conditions for a predeccesor:
 
+ * Order given in time recording entry OR
  * Global project_id must match time_recording.project_id OR data.project_id
- * Customer name must match time_recording.customer_id OR data.customernumbers
+ * Customer must match customer in time recording entry
  * The sales order must have at least one or more time related services
  * The Project needs to be valid and active
 
-The job doesn't care if the Sales Order is already delivered or closed.
+The job doesn't care if the sales order is already delivered or closed.
 If the sales order is overdelivered some organisational stuff needs to be done.
 The sales order may also already be closed, ie the amount is fully billed, but
 the services are not yet fully delivered (simple case: 'Payment in advance').
index 4626b3c..db32947 100644 (file)
@@ -1,4 +1,4 @@
-use Test::More tests => 20;
+use Test::More tests => 27;
 
 use strict;
 
@@ -20,7 +20,7 @@ use SL::Dev::ALL qw(:ALL);
 Support::TestSetup::login();
 
 sub clear_up {
-  foreach (qw(OrderItem Order DeliveryOrder TimeRecording Project Part Customer RecordLink)) {
+  foreach (qw(TimeRecording OrderItem Order DeliveryOrder Project Part Customer RecordLink)) {
     "SL::DB::Manager::${_}"->delete_all(all => 1);
   }
   SL::DB::Manager::Employee->delete_all(where => [ '!login' => 'unittests' ]);
@@ -69,10 +69,10 @@ push @time_recordings, new_time_recording(
 )->save;
 
 my %data   = (
-  link_project => 1,
-  project_id   => $project->id,
-  from_date    => '01.04.2021',
-  to_date      => '30.04.2021',
+  link_order => 1,
+  project_id => $project->id,
+  from_date  => '01.04.2021',
+  to_date    => '30.04.2021',
 );
 my $db_obj = SL::DB::BackgroundJob->new();
 $db_obj->set_data(%data);
@@ -135,10 +135,10 @@ push @time_recordings, new_time_recording(
 
 # two time recordings, one order linked with project_id
 %data = (
-  link_project => 1,
-  project_id   => $project->id,
-  from_date    => '01.04.2021',
-  to_date      => '30.04.2021',
+  link_order => 1,
+  project_id => $project->id,
+  from_date  => '01.04.2021',
+  to_date    => '30.04.2021',
 );
 $db_obj = SL::DB::BackgroundJob->new();
 $db_obj->set_data(%data);
@@ -194,10 +194,10 @@ push @time_recordings, new_time_recording(
 
 # two time recordings, one order linked with project_id
 %data = (
-  link_project => 1,
-  project_id   => $project->id,
-  from_date    => '01.04.2021',
-  to_date      => '30.04.2021',
+  link_order => 1,
+  project_id => $project->id,
+  from_date  => '01.04.2021',
+  to_date    => '30.04.2021',
 );
 $db_obj = SL::DB::BackgroundJob->new();
 $db_obj->set_data(%data);
@@ -243,9 +243,9 @@ push @time_recordings, new_time_recording(
 )->save;
 
 %data = (
-  link_project => 0,
-  from_date    => '01.04.2021',
-  to_date      => '30.04.2021',
+  link_order => 0,
+  from_date  => '01.04.2021',
+  to_date    => '30.04.2021',
 );
 $db_obj = SL::DB::BackgroundJob->new();
 $db_obj->set_data(%data);
@@ -259,6 +259,59 @@ is($dos->[0]->items->[0]->base_qty*1, 180, 'date/duration and start/end2: base_q
 clear_up();
 
 
+########################################
+# time recording, linked with order_id
+########################################
+$part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
+$customer = new_customer()->save;
+
+# sales order with globalproject_id
+$sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part, qty => 3, sellprice => 70), ]
+);
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute =>  5),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute =>  5),
+  customer   => $customer,
+  order      => $sales_order,
+  part       => $part,
+)->save;
+
+%data = (
+  link_order => 1,
+  from_date  => '01.04.2021',
+  to_date    => '30.04.2021',
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+$ret    = $job->run($db_obj);
+
+is_deeply($job->{job_errors}, [], 'no errros');
+like($ret, qr{^Number of delivery orders created: 1}, 'linked by order_id: one delivery order created');
+
+$linked_dos = $sales_order->linked_records(to => 'DeliveryOrder');
+is(scalar @$linked_dos, 1, 'linked by order_id: one delivery order linked to order');
+
+$linked_items = $sales_order->items->[0]->linked_records(to => 'DeliveryOrderItem');
+is(scalar @$linked_items, 1, 'linked by order_id: one delivery order item linked to order item');
+is($linked_items->[0]->qty*1, 1, 'linked by order_id: qty in delivery order');
+is($linked_items->[0]->base_qty*1, 1, 'linked by order_id: base_qty in delivery order');
+
+# reload order and orderitems to get changes to deliverd and ship
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->load;
+
+is($sales_order->items->[0]->ship*1, 1, 'linked by order_id: ship in related order');
+
+clear_up();
+
+
 ########################################
 
 $::locale = $old_locale;