+ return @donumbers;
+}
+
+sub convert_with_linking {
+ my ($self, $time_recordings_by_order_id, $orders_by_order_id) = @_;
+
+ my %convert_params = (
+ rounding => $self->params->{rounding},
+ override_part_id => $self->params->{override_part_id},
+ default_part_id => $self->params->{default_part_id},
+ );
+
+ my @donumbers;
+ foreach my $related_order_id (keys %$time_recordings_by_order_id) {
+ my $related_order = $orders_by_order_id->{$related_order_id};
+ my $do;
+ if (!eval {
+ $do = SL::DB::DeliveryOrder->new_from_time_recordings($time_recordings_by_order_id->{$related_order_id}, related_order => $related_order, %convert_params);
+ 1;
+ }) {
+ $self->log_error("creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}});
+ }
+
+ if ($do) {
+ if (!SL::DB->client->with_transaction(sub {
+ $do->save;
+ $_->update_attributes(booked => 1) for @{$time_recordings_by_order_id->{$related_order_id}};
+
+ $related_order->link_to_record($do);
+
+ # TODO extend link_to_record for items, otherwise long-term no d.r.y.
+ foreach my $item (@{ $do->items }) {
+ foreach (qw(orderitems)) {
+ if ($item->{"converted_from_${_}_id"}) {
+ die unless $item->{id};
+ RecordLinks->create_links('mode' => 'ids',
+ 'from_table' => $_,
+ 'from_ids' => $item->{"converted_from_${_}_id"},
+ 'to_table' => 'delivery_order_items',
+ 'to_id' => $item->{id},
+ ) || die;
+ delete $item->{"converted_from_${_}_id"};
+ }
+ }
+ }
+
+ # update delivered and item's ship for related order
+ my $helper = SL::Helper::ShippedQty->new->calculate($related_order)->write_to_objects;
+ $related_order->delivered($related_order->{delivered});
+ $_->ship($_->{shipped_qty}) for @{$related_order->items};
+ $related_order->save(cascade => 1);
+
+ 1;
+ })) {
+ $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;
+ }
+ }
+ }
+
+ return @donumbers;
+}
+
+sub get_order_for_time_recording {
+ my ($self, $tr) = @_;
+
+ my $orders;
+
+ if (!$tr->order_id) {
+ # check project
+ my $project_id;
+ $project_id = $self->params->{override_project_id};
+ $project_id ||= $tr->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');
+ return;
+ }
+
+ my $project = SL::DB::Project->load_cached($project_id);
+
+ if (!$project) {
+ $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project not found');
+ return;
+ }
+ if (!$project->active || !$project->valid) {
+ $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) {
+ $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project customer does not match customer of time recording');
+ 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 (!scalar @$orders) {
+ $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->params->{override_part_id};
+ $part_id ||= $tr->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');
+ return;
+ }
+ my $part = SL::DB::Part->load_cached($part_id);
+ if (!$part->unit_obj->is_time_based) {
+ $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : part unit is not time based');
+ return;
+ }
+
+ my @matching_orders;
+ foreach my $order (@$orders) {
+ if (any { $_->parts_id == $part_id } @{ $order->items_sorted }) {
+ push @matching_orders, $order;
+ }
+ }
+
+ if (1 != scalar @matching_orders) {
+ $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) {
+ $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) {
+ $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 && !$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;
+ }
+
+ 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;