Zeiterfassung: Konvertierung: Artikel/Projekt: override- und default-Parameter
authorBernd Bleßmann <bernd@kivitendo-premium.de>
Wed, 5 May 2021 16:03:31 +0000 (18:03 +0200)
committerBernd Bleßmann <bernd@kivitendo-premium.de>
Wed, 5 May 2021 16:39:18 +0000 (18:39 +0200)
SL/BackgroundJob/ConvertTimeRecordings.pm
SL/DB/DeliveryOrder.pm
t/background_job/convert_time_recordings.t

index 75c09ad..ff8c79b 100644 (file)
@@ -85,13 +85,15 @@ sub initialize_params {
 
   # valid parameters with default values
   my %valid_params = (
 
   # 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,
+    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     => [],
+    override_part_id    => undef,
+    default_part_id     => undef,
+    override_project_id => undef,
+    default_project_id  => undef,
+    rounding            => 1,
+    link_order          => 0,
   );
 
 
   );
 
 
@@ -136,16 +138,24 @@ sub initialize_params {
 
 
   # check part
 
 
   # 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';
+  if ($self->params->{override_part_id} && !SL::DB::Manager::Part->find_by(id => $self->params->{override_part_id},
+                                                                           or => [obsolete => undef, obsolete => 0])) {
+    die 'No valid part found by given override part id';
+  }
+  if ($self->params->{default_part_id} && !SL::DB::Manager::Part->find_by(id => $self->params->{default_part_id},
+                                                                           or => [obsolete => undef, obsolete => 0])) {
+    die 'No valid part found by given default part id';
   }
 
 
   # check project
   }
 
 
   # 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';
+  if ($self->params->{override_project_id} && !SL::DB::Manager::Project->find_by(id => $self->params->{override_project_id},
+                                                                                 active => 1, valid => 1)) {
+    die 'No valid project found by given override project id';
+  }
+  if ($self->params->{default_project_id} && !SL::DB::Manager::Project->find_by(id => $self->params->{default_project_id},
+                                                                                active => 1, valid => 1)) {
+    die 'No valid project found by given default project id';
   }
 
   return $self->params;
   }
 
   return $self->params;
@@ -158,8 +168,9 @@ sub convert_without_linking {
   push @{ $time_recordings_by_customer_id{$_->customer_id} }, $_ for @$time_recordings;
 
   my %convert_params = (
   push @{ $time_recordings_by_customer_id{$_->customer_id} }, $_ for @$time_recordings;
 
   my %convert_params = (
-    rounding        => $self->params->{rounding},
-    default_part_id => $self->params->{part_id},
+    rounding         => $self->params->{rounding},
+    override_part_id => $self->params->{override_part_id},
+    default_part_id  => $self->params->{default_part_id},
   );
 
   my @donumbers;
   );
 
   my @donumbers;
@@ -193,7 +204,8 @@ sub convert_with_linking {
 
   my %convert_params = (
     rounding        => $self->params->{rounding},
 
   my %convert_params = (
     rounding        => $self->params->{rounding},
-    default_part_id => $self->params->{part_id},
+    override_part_id => $self->params->{override_part_id},
+    default_part_id  => $self->params->{default_part_id},
   );
 
   my @donumbers;
   );
 
   my @donumbers;
@@ -257,10 +269,9 @@ sub get_order_for_time_recording {
   if (!$tr->order_id) {
     # check project
     my $project_id;
   if (!$tr->order_id) {
     # check project
     my $project_id;
-    #$project_id   = $self->override_project_id;
-    $project_id   = $self->params->{project_id};
+    $project_id   = $self->params->{override_project_id};
     $project_id ||= $tr->project_id;
     $project_id ||= $tr->project_id;
-    #$project_id ||= $self->default_project_id;
+    $project_id ||= $self->params->{default_project_id};
 
     if (!$project_id) {
       $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no project id');
 
     if (!$project_id) {
       $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no project id');
@@ -300,10 +311,9 @@ sub get_order_for_time_recording {
 
   # check part
   my $part_id;
 
   # check part
   my $part_id;
-  #$part_id   = $self->override_part_id;
+  $part_id   = $self->params->{override_part_id};
   $part_id ||= $tr->part_id;
   $part_id ||= $tr->part_id;
-  #$part_id ||= $self->default_part_id;
-  $part_id ||= $self->params->{part_id};
+  $part_id ||= $self->params->{default_part_id};
 
   if (!$part_id) {
     $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no part id');
 
   if (!$part_id) {
     $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no part id');
@@ -339,7 +349,7 @@ sub get_order_for_time_recording {
     return;
   }
 
     return;
   }
 
-  if ($tr->project_id && $tr->project_id != ($matching_order->globalproject_id || 0)) {
+  if ($tr->project_id && !$self->params->{override_project_id} && $tr->project_id != ($matching_order->globalproject_id || 0)) {
     $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project of order does not match project of time recording');
     return;
   }
     $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project of order does not match project of time recording');
     return;
   }
@@ -415,7 +425,13 @@ collected.
 
 customernumbers: [c1,22332,334343]
 
 
 customernumbers: [c1,22332,334343]
 
-=item C<part_id>
+=item C<override_part_id>
+
+The part id of a time based service which should be used to
+book the times instead of the parts which are set in the time
+recordings.
+
+=item C<default_part_id>
 
 The part id of a time based service which should be used to
 book the times if no part is set in the time recording entry.
 
 The part id of a time based service which should be used to
 book the times if no part is set in the time recording entry.
@@ -452,9 +468,15 @@ 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.
 
 Hint: take a look or extend the job CloseProjectsBelongingToClosedSalesOrder for
 further automatisation of your organisational needs.
 
-=item C<project_id>
+=item C<override_project_id>
 
 
-Use this project_id instead of the project_id in the time recordings.
+Use this project id instead of the project id in the time recordings to find
+a related order. This is only used if C<link_order> is true.
+
+=item C<default_project_id>
+
+Use this project id if no project id is set in the time recording
+entry. This is only used if C<link_order> is true.
 
 =back
 
 
 =back
 
@@ -468,17 +490,6 @@ 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.
 
 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
 
 
 =back
 
index 8a0ada7..395a449 100644 (file)
@@ -194,9 +194,12 @@ sub new_from_time_recordings {
   #  - merge same descriptions
   #
 
   #  - merge same descriptions
   #
 
-  my $default_part_id = $params{default_part_id}    ? $params{default_part_id}
-                      : $params{default_partnumber} ? SL::DB::Manager::Part->find_by(partnumber => $params{default_partnumber})->id
-                      : undef;
+  my $default_part_id  = $params{default_part_id}     ? $params{default_part_id}
+                       : $params{default_partnumber}  ? SL::DB::Manager::Part->find_by(partnumber => $params{default_partnumber})->id
+                       : undef;
+  my $override_part_id = $params{override_part_id}    ? $params{override_part_id}
+                       : $params{override_partnumber} ? SL::DB::Manager::Part->find_by(partnumber => $params{override_partnumber})->id
+                       : undef;
 
   # check parts and collect entries
   my %part_by_part_id;
 
   # check parts and collect entries
   my %part_by_part_id;
@@ -204,9 +207,9 @@ sub new_from_time_recordings {
   foreach my $source (@$sources) {
     next if !$source->duration;
 
   foreach my $source (@$sources) {
     next if !$source->duration;
 
-    my $part_id  = $source->part_id ? $source->part_id
-                 : $default_part_id ? $default_part_id
-                 : undef;
+    my $part_id   = $override_part_id;
+    $part_id    ||= $source->part_id;
+    $part_id    ||= $default_part_id;
 
     die 'article not found for entry "' . $source->displayable_times . '"' if !$part_id;
 
 
     die 'article not found for entry "' . $source->displayable_times . '"' if !$part_id;
 
@@ -434,7 +437,8 @@ Creates a new C<SL::DB::DeliveryOrder> instance from the time recordings
 given as C<$sources>. All time recording entries must belong to the same
 customer. Time recordings are sorted by article and date. For each article
 a new delivery order item is created. If no article is associated with an
 given as C<$sources>. All time recording entries must belong to the same
 customer. Time recordings are sorted by article and date. For each article
 a new delivery order item is created. If no article is associated with an
-entry, a default article will be used.
+entry, a default article will be used. The article given in the time
+recording entry can be overriden.
 Entries of the same date (for each article) are summed together and form a
 list entry in the long description of the item.
 
 Entries of the same date (for each article) are summed together and form a
 list entry in the long description of the item.
 
@@ -451,6 +455,24 @@ C<%params> can include the following options:
 An optional hash reference. If it exists then it is used to set
 attributes of the newly created delivery order object.
 
 An optional hash reference. If it exists then it is used to set
 attributes of the newly created delivery order object.
 
+=item C<default_part_id>
+
+An optional part id which is used as default value if no part is set
+in the time recording entry.
+
+=item C<default_partnumber>
+
+Like C<default_part_id> but given as partnumber, not as id.
+
+=item C<override_part_id>
+
+An optional part id which is used instead of a value set in the time
+recording entry.
+
+=item C<override_partnumber>
+
+Like C<overrride_part_id> but given as partnumber, not as id.
+
 =item C<related_order>
 
 An optional C<SL::DB::Order> object. If it exists then it is used to
 =item C<related_order>
 
 An optional C<SL::DB::Order> object. If it exists then it is used to
index 32e2828..454af11 100644 (file)
@@ -1,4 +1,4 @@
-use Test::More tests => 40;
+use Test::More tests => 52;
 
 use strict;
 
 
 use strict;
 
@@ -37,7 +37,7 @@ $::locale = Locale->new('en');
 clear_up();
 
 ########################################
 clear_up();
 
 ########################################
-# two time recordings, one order linked with project_id
+# two time recordings, one order linked with project_id in time recording entry
 ########################################
 my $part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
 my $project  = create_project(projectnumber => 'p1', description => 'Project 1');
 ########################################
 my $part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
 my $project  = create_project(projectnumber => 'p1', description => 'Project 1');
@@ -70,7 +70,6 @@ push @time_recordings, new_time_recording(
 
 my %data   = (
   link_order => 1,
 
 my %data   = (
   link_order => 1,
-  project_id => $project->id,
   from_date  => '01.01.2021',
   to_date    => '30.04.2021',
 );
   from_date  => '01.01.2021',
   to_date    => '30.04.2021',
 );
@@ -102,7 +101,7 @@ clear_up();
 
 
 ########################################
 
 
 ########################################
-# two time recordings, one order linked with project_id
+# two time recordings, one order linked with project_id in time recording entry
 # unit in order is 'min', but part is 'Std'
 ########################################
 $part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
 # unit in order is 'min', but part is 'Std'
 ########################################
 $part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
@@ -133,10 +132,8 @@ push @time_recordings, new_time_recording(
   part       => $part,
 )->save;
 
   part       => $part,
 )->save;
 
-# two time recordings, one order linked with project_id
 %data = (
   link_order => 1,
 %data = (
   link_order => 1,
-  project_id => $project->id,
   from_date  => '01.04.2021',
   to_date    => '30.04.2021',
 );
   from_date  => '01.04.2021',
   to_date    => '30.04.2021',
 );
@@ -161,7 +158,7 @@ clear_up();
 
 
 ########################################
 
 
 ########################################
-# two time recordings, one order linked with project_id
+# two time recordings, one order linked with project_id in time recording entry
 # unit in order is 'Std', but part is 'min'
 ########################################
 $part     = new_service(partnumber => 'Serv1', unit => 'min')->save;
 # unit in order is 'Std', but part is 'min'
 ########################################
 $part     = new_service(partnumber => 'Serv1', unit => 'min')->save;
@@ -192,10 +189,8 @@ push @time_recordings, new_time_recording(
   part       => $part,
 )->save;
 
   part       => $part,
 )->save;
 
-# two time recordings, one order linked with project_id
 %data = (
   link_order => 1,
 %data = (
   link_order => 1,
-  project_id => $project->id,
   from_date  => '01.04.2021',
   to_date    => '30.04.2021',
 );
   from_date  => '01.04.2021',
   to_date    => '30.04.2021',
 );
@@ -313,6 +308,135 @@ is($sales_order->items->[0]->ship*1, 1, 'linked by order_id: ship in related ord
 clear_up();
 
 
 clear_up();
 
 
+########################################
+# override project and part
+########################################
+$part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
+my $part2    = new_service(partnumber => 'Serv2', unit => 'min')->save;
+$project  = create_project(projectnumber => 'p1', description => 'Project 1');
+my $project2 = create_project(projectnumber => 'p2', description => 'Project 2');
+$customer = new_customer()->save;
+
+$sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  globalproject    => $project,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part, qty => 180, unit => 'min', sellprice => 70), ]
+);
+my $sales_order2 = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  globalproject    => $project,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part2, qty => 180, unit => 'min', sellprice => 70), ]
+);
+
+new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute => 10),
+  customer   => $customer,
+  project    => $project,
+  part       => $part,
+)->save;
+new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 12, minute => 10),
+  customer   => $customer,
+  project    => $project2,
+  part       => $part2,
+)->save;
+new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 12, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 13, minute => 10),
+  customer   => $customer,
+)->save;
+
+%data = (
+  link_order          => 1,
+  from_date           => '01.04.2021',
+  to_date             => '30.04.2021',
+  override_part_id    => $part->id,
+  override_project_id => $project->id,
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+$ret    = $job->run($db_obj);
+
+$linked_dos = $sales_order->linked_records(to => 'DeliveryOrder');
+is($linked_dos->[0]->globalproject_id, $project->id, 'overriden part and project: project in delivery order');
+
+$linked_items = $sales_order->items->[0]->linked_records(to => 'DeliveryOrderItem');
+is($linked_items->[0]->qty*1, 3, 'overriden part and project: qty in delivery order');
+is($linked_items->[0]->base_qty*1, 3, 'overriden part and project: base_qty in delivery order');
+is($linked_items->[0]->parts_id, $part->id, 'overriden part and project: part id');
+
+my $linked_dos2 = $sales_order2->linked_records(to => 'DeliveryOrder');
+is(scalar @$linked_dos2, 0, 'overriden part and project: no delivery order for unused order');
+
+# reload order and orderitems to get changes to deliverd and ship
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->load;
+Rose::DB::Object::Helpers::forget_related($sales_order2, 'orderitems');
+$sales_order2->load;
+
+is($sales_order ->items->[0]->ship||0, 180, 'overriden part and project: ship in related order');
+is($sales_order2->items->[0]->ship||0,   0, 'overriden part and project: ship in not related order');
+
+clear_up();
+
+
+########################################
+# default project and part
+########################################
+$part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
+$project  = create_project(projectnumber => 'p1', description => 'Project 1');
+$customer = new_customer()->save;
+
+$sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  globalproject    => $project,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part, qty => 180, unit => 'min', sellprice => 70), ]
+);
+
+new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute => 40),
+  customer   => $customer,
+)->save;
+
+%data = (
+  link_order         => 1,
+  from_date          => '01.04.2021',
+  to_date            => '30.04.2021',
+  default_part_id    => $part->id,
+  default_project_id => $project->id,
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+$ret    = $job->run($db_obj);
+
+$linked_dos = $sales_order->linked_records(to => 'DeliveryOrder');
+is($linked_dos->[0]->globalproject_id, $project->id, 'default and project: project in delivery order');
+
+$linked_items = $sales_order->items->[0]->linked_records(to => 'DeliveryOrderItem');
+is($linked_items->[0]->qty*1, 1.5, 'default part and project: qty in delivery order');
+is($linked_items->[0]->base_qty*1, 1.5, 'default part and project: base_qty in delivery order');
+is($linked_items->[0]->parts_id, $part->id, 'default part and project: part id');
+
+# 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, 90, 'default part and project: ship in related order');
+
+clear_up();
+
+
 ########################################
 # check rounding
 ########################################
 ########################################
 # check rounding
 ########################################
@@ -453,7 +577,7 @@ ok($err_msg =~ '^Customer numbers must be given in an array', 'wrong customer nu
 #####
 
 %data = (
 #####
 
 %data = (
-  part_id => '123',
+  override_part_id => '123',
 );
 
 $db_obj = SL::DB::BackgroundJob->new();
 );
 
 $db_obj = SL::DB::BackgroundJob->new();
@@ -462,13 +586,13 @@ $job    = SL::BackgroundJob::ConvertTimeRecordings->new;
 
 $err_msg = '';
 eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
 
 $err_msg = '';
 eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
-ok($err_msg =~ '^No valid part found by given part id', 'invalid part id detected');
+ok($err_msg =~ '^No valid part found by given override part id', 'invalid part id detected');
 
 #####
 
 $part = new_service(partnumber => 'Serv1', unit => 'Std', obsolete => 1)->save;
 %data = (
 
 #####
 
 $part = new_service(partnumber => 'Serv1', unit => 'Std', obsolete => 1)->save;
 %data = (
-  part_id => $part->id,
+  override_part_id => $part->id,
 );
 
 $db_obj = SL::DB::BackgroundJob->new();
 );
 
 $db_obj = SL::DB::BackgroundJob->new();
@@ -477,12 +601,12 @@ $job    = SL::BackgroundJob::ConvertTimeRecordings->new;
 
 $err_msg = '';
 eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
 
 $err_msg = '';
 eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
-ok($err_msg =~ '^No valid part found by given part id', 'obsolete part detected');
+ok($err_msg =~ '^No valid part found by given override part id', 'obsolete part detected');
 
 #####
 
 %data = (
 
 #####
 
 %data = (
-  project_id => 123,
+  override_project_id => 123,
 );
 
 $db_obj = SL::DB::BackgroundJob->new();
 );
 
 $db_obj = SL::DB::BackgroundJob->new();
@@ -491,13 +615,13 @@ $job    = SL::BackgroundJob::ConvertTimeRecordings->new;
 
 $err_msg = '';
 eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
 
 $err_msg = '';
 eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
-ok($err_msg =~ '^No valid project found by given project id', 'invalid project id detected');
+ok($err_msg =~ '^No valid project found by given override project id', 'invalid project id detected');
 
 #####
 
 $project = create_project(projectnumber => 'p1', description => 'Project 1', valid => 0)->save;
 %data = (
 
 #####
 
 $project = create_project(projectnumber => 'p1', description => 'Project 1', valid => 0)->save;
 %data = (
-  project_id => $project->id,
+  override_project_id => $project->id,
 );
 
 $db_obj = SL::DB::BackgroundJob->new();
 );
 
 $db_obj = SL::DB::BackgroundJob->new();
@@ -506,7 +630,7 @@ $job    = SL::BackgroundJob::ConvertTimeRecordings->new;
 
 $err_msg = '';
 eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
 
 $err_msg = '';
 eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
-ok($err_msg =~ '^No valid project found by given project id', 'invalid project detected');
+ok($err_msg =~ '^No valid project found by given override project id', 'invalid project detected');
 
 #####
 
 
 #####