]> wagnertech.de Git - mfinanz.git/blobdiff - SL/Controller/DeliveryOrder.pm
Merge branch 'master' of http://wagnertech.de/git/mfinanz
[mfinanz.git] / SL / Controller / DeliveryOrder.pm
index ab4349774f295fe1e395371c36a62c48df88c004..0603113a19c3b6819eab8b407ddbf9e76ecc84e6 100644 (file)
@@ -4,7 +4,7 @@ use strict;
 use parent qw(SL::Controller::Base);
 
 use SL::Helper::Flash qw(flash_later);
-use SL::Helper::Number qw(_format_number_units _parse_number);
+use SL::Helper::Number qw(_format_number _parse_number);
 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
 use SL::Presenter::DeliveryOrder qw(delivery_order_status_line);
 use SL::Locale::String qw(t8);
@@ -15,29 +15,46 @@ use SL::File;
 use SL::MIME;
 use SL::Util qw(trim);
 use SL::YAML;
+use SL::DBUtils qw(selectall_hashref_query);
 use SL::DB::History;
-use SL::DB::Order;
 use SL::DB::Default;
 use SL::DB::Unit;
 use SL::DB::Order;
+use SL::DB::Order::TypeData qw(:types);
 use SL::DB::Part;
 use SL::DB::PartClassification;
 use SL::DB::PartsGroup;
 use SL::DB::Printer;
 use SL::DB::Language;
+use SL::DB::Reclamation;
+use SL::DB::Reclamation::TypeData qw(:types);
 use SL::DB::RecordLink;
 use SL::DB::Shipto;
 use SL::DB::Translation;
 use SL::DB::TransferType;
+use SL::DB::ValidityToken;
+use SL::DB::EmailJournal;
+use SL::DB::Warehouse;
+use SL::DB::Bin;
+use SL::DB::Helper::RecordLink qw(set_record_link_conversions RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
+use SL::DB::Helper::TypeDataProxy;
+use SL::DB::Helper::Record qw(get_object_name_from_type get_class_from_type);
+use SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData qw(:types);
+use SL::DB::Manager::DeliveryOrderItem;
+use SL::DB::DeliveryOrderItemsStock;
+use SL::Model::Record;
 
 use SL::Helper::CreatePDF qw(:all);
 use SL::Helper::PrintOptions;
 use SL::Helper::ShippedQty;
+use SL::Helper::Inventory;
+use SL::Helper::DateTime;
+use SL::Helper::UserPreferences::DisplayPreferences;
 use SL::Helper::UserPreferences::PositionsScrollbar;
 use SL::Helper::UserPreferences::UpdatePositions;
 
 use SL::Controller::Helper::GetModels;
-use SL::Controller::DeliveryOrder::TypeData qw(:types);
 
 use List::Util qw(first sum0);
 use List::UtilsBy qw(sort_by uniq_by);
@@ -47,20 +64,32 @@ use File::Spec;
 use Cwd;
 use Sort::Naturally;
 
-use Rose::Object::MakeMethods::Generic
-(
- scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
- 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids type_data) ],
+use Rose::Object::MakeMethods::Generic (
+    scalar => [qw(item_ids_to_delete is_custom_shipto_to_delete)],
+    'scalar --get_set_init' => [ qw(
+      order valid_types type cv p all_price_factors search_cvpartnumber
+      show_update_button part_picker_classification_ids type_data
+      ) ],
 );
 
 
 # safety
 __PACKAGE__->run_before('check_auth',
-                        except => [ qw(update_stock_information) ]);
+  except => [ qw(
+    update_stock_information
+    ) ]);
+
+__PACKAGE__->run_before('check_auth_for_edit',
+  except => [ qw(
+    update_stock_information edit
+    stock_in_out_dialog load_second_rows
+    ) ]);
 
 __PACKAGE__->run_before('get_unalterable_data',
-                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
-                                     print send_email) ]);
+  only => [ qw(
+    save save_as_new workflow_new_record workflow_invoice
+    save_and_ap_transaction print send_email
+    ) ]);
 
 #
 # actions
@@ -70,11 +99,14 @@ __PACKAGE__->run_before('get_unalterable_data',
 sub action_add {
   my ($self) = @_;
 
-  $self->order->transdate(DateTime->now_local());
-  $self->type_data->set_reqdate_by_type;
+  $self->pre_render();
 
+  if (!$::form->{form_validity_token}) {
+    $::form->{form_validity_token} = SL::DB::ValidityToken->create(
+      scope => SL::DB::ValidityToken::SCOPE_DELIVERY_ORDER_SAVE()
+    )->token;
+  }
 
-  $self->pre_render();
   $self->render(
     'delivery_order/form',
     title => $self->get_title_for('add'),
@@ -82,39 +114,70 @@ sub action_add {
   );
 }
 
-sub action_add_from_order {
+sub action_add_from_record {
   my ($self) = @_;
-  # this interfers with init_order
-  $self->{converted_from_oe_id} = delete $::form->{id};
+  my $from_type = $::form->{from_type};
+  my $from_id   = $::form->{from_id};
+
+  die "No 'from_type' was given." unless ($from_type);
+  die "No 'from_id' was given."   unless ($from_id);
+
+  my %flags = ();
+  if (defined($::form->{from_item_ids})) {
+    my %use_item = map { $_ => 1 } @{$::form->{from_item_ids}};
+    $flags{item_filter} = sub {
+      my ($item) = @_;
+      return %use_item{$item->{RECORD_ITEM_ID()}};
+    }
+  }
 
-  $self->type_data->validate($::form->{type});
+  my $record = SL::Model::Record->get_record($from_type, $from_id);
 
-  my $order = SL::DB::Order->new(id => $self->{converted_from_oe_id})->load;
+  # If we are coming from an order workflow, only consider not delivered
+  # quantities.
+  if (ref $record eq 'SL::DB::Order') {
+    # Calculate shipped qtys here to prevent calling calculate for every item
+    # via the items method.
+    SL::Helper::ShippedQty->new->calculate($record)->write_to(\@{$record->items});
 
-  $self->order(SL::DB::DeliveryOrder->new_from($order, type => $::form->{type}));
+    my @items_with_not_delivered_qty =
+      grep {$_->qty > 0}
+      map  {$_->qty($_->qty - $_->shipped_qty); $_}
+      @{$record->items_sorted};
+
+    $flags{items} = \@items_with_not_delivered_qty;
+  }
+
+  my $delivery_order = SL::Model::Record->new_from_workflow($record, $self->type, %flags);
+  $self->order($delivery_order);
+  $self->reinit_after_new_order();
 
   $self->action_add;
 }
 
-# edit an existing order
-sub action_edit {
+sub action_add_from_email_journal {
   my ($self) = @_;
+  die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
 
-  if ($::form->{id}) {
-    $self->load_order;
+  $self->action_add();
+}
 
-  } else {
-    # this is to edit an order from an unsaved order object
+sub action_edit_with_email_journal_workflow {
+  my ($self) = @_;
+  die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
+  $::form->{workflow_email_journal_id}    = delete $::form->{email_journal_id};
+  $::form->{workflow_email_attachment_id} = delete $::form->{email_attachment_id};
+  $::form->{workflow_email_callback}      = delete $::form->{callback};
 
-    # set item ids to new fake id, to identify them as new items
-    foreach my $item (@{$self->order->items_sorted}) {
-      $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
-    }
-    # trigger rendering values for second row as hidden, because they
-    # are loaded only on demand. So we need to keep the values from
-    # the source.
-    $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
-  }
+  $self->action_edit();
+}
+
+# edit an existing order
+sub action_edit {
+  my ($self) = @_;
+  die "No 'id' was given." unless $::form->{id};
+
+  $self->load_order;
 
   $self->pre_render();
   $self->render(
@@ -124,47 +187,11 @@ sub action_edit {
   );
 }
 
-# edit a collective order (consisting of one or more existing orders)
-sub action_edit_collective {
-  my ($self) = @_;
-
-  # collect order ids
-  my @multi_ids = map {
-    $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
-  } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
-
-  # fall back to add if no ids are given
-  if (scalar @multi_ids == 0) {
-    $self->action_add();
-    return;
-  }
-
-  # fall back to save as new if only one id is given
-  if (scalar @multi_ids == 1) {
-    $self->order(SL::DB::DeliveryOrder->new(id => $multi_ids[0])->load);
-    $self->action_save_as_new();
-    return;
-  }
-
-  # make new order from given orders
-  my @multi_orders = map { SL::DB::DeliveryOrder->new(id => $_)->load } @multi_ids;
-  $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
-  $self->order(SL::DB::DeliveryOrder->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
-
-  $self->action_edit();
-}
-
 # delete the order
 sub action_delete {
   my ($self) = @_;
 
-  my $errors = $self->delete();
-
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
-  }
-
+  SL::Model::Record->delete($self->order);
   flash_later('info', $self->type_data->text("delete"));
 
   my @redirect_params = (
@@ -179,20 +206,28 @@ sub action_delete {
 sub action_save {
   my ($self) = @_;
 
-  my $errors = $self->save();
-
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
+  if ( $self->order->delivered ) {
+    $self->js->flash('error', t8('This record has already been delivered.'));
     return $self->js->render();
   }
 
+  $self->save();
+
   flash_later('info', $self->type_data->text("saved"));
 
-  my @redirect_params = (
-    action => 'edit',
-    type   => $self->type,
-    id     => $self->order->id,
-  );
+  my @redirect_params;
+  if ($::form->{back_to_caller}) {
+    @redirect_params = $::form->{callback} ? ($::form->{callback})
+                                           : (controller => 'LoginScreen', action => 'user_login');
+
+  } else {
+    @redirect_params = (
+      action   => 'edit',
+      type     => $self->type,
+      id       => $self->order->id,
+      callback => $::form->{callback},
+    );
+  }
 
   $self->redirect_to(@redirect_params);
 }
@@ -208,37 +243,38 @@ sub action_save_as_new {
     return $self->js->render();
   }
 
-  # load order from db to check if values changed
   my $saved_order = SL::DB::DeliveryOrder->new(id => $order->id)->load;
 
-  my %new_attrs;
-  # Lets assign a new number if the user hasn't changed the previous one.
-  # If it has been changed manually then use it as-is.
-  $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
-                        ? ''
-                        : trim($order->number);
-
-  # Clear transdate unless changed
-  $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
-                        ? DateTime->today_local
-                        : $order->transdate;
-
-  # Set new reqdate unless changed if it is enabled in client config
-  $new_attrs{reqdate} = $self->type_data->get_reqdate_by_type($order->reqdate, $saved_order->reqdate);
-
-  # Update employee
-  $new_attrs{employee}  = SL::DB::Manager::Employee->current;
-
   # Create new record from current one
-  $self->order(SL::DB::DeliveryOrder->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
+  my $new_order = SL::Model::Record->clone_for_save_as_new($saved_order, $order);
+  $self->order($new_order);
 
-  # no linked records on save as new
-  delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
+  if (!$::form->{form_validity_token}) {
+    $::form->{form_validity_token} = SL::DB::ValidityToken->create(
+      scope => SL::DB::ValidityToken::SCOPE_DELIVERY_ORDER_SAVE()
+    )->token;
+  }
 
   # save
   $self->action_save();
 }
 
+# close a already saved order (potentially already delivered)
+sub action_close_order {
+  my ($self) = @_;
+
+  $self->order->update_attributes(
+    closed => 1
+  );
+
+  $self->js
+    ->flash("info", t8("The record has been closed."))
+    ->run('kivi.ActionBar.setDisabled', '#close_order',
+          t8('This record has already been closed.'))
+    ->html('#data-status-line', delivery_order_status_line($self->order))
+    ->render
+}
+
 # print the order
 #
 # This is called if "print" is pressed in the print dialog.
@@ -247,14 +283,16 @@ sub action_save_as_new {
 sub action_print {
   my ($self) = @_;
 
-  my $errors = $self->save();
-
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
+  if ( !$self->order->delivered ) {
+    $self->save();
+    $self->js_reset_order_and_item_ids_after_save;
   }
 
-  $self->js_reset_order_and_item_ids_after_save;
+  my $redirect_url = $self->url_for(
+    action => 'edit',
+    type   => $self->type,
+    id     => $self->order->id,
+  );
 
   my $format      = $::form->{print_options}->{format};
   my $media       = $::form->{print_options}->{media};
@@ -265,12 +303,14 @@ sub action_print {
 
   # only pdf and opendocument by now
   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
-    return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
+    flash_later('error', t8('Format \'#1\' is not supported yet/anymore.', $format));
+    return $self->js->redirect_to($redirect_url)->render;
   }
 
   # only screen or printer by now
   if (none { $media eq $_ } qw(screen printer)) {
-    return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
+    flash_later('error', t8('Media \'#1\' is not supported yet/anymore.', $media));
+    return $self->js->redirect_to($redirect_url)->render;
   }
 
   # create a form for generate_attachment_filename
@@ -279,22 +319,25 @@ sub action_print {
   $form->{type}             = $self->type;
   $form->{format}           = $format;
   $form->{formname}         = $formname;
-  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
+  $form->{language}         =
+    '_' . $self->order->language->template_code if $self->order->language;
   my $pdf_filename          = $form->generate_attachment_filename();
-
   my $pdf;
-  my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
-                                                   formname   => $formname,
-                                                   language   => $self->order->language,
-                                                   printer_id => $printer_id,
-                                                   groupitems => $groupitems });
+  my @errors = generate_pdf($self->order, \$pdf, {
+      format     => $format,
+      formname   => $formname,
+      language   => $self->order->language,
+      printer_id => $printer_id,
+      groupitems => $groupitems
+    });
   if (scalar @errors) {
-    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
+    flash_later('error', t8('Generating the document failed: #1', $errors[0]));
+    return $self->js->redirect_to($redirect_url)->render;
   }
 
   if ($media eq 'screen') {
     # screen/download
-    $self->js->flash('info', t8('The PDF has been created'));
+    flash_later('info', t8('The document has been created.'));
     $self->send_file(
       \$pdf,
       type         => SL::MIME->mime_type_from_ext($pdf_filename),
@@ -310,30 +353,34 @@ sub action_print {
       content => $pdf,
     );
 
-    $self->js->flash('info', t8('The PDF has been printed'));
+    flash_later('info', t8('The document has been printed.'));
   }
 
-  my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $pdf_filename);
+  my @warnings = store_pdf_to_webdav_and_filemanagement(
+    $self->order, $pdf, $pdf_filename, $formname
+  );
   if (scalar @warnings) {
-    $self->js->flash('warning', $_) for @warnings;
+    flash_later('warning', $_) for @warnings;
   }
 
   $self->save_history('PRINTED');
 
-  $self->js
-    ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
-    ->render;
+  $self->js->redirect_to($redirect_url)->render;
 }
+
 sub action_preview_pdf {
   my ($self) = @_;
 
-  my $errors = $self->save();
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
+  if ( !$self->order->delivered ) {
+    $self->save();
+    $self->js_reset_order_and_item_ids_after_save;
   }
 
-  $self->js_reset_order_and_item_ids_after_save;
+  my $redirect_url = $self->url_for(
+    action => 'edit',
+    type   => $self->type,
+    id     => $self->order->id,
+  );
 
   my $format      = 'pdf';
   my $media       = 'screen';
@@ -346,103 +393,138 @@ sub action_preview_pdf {
   $form->{type}             = $self->type;
   $form->{format}           = $format;
   $form->{formname}         = $formname;
-  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
+  $form->{language}         =
+    '_' . $self->order->language->template_code if $self->order->language;
   my $pdf_filename          = $form->generate_attachment_filename();
 
   my $pdf;
-  my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
-                                                   formname   => $formname,
-                                                   language   => $self->order->language,
-                                                 });
+  my @errors = generate_pdf($self->order, \$pdf, {
+      format     => $format,
+      formname   => $formname,
+      language   => $self->order->language,
+    });
   if (scalar @errors) {
-    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
+    flash_later('error', t8('Conversion to PDF failed: #1', $errors[0]));
+    return $self->js->redirect_to($redirect_url)->render;
   }
   $self->save_history('PREVIEWED');
-  $self->js->flash('info', t8('The PDF has been previewed'));
+  flash_later('info', t8('The PDF has been previewed'));
   # screen/download
   $self->send_file(
     \$pdf,
     type         => SL::MIME->mime_type_from_ext($pdf_filename),
     name         => $pdf_filename,
-    js_no_render => 0,
+    js_no_render => 1,
   );
+  $self->js->redirect_to($redirect_url)->render;
 }
 
 # open the email dialog
 sub action_save_and_show_email_dialog {
   my ($self) = @_;
 
-  my $errors = $self->save();
-
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
+  if (!$self->order->delivered) {
+    $self->save();
+    $self->js_reset_order_and_item_ids_after_save;
   }
 
-  my $cv_method = $self->cv;
-
-  if (!$self->order->$cv_method) {
-    return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
-                    ->render($self);
-  }
-
-  my $email_form;
-  $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
-  $email_form->{to} ||= $self->order->$cv_method->email;
-  $email_form->{cc}   = $self->order->$cv_method->cc;
-  $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
-  # Todo: get addresses from shipto, if any
+  my $cv = $self->order->customervendor
+    or return $self->js->flash('error',
+      $self->cv eq 'customer' ?
+           t8('Cannot send E-mail without customer given')
+         : t8('Cannot send E-mail without vendor given')
+    )->render($self);
 
   my $form = Form->new;
   $form->{$self->nr_key()}  = $self->order->number;
   $form->{cusordnumber}     = $self->order->cusordnumber;
   $form->{formname}         = $self->type;
   $form->{type}             = $self->type;
-  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
-  $form->{language_id}      = $self->order->language->id                  if $self->order->language;
+  $form->{language}         =
+    '_' . $self->order->language->template_code if $self->order->language;
+  $form->{language_id}      =
+    $self->order->language->id                  if $self->order->language;
   $form->{format}           = 'pdf';
-  $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
+  $form->{cp_id}            =
+    $self->order->contact->cp_id if $self->order->contact;
 
+  my $email_form;
+  $email_form->{to} =
+       ($self->order->contact ? $self->order->contact->cp_email : undef)
+    || ($cv->is_customer ? $cv->delivery_order_mail : undef)
+    ||  $cv->email;
+  $email_form->{cc}  = $cv->cc;
+  $email_form->{bcc} = join ', ', grep $_, $cv->bcc;
+  # Todo: get addresses from shipto, if any
   $email_form->{subject}             = $form->generate_email_subject();
   $email_form->{attachment_filename} = $form->generate_attachment_filename();
   $email_form->{message}             = $form->generate_email_body();
   $email_form->{js_send_function}    = 'kivi.DeliveryOrder.send_email()';
 
   my %files = $self->get_files_for_email_dialog();
-  $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
-  my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
-                                  email_form  => $email_form,
-                                  show_bcc    => $::auth->assert('email_bcc', 'may fail'),
-                                  FILES       => \%files,
-                                  is_customer => $self->type_data->is_customer,
-                                  ALL_EMPLOYEES => $self->{all_employees},
+
+  my @employees_with_email = grep {
+    my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
+    $user && !!trim($user->get_config_value('email'));
+  } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
+
+  my $dialog_html = $self->render(
+    'common/_send_email_dialog', { output => 0 },
+    email_form  => $email_form,
+    show_bcc    => $::auth->assert('email_bcc', 'may fail'),
+    FILES       => \%files,
+    is_customer => $self->type_data->properties("is_customer"),
+    ALL_EMPLOYEES => \@employees_with_email,
+    ALL_PARTNER_EMAIL_ADDRESSES => $cv->get_all_email_addresses(),
   );
 
   $self->js
-      ->run('kivi.DeliveryOrder.show_email_dialog', $dialog_html)
-      ->reinit_widgets
-      ->render($self);
+    ->run('kivi.DeliveryOrder.show_email_dialog', $dialog_html)
+    ->reinit_widgets
+    ->render($self);
 }
 
 # send email
-#
-# Todo: handling error messages: flash is not displayed in dialog, but in the main form
 sub action_send_email {
   my ($self) = @_;
 
-  my $errors = $self->save();
-
-  if (scalar @{ $errors }) {
-    $self->js->run('kivi.DeliveryOrder.close_email_dialog');
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
+  if ( !$self->order->delivered ) {
+    eval {
+      $self->save();
+      1;
+    } or do {
+      $self->js->run('kivi.Order.close_email_dialog');
+      die $EVAL_ERROR;
+    };
   }
 
-  $self->js_reset_order_and_item_ids_after_save;
+  my @redirect_params = (
+    action => 'edit',
+    type   => $self->type,
+    id     => $self->order->id,
+  );
 
+  # Set the error handler to reload the document and display errors later,
+  # because the document is already saved and saving can have some side effects
+  # such as generating a document number, project number or record links,
+  # which will be up to date when the document is reloaded.
+  # Hint: Do not use "die" here and try to catch exceptions in subroutine
+  # calls. You should use "$::form->error" which respects the error handler.
+  local $::form->{__ERROR_HANDLER} = sub {
+      flash_later('error', $_[0]);
+      $self->redirect_to(@redirect_params);
+      $::dispatcher->end_request;
+  };
+
+  # move $::form->{email_form} to $::form
   my $email_form  = delete $::form->{email_form};
-  my %field_names = (to => 'email');
 
+  if ($email_form->{additional_to}) {
+    $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
+    delete $email_form->{additional_to};
+  }
+
+  my %field_names = (to => 'email');
   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
   # for Form::cleanup which may be called in Form::send_email
@@ -452,19 +534,37 @@ sub action_send_email {
   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
   $::form->{media}  = 'email';
 
-  if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
+  $::form->{attachment_policy} //= '';
+
+  # Is an old file version available?
+  my $attfile;
+  if ($::form->{attachment_policy} eq 'old_file') {
+    $attfile = SL::File->get_all(
+      object_id   => $self->order->id,
+      object_type => $::form->{formname},
+      file_type   => 'document',
+      print_variant => $::form->{formname},
+    );
+  }
+
+  if (   $::form->{attachment_policy} ne 'no_file'
+    && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
     my $pdf;
-    my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
-                                                    format     => $::form->{print_options}->{format},
-                                                    formname   => $::form->{print_options}->{formname},
-                                                    language   => $self->order->language,
-                                                    printer_id => $::form->{print_options}->{printer_id},
-                                                    groupitems => $::form->{print_options}->{groupitems}});
+    my @errors = generate_pdf($self->order, \$pdf, {
+        media      => $::form->{media},
+        format     => $::form->{print_options}->{format},
+        formname   => $::form->{print_options}->{formname},
+        language   => $self->order->language,
+        printer_id => $::form->{print_options}->{printer_id},
+        groupitems => $::form->{print_options}->{groupitems}},
+    );
     if (scalar @errors) {
-      return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
+      $::form->error(t8('Generating the document failed: #1', $errors[0]));
     }
 
-    my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $::form->{attachment_filename});
+    my @warnings = store_pdf_to_webdav_and_filemanagement(
+      $self->order, $pdf, $::form->{attachment_filename}, $::form->{formname}
+    );
     if (scalar @warnings) {
       flash_later('warning', $_) for @warnings;
     }
@@ -474,87 +574,77 @@ sub action_send_email {
     $sfile->fh->close;
 
     $::form->{tmpfile} = $sfile->file_name;
-    $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
+    $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be
+                                           # called in Form::send_email
   }
 
-  $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
+  $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a
+                                    # linked record to the mail
   $::form->send_email(\%::myconfig, 'pdf');
 
-  # internal notes
-  my $intnotes = $self->order->intnotes;
-  $intnotes   .= "\n\n" if $self->order->intnotes;
-  $intnotes   .= t8('[email]')                                                                                        . "\n";
-  $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
-  $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
-  $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
-  $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
-  $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
-  $intnotes   .= t8('Message')    . ": " . $::form->{message};
-
-  $self->order->update_attributes(intnotes => $intnotes);
-
   $self->save_history('MAILED');
-
   flash_later('info', t8('The email has been sent.'));
 
-  my @redirect_params = (
-    action => 'edit',
-    type   => $self->type,
-    id     => $self->order->id,
-  );
+  # internal notes unless no email journal
+  unless ($::instance_conf->get_email_journal) {
+    my $intnotes = $self->order->intnotes;
+    $intnotes   .= "\n\n" if $self->order->intnotes;
+    $intnotes   .= t8('[email]')                                . "\n";
+    $intnotes   .= t8('Date')       . ": " .
+      $::locale->format_date_object(
+        DateTime->now_local, precision => 'seconds'
+      ) . "\n";
+    $intnotes   .= t8('To (email)') . ": " . $::form->{email}   . "\n";
+    $intnotes   .= t8('Cc')         . ": " . $::form->{cc}      . "\n"    if $::form->{cc};
+    $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}     . "\n"    if $::form->{bcc};
+    $intnotes   .= t8('Subject')    . ": " . $::form->{subject} . "\n\n";
+    $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
+
+    $self->order->update_attributes(intnotes => $intnotes);
+  }
 
   $self->redirect_to(@redirect_params);
 }
 
-# save the order and redirect to the frontend subroutine for a new
-# delivery order
-sub action_save_and_delivery_order {
+sub action_workflow_new_record {
   my ($self) = @_;
-
-  $self->save_and_redirect_to(
-    controller => 'oe.pl',
-    action     => 'oe_delivery_order_from_order',
+  my $to_type = $::form->{to_type};
+  my $to_controller = get_object_name_from_type($to_type);
+
+  my %additional_params = ();
+  if ($::form->{only_selected_item_positions}) { # ids can be unset before save
+    my $item_positions = $::form->{selected_item_positions} || [];
+    my @from_item_ids = map { $self->order->items_sorted->[$_]->id } @$item_positions;
+    $additional_params{from_item_ids} = \@from_item_ids;
+  }
+
+  flash_later('info', $self->type_data->text('saved'));
+
+  $self->redirect_to(
+    controller => $to_controller,
+    action     => 'add_from_record',
+    type       => $to_type,
+    from_id    => $self->order->id,
+    from_type  => $self->order->type,
+    email_journal_id    => $::form->{workflow_email_journal_id},
+    email_attachment_id => $::form->{workflow_email_attachment_id},
+    callback            => $::form->{workflow_email_callback},
+    %additional_params,
   );
 }
 
 # save the order and redirect to the frontend subroutine for a new
 # invoice
-sub action_save_and_invoice {
+sub action_workflow_invoice {
   my ($self) = @_;
 
-  $self->save_and_redirect_to(
-    controller => 'oe.pl',
-    action     => 'oe_invoice_from_order',
-  );
-}
-
-# workflow from sales order to sales quotation
-sub action_sales_quotation {
-  $_[0]->workflow_sales_or_request_for_quotation();
-}
-
-# workflow from sales order to sales quotation
-sub action_request_for_quotation {
-  $_[0]->workflow_sales_or_request_for_quotation();
-}
-
-# workflow from sales quotation to sales order
-sub action_sales_order {
-  $_[0]->workflow_sales_or_purchase_order();
-}
-
-# workflow from rfq to purchase order
-sub action_purchase_order {
-  $_[0]->workflow_sales_or_purchase_order();
-}
-
-# workflow from purchase order to ap transaction
-sub action_save_and_ap_transaction {
-  my ($self) = @_;
-
-  $self->save_and_redirect_to(
-    controller => 'ap.pl',
-    action     => 'add_from_purchase_order',
+  $self->redirect_to(
+    controller => 'do.pl',
+    action     => 'invoice_from_delivery_order_controller',
+    from_id    => $self->order->id,
+    email_journal_id    => $::form->{workflow_email_journal_id},
+    email_attachment_id => $::form->{workflow_email_attachment_id},
+    callback            => $::form->{workflow_email_callback},
   );
 }
 
@@ -564,11 +654,14 @@ sub action_save_and_ap_transaction {
 sub action_customer_vendor_changed {
   my ($self) = @_;
 
-  setup_order_from_cv($self->order);
+  $self->order(
+    SL::Model::Record->update_after_customer_vendor_change($self->order)
+  );
 
   my $cv_method = $self->cv;
 
-  if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
+  if ( $self->order->$cv_method->contacts
+    && scalar @{ $self->order->$cv_method->contacts } > 0) {
     $self->js->show('#cp_row');
   } else {
     $self->js->hide('#cp_row');
@@ -601,40 +694,6 @@ sub action_customer_vendor_changed {
   $self->js->render();
 }
 
-# open the dialog for customer/vendor details
-sub action_show_customer_vendor_details_dialog {
-  my ($self) = @_;
-
-  my $is_customer = 'customer' eq $::form->{vc};
-  my $cv;
-  if ($is_customer) {
-    $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
-  } else {
-    $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
-  }
-
-  my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
-  $details{discount_as_percent} = $cv->discount_as_percent;
-  $details{creditlimt}          = $cv->creditlimit_as_number;
-  $details{business}            = $cv->business->description      if $cv->business;
-  $details{language}            = $cv->language_obj->description  if $cv->language_obj;
-  $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
-  $details{payment_terms}       = $cv->payment->description       if $cv->payment;
-  $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
-
-  foreach my $entry (@{ $cv->shipto }) {
-    push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
-  }
-  foreach my $entry (@{ $cv->contacts }) {
-    push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
-  }
-
-  $_[0]->render('common/show_vc_details', { layout => 0 },
-                is_customer => $is_customer,
-                %details);
-
-}
-
 # called if a unit in an existing item row is changed
 sub action_unit_changed {
   my ($self) = @_;
@@ -645,8 +704,11 @@ sub action_unit_changed {
   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
-  $self->js
-    ->run('kivi.DeliveryOrder.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
+  $self->js->run(
+    'kivi.DeliveryOrder.update_sellprice',
+    $::form->{item_id},
+    $item->sellprice_as_number
+  );
   $self->js_redisplay_line_values;
   $self->js->render();
 }
@@ -672,12 +734,15 @@ sub action_add_item {
                                      ITEM => $item,
                                      ID   => $item_id,
                                      SELF => $self,
-                                     in_out => $self->type_data->transfer,
+                                     in_out => $self->type_data->properties("transfer"),
   );
 
   if ($::form->{insert_before_item_id}) {
     $self->js
-      ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+      ->before(
+        '.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')',
+        $row_as_html
+      );
   } else {
     $self->js
       ->append('#row_table_id', $row_as_html);
@@ -686,19 +751,26 @@ sub action_add_item {
   if ( $item->part->is_assortment ) {
     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
-      my $attr = { parts_id => $assortment_item->parts_id,
-                   qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
-                   unit     => $assortment_item->unit,
-                   description => $assortment_item->part->description,
-                 };
+      my $attr = {
+        parts_id => $assortment_item->parts_id,
+        qty      => $assortment_item->qty *
+          $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
+        unit     => $assortment_item->unit,
+        description => $assortment_item->part->description,
+      };
       my $item = new_item($self->order, $attr);
 
-      # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
+      # set discount to 100% if item isn't supposed to be charged, overwriting
+      # any customer discount
       $item->discount(1) unless $assortment_item->charge;
 
       $self->order->add_items( $item );
       $self->get_item_cvpartnumber($item);
-      my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+      my $item_id = join('_',
+        'new',
+        Time::HiRes::gettimeofday(),
+        int rand 1000000000000
+      );
       my $row_as_html = $self->p->render('delivery_order/tabs/_row',
                                          ITEM => $item,
                                          ID   => $item_id,
@@ -706,7 +778,10 @@ sub action_add_item {
       );
       if ($::form->{insert_before_item_id}) {
         $self->js
-          ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+          ->before(
+            '.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')',
+            $row_as_html
+          );
       } else {
         $self->js
           ->append('#row_table_id', $row_as_html);
@@ -738,14 +813,16 @@ sub action_add_multi_items {
     push @items, $item;
     if ( $item->part->is_assortment ) {
       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
-        my $attr = { parts_id => $assortment_item->parts_id,
-                     qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
-                     unit     => $assortment_item->unit,
-                     description => $assortment_item->part->description,
-                   };
+        my $attr = {
+          parts_id => $assortment_item->parts_id,
+          qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
+          unit     => $assortment_item->unit,
+          description => $assortment_item->part->description,
+        };
         my $item = new_item($self->order, $attr);
 
-        # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
+        # set discount to 100% if item isn't supposed to be charged, overwriting
+        # any customer discount
         $item->discount(1) unless $assortment_item->charge;
         push @items, $item;
       }
@@ -755,17 +832,24 @@ sub action_add_multi_items {
 
   foreach my $item (@items) {
     $self->get_item_cvpartnumber($item);
-    my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+    my $item_id = join('_',
+      'new',
+      Time::HiRes::gettimeofday(),
+      int rand 1000000000000
+    );
     my $row_as_html = $self->p->render('delivery_order/tabs/_row',
-                                       ITEM => $item,
-                                       ID   => $item_id,
-                                       SELF => $self,
-                                       in_out => $self->type_data->transfer,
+      ITEM => $item,
+      ID   => $item_id,
+      SELF => $self,
+      in_out => $self->type_data->properties("transfer"),
     );
 
     if ($::form->{insert_before_item_id}) {
       $self->js
-        ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+        ->before(
+          '.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')',
+          $row_as_html
+        );
     } else {
       $self->js
         ->append('#row_table_id', $row_as_html);
@@ -810,7 +894,9 @@ sub action_reorder_items {
   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
   my $method = $sort_keys{$::form->{order_by}};
-  my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
+  my @to_sort =
+    map { { old_pos => $_->position, order_by => $method->($_) } }
+    @{ $self->order->items_sorted };
   if ($::form->{sort_dir}) {
     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
@@ -829,16 +915,6 @@ sub action_reorder_items {
     ->render;
 }
 
-# show the popup to choose a price/discount source
-sub action_price_popup {
-  my ($self) = @_;
-
-  my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
-  my $item = $self->order->items_sorted->[$idx];
-
-  $self->render_price_dialog($item);
-}
-
 # save the order in a session variable and redirect to the part controller
 sub action_create_part {
   my ($self) = @_;
@@ -851,14 +927,16 @@ sub action_create_part {
     previousform => $previousform,
   );
 
-  flash_later('info', t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.'));
+  flash_later('info',
+    t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.')
+  );
 
   my @redirect_params = (
-    controller => 'Part',
-    action     => 'add',
-    part_type  => $::form->{add_item}->{create_part_type},
-    callback   => $callback,
-    show_abort => 1,
+    controller    => 'Part',
+    action        => 'add',
+    part_type     => $::form->{add_item}->{create_part_type},
+    callback      => $callback,
+    inline_create => 1,
   );
 
   $self->redirect_to(@redirect_params);
@@ -867,60 +945,82 @@ sub action_create_part {
 sub action_return_from_create_part {
   my ($self) = @_;
 
-  $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
+  $self->{created_part} = SL::DB::Part->new(
+    id => delete $::form->{new_parts_id}
+  )->load if $::form->{new_parts_id};
 
   $::auth->restore_form_from_session(delete $::form->{previousform});
 
-  # set item ids to new fake id, to identify them as new items
-  foreach my $item (@{$self->order->items_sorted}) {
-    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
-  }
-
-  $self->get_unalterable_data();
-  $self->pre_render();
-
-  # trigger rendering values for second row/longdescription as hidden,
-  # because they are loaded only on demand. So we need to keep the values
-  # from the source.
-  $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
-  $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
+  $self->order($self->init_order);
+  $self->reinit_after_new_order();
 
-  $self->render(
-    'delivery_order/form',
-    title => $self->get_title_for('edit'),
-    %{$self->{template_args}}
-  );
+  if ($self->order->id) {
+    $self->pre_render();
+    $self->render(
+      'delivery_order/form',
+      title => $self->get_title_for('edit'),
+      %{$self->{template_args}}
+    );
+  } else {
+    $self->action_add;
+  }
 
 }
 
 sub action_stock_in_out_dialog {
   my ($self) = @_;
 
-  my $part    = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
-  my $stock   = $::form->{stock};
-  my $unit    = $::form->{unit};
-  my $row     = $::form->{row};
-  my $item_id = $::form->{item_id};
-  my $qty     = _parse_number($::form->{qty_as_number});
+  my $part        = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
+  my $unit        = SL::DB::Unit->load_cached($::form->{unit}) or die "need unit";
+  my $stock       = $::form->{stock};
+  my $row         = $::form->{row};
+  my $item_id     = $::form->{item_id};
+  my $qty         = _parse_number($::form->{qty_as_number});
+  my $row_ui_id   = $::form->{row_ui_id};
+  my $next_button = $::form->{next_button} eq 'true';
 
-  my $inout = $self->type_data->transfer;
+  my $inout = $self->type_data->properties("transfer");
 
   my @contents   = DO->get_item_availability(parts_id => $part->id);
   my $stock_info = DO->unpack_stock_information(packed => $stock);
 
-  $self->merge_stock_data($stock_info, \@contents, $part);
+  $self->merge_stock_data($stock_info, \@contents, $part, $unit);
 
+  my $delivered = $self->order->delivered;
   $self->render("delivery_order/stock_dialog", { layout => 0 },
-    WHCONTENTS => $self->order->delivered ? $stock_info : \@contents,
-    part       => $part,
-    do_qty     => $qty,
-    do_unit    => $unit,
-    delivered  => $self->order->delivered,
-    row        => $row,
-    item_id    => $item_id,
+    WHCONTENTS  => \@contents,
+    STOCK_INFO  => $stock_info,
+    WAREHOUSES  => !$delivered ? SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]], with_objects=> ["bins",]) : [],
+    part        => $part,
+    do_qty      => $qty,
+    do_unit     => $unit->name,
+    delivered   => $self->order->delivered,
+    row         => $row,
+    item_id     => $item_id,
+    in_out      => $inout,
+    row_ui_id   => $row_ui_id,
+    next_button => $next_button,
   );
 }
 
+sub action_add_stock_in_line_to_dialog {
+  my ($self) = @_;
+
+  my $do_qty       = _parse_number($::form->{do_qty});
+  my $qty_sum   = $::form->{qty_sum};
+  my $row_count = $::form->{row_count};
+  my $part      = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
+
+  my $row_as_html = $self->p->render('delivery_order/stock_dialog/_stock_in_new_row',
+    WAREHOUSES => SL::DB::Manager::Warehouse->get_all(with_objects=> ["bins",]),
+    PART => $part,
+    pos  => $row_count + 1,
+    remaining_qty => $do_qty - $qty_sum,
+  );
+
+  $self->js->append('#stock-in-out-table tbody', $row_as_html)->render();
+}
+
 sub action_update_stock_information {
   my ($self) = @_;
 
@@ -933,17 +1033,22 @@ sub action_update_stock_information {
     stock_info => $yaml,
     stock_qty => $stock_qty,
   };
-
-  $self->render(\ SL::JSON::to_json($response), { layout => 0, type => 'json', process => 0 });
+  $self->render(
+    \ SL::JSON::to_json($response),
+    { layout => 0, type => 'json', process => 0 }
+  );
 }
 
 sub merge_stock_data {
-  my ($self, $stock_info, $contents, $part) = @_;
+  my ($self, $stock_info, $contents, $part, $unit) = @_;
   # TODO rewrite to mapping
 
   if (!$self->order->delivered) {
     for my $row (@$contents) {
-      $row->{available_qty} = _format_number_units($row->{qty}, $row->{unit}, $part->unit);
+      # row here is in parts units. stock is in item units
+      $row->{available_qty} = _format_number(
+        $part->unit_obj->convert_to($row->{qty}, $unit)
+      );
 
       for my $sinfo (@{ $stock_info }) {
         next if $row->{bin_id}       != $sinfo->{bin_id} ||
@@ -980,7 +1085,8 @@ sub action_load_second_rows {
     $self->js_load_second_row($item, $item_id, 0);
   }
 
-  $self->js->run('kivi.DeliveryOrder.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
+  # for lastcosts change-callback
+  $self->js->run('kivi.DeliveryOrder.init_row_handlers') if $self->order->is_sales;
 
   $self->js->render();
 }
@@ -997,22 +1103,7 @@ sub action_update_row_from_master_data {
     $item->description($texts->{description});
     $item->longdescription($texts->{longdescription});
 
-    my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
-
-    my $price_src;
-    if ($item->part->is_assortment) {
-    # add assortment items with price 0, as the components carry the price
-      $price_src = $price_source->price_from_source("");
-      $price_src->price(0);
-    } else {
-      $price_src = $price_source->best_price
-                 ? $price_source->best_price
-                 : $price_source->price_from_source("");
-      $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
-      $price_src->price(0) if !$price_source->best_price;
-    }
-
-
+    my ($price_src, undef) = SL::Model::Record->get_best_price_and_discount_source($self->order, $item, ignore_given => 1);
     $item->sellprice($price_src->price);
     $item->active_price_source($price_src);
 
@@ -1034,27 +1125,27 @@ sub action_update_row_from_master_data {
 }
 
 sub action_transfer_stock {
-  my ($self) = @_;
+  my ($self, $default_transfer) = @_;
 
   if ($self->order->delivered) {
-    return $self->js->flash("error", t8('The parts for this order have already been transferred'))->render;
+    return $self->js->flash("error",
+      t8('The parts for this order have already been transferred')
+    )->render;
   }
 
   my $inout = $self->type_data->properties('transfer');
 
-  my $errors = $self->save;
-
-  if (@$errors) {
-    $self->js->flash('error', $_) for @$errors;
-    return $self->js->render;
-  }
+  $self->save;
 
   my $order = $self->order;
 
   # TODO move to type data
   my $trans_type = $inout eq 'in'
-    ? SL::DB::Manager::TransferType->find_by(direction => "id", description => "stock")
-    : SL::DB::Manager::TransferType->find_by(direction => "out", description => "shipped");
+    ? SL::DB::Manager::TransferType->find_by(
+        direction => "in", description => "stock")
+    : SL::DB::Manager::TransferType->find_by(
+        direction => "out", description => "shipped");
+
 
   my @transfer_requests;
 
@@ -1062,7 +1153,10 @@ sub action_transfer_stock {
     for my $stock (@{ $item->delivery_order_stock_entries }) {
       my $transfer = SL::DB::Inventory->new_from($stock);
       $transfer->trans_type($trans_type);
+      $transfer->oe_id($order->id);
       $transfer->qty($transfer->qty * -1) if $inout eq 'out';
+      $transfer->qty($transfer->qty * 1) if $inout eq 'in';
+      $transfer->comment(t8("Default transfer delivery order")) if $default_transfer;
 
       push @transfer_requests, $transfer if defined $transfer->qty && $transfer->qty != 0;
     };
@@ -1072,19 +1166,259 @@ sub action_transfer_stock {
     return $self->js->flash("error", t8("No stock to transfer"))->render;
   }
 
+  if ($inout eq 'out') { # check stock for enough qty
+    my @missing_qtys = SL::Helper::Inventory::check_stock_out_transfer_requests(
+      transfer_requests => \@transfer_requests,
+      default_transfer  => $default_transfer,
+    );
+
+    if (scalar @missing_qtys) {
+      my $error = t8('The stock is to low: #1.',
+        join(". ", map {
+              $_->{chargenumber} && $_->{bestbefore}
+            ? t8(
+                "For #1, #2 #3 are missing of batch with chargenumber #4 and bestbefore date of #5 in bin #6",
+                $_->{part}->displayable_name,
+                $::form->format_amount(\%::myconfig, $_->{missing_qty}),
+                $_->{part}->unit,
+                $_->{chargenumber},
+                DateTime->from_ymdhms($_->{bestbefore})->to_kivitendo,
+                $_->{bin}->full_description,
+              )
+            : $_->{chargenumber}
+            ? t8(
+                "For #1, #2 #3 are missing of batch with chargenumber #4 in bin #5",
+                $_->{part}->displayable_name,
+                $::form->format_amount(\%::myconfig, $_->{missing_qty}),
+                $_->{part}->unit,
+                $_->{chargenumber},
+                $_->{bin}->full_description,
+              )
+            : $_->{bestbefore}
+            ? t8(
+                "For #1, #2 #3 are missing with a bestbefore date of #4 in bin #5",
+                $_->{part}->displayable_name,
+                $::form->format_amount(\%::myconfig, $_->{missing_qty}),
+                $_->{part}->unit,
+                DateTime->from_ymdhms($_->{bestbefore})->to_kivitendo,
+                $_->{bin}->full_description,
+              )
+            : t8(
+                "For #1, #2 #3 are missing in bin #4",
+                $_->{part}->displayable_name,
+                $::form->format_amount(\%::myconfig, $_->{missing_qty}),
+                $_->{part}->unit,
+                $_->{bin}->full_description,
+              )
+            ;
+          } @missing_qtys
+        )
+      );
+      return $self->js->flash("error", $error)->render;
+    }
+  }
+
   SL::DB->client->with_transaction(sub {
+
     $_->save for @transfer_requests;
     $self->order->update_attributes(delivered => 1);
   });
+  # update qty and stock info
+  foreach my $item (@{$self->order->items}) {
+    $self->order->prepare_stock_info($item);
+    my $stock_info_yaml = $item->{stock_info};
+    my $item_position = $item->position;
+    my $stock_qty = $self->calculate_stock_in_out($item);
+    my $unit = $item->unit;
+    $self->js->text("[data-position=$item_position] .data-stock-qty", "$stock_qty $unit");
+    my $selector = "[data-position=$item_position] .data-stock-info";
+    $self->js->val($selector, $stock_info_yaml);
+  }
 
   $self->js
     ->flash("info", t8("Stock transfered"))
-    ->run('kivi.ActionBar.setDisabled', '#transfer_out_action', t8('The parts for this order have already been transferred'))
-    ->run('kivi.ActionBar.setDisabled', '#transfer_in_action', t8('The parts for this order have already been transferred'))
-    ->run('kivi.ActionBar.setDisabled', '#delete_action', t8('The parts for this order have already been transferred'))
-    ->replaceWith('#data-status-line', delivery_order_status_line($self->order))
+    ->run('kivi.ActionBar.setDisabled', '#save_action',
+          t8('This record has already been delivered.'))
+    ->run('kivi.ActionBar.setDisabled', '#save_and_close',
+          t8('This record has already been delivered.'))
+    ->run('kivi.ActionBar.setDisabled', '#transfer_out_action',
+          t8('The parts for this order have already been transferred'))
+    ->run('kivi.ActionBar.setDisabled', '#transfer_out_default_action',
+          t8('The parts for this order have already been transferred'))
+    ->run('kivi.ActionBar.setDisabled', '#transfer_in_action',
+          t8('The parts for this order have already been transferred'))
+    ->run('kivi.ActionBar.setDisabled', '#transfer_in_default_action',
+          t8('The parts for this order have already been transferred'))
+    ->run('kivi.ActionBar.setDisabled', '#delete_action',
+          t8('The parts for this order have already been transferred'))
+    ->run('kivi.ActionBar.setEnabled', '#undo_transfer_action',
+          t8('The parts for this order have already been transferred'))
+    ->html('#data-status-line', delivery_order_status_line($self->order))
     ->render;
+}
+
+sub action_transfer_stock_default {
+  my ($self) = @_;
+  my $delivery_order = $self->order;
+  my @items = @{$delivery_order->items_sorted};
+
+  # get default bin if set in config
+  my ($default_warehouse_id, $default_bin_id);
+  if ($::instance_conf->get_transfer_default_use_master_default_bin) {
+    $default_warehouse_id = $::instance_conf->get_warehouse_id;
+    $default_bin_id       = $::instance_conf->get_bin_id;
+  }
+
+  my @transfer_requests = ();
+  my %parts_qty = ();
+  my %units_by_name = map { $_->name => $_ } @{ SL::DB::Manager::Unit->get_all };
+  foreach my $item (@items) {
+    my $part = $item->part;
+    my $base_unit_factor = $units_by_name{$part->unit}->factor || 1;
+    my $item_unit_factor = $units_by_name{$item->unit}->factor || 1;
+    my $qty = $item->qty * $item_unit_factor / $base_unit_factor;
+    return $self->js->flash('error', t8('Cannot transfer negative entries.'))->render() if $qty < 0;
+    $qty = 0 if (!$::instance_conf->get_transfer_default_services && $part->is_service);
+
+    $parts_qty{$part->id} += $qty if $qty;
+    push @transfer_requests, {
+      'warehouse_id'        => $part->warehouse_id || $default_warehouse_id,
+      'bin_id'              => $part->bin_id       || $default_bin_id,
+      'unit'                => $item->unit,
+      'qty'                 => $qty,
+      # added in check transfer_request out direction if possible
+      'chargenumber'        => undef, # $item->serialnumber, # Is not used in delivery order
+      'bestbefore'          => undef, # $item->bestbefore,   # Is not used in delivery order
+    }
+  }
+
+  # check transfer_requests are correctly
+  my %parts_errors = (); # missing_bin, missing_qty, multiple_options
+  my $grouped_qty_query = qq|
+    SELECT SUM(qty) as qty, chargenumber, bestbefore
+    FROM inventory
+    WHERE parts_id = ? AND bin_id = ?
+    GROUP BY chargenumber, bestbefore
+  |;
+  my $dbh = $self->order->dbh;
+  my $in_out_direction = $delivery_order->type_data->properties('transfer');
+  for my $idx (0 .. scalar @transfer_requests - 1) {
+    my $transfer_request = $transfer_requests[$idx];
+    next unless $transfer_request->{qty}; # empty request
+    my $item = $items[$idx];
+    my $part_id = $item->parts_id;
+    my $bin_id  = $transfer_request->{bin_id};
+    $parts_errors{$part_id}{missing_bin} = 1 unless $bin_id;
+    next                                     unless $bin_id;
+    if ($in_out_direction eq 'out') {
+      my @grouped_qty = selectall_hashref_query(
+        $::form, $dbh, $grouped_qty_query, $part_id, $bin_id);
+
+      if (1 < scalar grep {$_->{qty} != 0} @grouped_qty) {
+        $parts_errors{$part_id}{multiple_options} = 1;
+      }
+      my $max_qty = sum0(map {$_->{qty}} @grouped_qty);
+      if ($max_qty < $parts_qty{$part_id}) {
+        $parts_errors{$part_id}{missing_qty} = $parts_qty{$part_id} - $max_qty;
+        $parts_errors{$part_id}{bin_id}      = $bin_id;
+      }
+
+      next if $parts_errors{$part_id};
+      # find correct chargenumber and bestbefore
+      my $stock_info = first {$_->{qty} >= $transfer_request->{qty}} @grouped_qty;
+      $transfer_request->{chargenumber} = $stock_info->{chargenumber};
+      $transfer_request->{bestbefore}   = $stock_info->{bestbefore};
+    }
+  }
+
+  # auslagern soll immer gehen, auch wenn nicht genügend auf lager ist.
+  # der lagerplatz ist hier extra konfigurierbar, bspw. Lager-Korrektur mit
+  # Lagerplatz Lagerplatz-Korrektur
+  my $default_warehouse_id_ignore_onhand = $::instance_conf->get_warehouse_id_ignore_onhand;
+  my $default_bin_id_ignore_onhand       = $::instance_conf->get_bin_id_ignore_onhand;
+  if ($::instance_conf->get_transfer_default_ignore_onhand && $default_bin_id_ignore_onhand) {
+    foreach my $part_id (keys %parts_errors) {
+      # entsprechende defaults holen
+      # falls chargenumber, bestbefore oder anzahl nicht stimmt, auf automatischen
+      # lagerplatz wegbuchen!
+      for my $idx (0 .. scalar @transfer_requests - 1) {
+        my $transfer_request = $transfer_requests[$idx];
+        next unless $transfer_request->{qty}; # empty request
+
+        if ($items[$idx]->parts_id eq $part_id){
+          $transfer_request->{bin_id}        = $default_bin_id_ignore_onhand;
+          $transfer_request->{warehouse_id}  = $default_warehouse_id_ignore_onhand;
+        }
+      }
+      delete %parts_errors{$part_id};
+    }
+  }
+
+  # render errors
+  if (scalar keys %parts_errors) {
+    my @multiple_options = ();
+    foreach my $part_id (keys %parts_errors) {
+      my $part = SL::DB::Part->new(id => $part_id)->load();
+      if ($parts_errors{$part_id}{missing_bin}){
+        $self->js->error(t8('No standard bin set for #1.', $part->displayable_name));
+      }
+      if ($parts_errors{$part_id}{missing_qty}) {
+        my $bin = SL::DB::Manager::Bin->find_by(
+          id => $parts_errors{$part_id}{bin_id}
+        );
+        $self->js->error(
+          t8('There are #1 of "#2" missing from the bin #3 for transfer.',
+            $parts_errors{$part_id}{missing_qty}, $part->displayable_name, $bin->full_description));
+      }
+      if ($parts_errors{$part_id}{multiple_options}){
+        push @multiple_options, $part;
+      }
+    }
+    if (scalar @multiple_options) {
+        $self->js->error(t8(
+            "There are parts with multiple chargenumbers or bestbefore dates set. This can't be decided automatically. Pleas transfer this delivery order manually. Can't decided for #1.",
+            join ", ", map {$_->displayable_name} @multiple_options)
+        );
+    }
+    return $self->js->render();
+  }
+
+  # assign each delivery_order_item it's stock
+  for my $idx (0 .. scalar @transfer_requests - 1) {
+    my %transfer_request = %{$transfer_requests[$idx]};
+    next unless $transfer_request{qty}; # empty request
+
+    my $item = $items[$idx];
+    my @stocks = (SL::DB::DeliveryOrderItemsStock->new(%transfer_request));
+    $item->delivery_order_stock_entries(@stocks);
+  }
+
+  my $default_transfer = 1;
+  $self->action_transfer_stock($default_transfer);
+}
+
+sub action_undo_transfers {
+  my ( $self ) = @_;
+
+  SL::DB->client->with_transaction(sub {
+    foreach my $item (@{$self->order->orderitems}) {
+      foreach my $inv_item (@{ $item->delivery_order_stock_entries}) {
+        $inv_item->inventory->delete;
+        $inv_item->delete;
+      }
+    }
+    $self->order->update_attributes(delivered => 0);
+    $self->order->update_attributes(closed => 0);
+  });
 
+  flash_later('info', t8("Transfer undone"));
+  my @redirect_params = (
+    action => 'edit',
+    type   => $self->type,
+    id     => $self->order->id,
+  );
+
+  $self->redirect_to(@redirect_params);
 }
 
 sub js_load_second_row {
@@ -1148,7 +1482,8 @@ sub js_reset_order_and_item_ids_after_save {
 
   $self->js
     ->val('#id', $self->order->id)
-    ->val('#converted_from_oe_id', '')
+    ->val('#converted_from_record_type_ref', '')
+    ->val('#converted_from_record_id',  '')
     ->val('#order_' . $self->nr_key(), $self->order->number);
 
   my $idx = 0;
@@ -1156,13 +1491,17 @@ sub js_reset_order_and_item_ids_after_save {
     next if !$self->order->items_sorted->[$idx]->id;
     next if $form_item_id !~ m{^new};
     $self->js
-      ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
+      ->val (
+        '[name="orderitem_ids[+]"][value="' . $form_item_id . '"]',
+        $self->order->items_sorted->[$idx]->id)
       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
-      ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
+      ->attr('#item_' . $form_item_id, "id",
+        'item_' . $self->order->items_sorted->[$idx]->id);
   } continue {
     $idx++;
   }
-  $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
+  $self->js->val('[name="converted_from_record_item_type_refs[+]"]', '');
+  $self->js->val('[name="converted_from_record_item_ids[+]"]', '');
 }
 
 #
@@ -1172,17 +1511,18 @@ sub js_reset_order_and_item_ids_after_save {
 sub init_type {
   my ($self) = @_;
 
-  if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
+  my $type = $self->order->record_type;
+  if (none { $type eq $_ } @{$self->valid_types}) {
     die "Not a valid type for delivery order";
   }
 
-  $self->type($::form->{type});
+  $self->type($type);
 }
 
 sub init_cv {
   my ($self) = @_;
 
-  return $self->type_data->customervendor;
+  return $self->type_data->properties("customervendor");
 }
 
 sub init_search_cvpartnumber {
@@ -1210,20 +1550,24 @@ sub init_order {
   $_[0]->make_order;
 }
 
-sub init_all_price_factors {
-  SL::DB::Manager::PriceFactor->get_all;
-}
-
 sub init_part_picker_classification_ids {
   my ($self)    = @_;
 
-  return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => $self->type_data->part_classification_query) } ];
+  return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(
+    where => $self->type_data->part_classification_query
+    ) } ];
 }
 
 sub check_auth {
   my ($self) = @_;
 
-  $::auth->assert($self->type_data->access || 'DOES_NOT_EXIST');
+  $::auth->assert($self->type_data->rights('view') || 'DOES_NOT_EXIST');
+}
+
+sub check_auth_for_edit {
+  my ($self) = @_;
+
+  $::auth->assert($self->type_data->rights('edit') || 'DOES_NOT_EXIST');
 }
 
 # build the selection box for contacts
@@ -1248,7 +1592,11 @@ sub build_shipto_select {
   my ($self) = @_;
 
   select_tag('order.shipto_id',
-             [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
+             [ {
+                 displayable_id => t8("No/individual shipping address"),
+                 shipto_id => ''
+               },
+               $self->order->{$self->cv}->shipto ],
              value_key  => 'shipto_id',
              title_key  => 'displayable_id',
              default    => $self->order->shipto_id,
@@ -1288,11 +1636,7 @@ sub load_order {
 
   $self->order(SL::DB::DeliveryOrder->new(id => $::form->{id})->load);
 
-  # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
-  # You need a custom shipto object to call cvars_by_config to get the cvars.
-  $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
-
-  $self->prepare_stock_info($_) for $self->order->items;
+  $self->reinit_after_new_order();
 
   return $self->order;
 }
@@ -1310,19 +1654,38 @@ sub make_order {
   # be retrieved via items until the order is saved. Adding empty items to new
   # order here solves this problem.
   my $order;
-  $order   = SL::DB::DeliveryOrder->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
-  $order ||= SL::DB::DeliveryOrder->new(orderitems  => [], currency_id => $::instance_conf->get_currency_id(), order_type => $self->type_data->validate($::form->{type}));
+  if ($::form->{id}) {
+    $order = SL::DB::DeliveryOrder->new(
+      id => $::form->{id}
+    )->load(
+      with => [
+        'orderitems',
+        'orderitems.part',
+      ]
+    );
+  } else {
+    $order = SL::DB::DeliveryOrder->new(
+      orderitems  => [],
+      currency_id => $::instance_conf->get_currency_id(),
+      record_type => $::form->{type}
+    );
+    $order = SL::Model::Record->update_after_new($order);
+  }
 
-  my $cv_id_method = $self->cv . '_id';
+  my $cv_id_method = $order->type_data->properties('customervendor'). '_id';
   if (!$::form->{id} && $::form->{$cv_id_method}) {
     $order->$cv_id_method($::form->{$cv_id_method});
-    setup_order_from_cv($order);
+    $order = SL::Model::Record->update_after_customer_vendor_change($order);
   }
 
-  my $form_orderitems                  = delete $::form->{order}->{orderitems};
+  # don't assign hashes as objects
+  my $form_orderitems = delete $::form->{order}->{orderitems};
 
   $order->assign_attributes(%{$::form->{order}});
 
+  # restore form values
+  $::form->{order}->{orderitems} = $form_orderitems;
+
   $self->setup_custom_shipto_from_form($order, $::form);
 
   # remove deleted items
@@ -1344,8 +1707,6 @@ sub make_order {
     $pos++;
   }
 
-  $self->prepare_stock_info($_) for $order->items, @items;
-
   $order->add_items(grep {!$_->id} @items);
 
   return $order;
@@ -1417,36 +1778,11 @@ sub new_item {
 
   $item->assign_attributes(%$attr);
 
-  my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
-  my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
-
+  my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
+  $item->qty(1.0)          if !$item->qty;
   $item->unit($part->unit) if !$item->unit;
 
-  my $price_src;
-  if ( $part->is_assortment ) {
-    # add assortment items with price 0, as the components carry the price
-    $price_src = $price_source->price_from_source("");
-    $price_src->price(0);
-  } elsif (defined $item->sellprice) {
-    $price_src = $price_source->price_from_source("");
-    $price_src->price($item->sellprice);
-  } else {
-    $price_src = $price_source->best_price
-               ? $price_source->best_price
-               : $price_source->price_from_source("");
-    $price_src->price(0) if !$price_source->best_price;
-  }
-
-  my $discount_src;
-  if (defined $item->discount) {
-    $discount_src = $price_source->discount_from_source("");
-    $discount_src->discount($item->discount);
-  } else {
-    $discount_src = $price_source->best_discount
-                  ? $price_source->best_discount
-                  : $price_source->discount_from_source("");
-    $discount_src->discount(0) if !$price_source->best_discount;
-  }
+  my ($price_src, $discount_src) = SL::Model::Record->get_best_price_and_discount_source($record, $item, ignore_given => 0);
 
   my %new_attr;
   $new_attr{part}                   = $part;
@@ -1466,44 +1802,17 @@ sub new_item {
   # saved. Adding empty custom_variables to new orderitem here solves this problem.
   $new_attr{custom_variables} = [];
 
-  my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
+  my $texts = get_part_texts(
+    $part, $record->language_id,
+    description => $new_attr{description},
+    longdescription => $new_attr{longdescription}
+  );
 
   $item->assign_attributes(%new_attr, %{ $texts });
 
   return $item;
 }
 
-sub prepare_stock_info {
-  my ($self, $item) = @_;
-
-  $item->{stock_info} = SL::YAML::Dump([
-    map +{
-      delivery_order_items_stock_id => $_->id,
-      qty                           => $_->qty,
-      warehouse_id                  => $_->warehouse_id,
-      bin_id                        => $_->bin_id,
-      chargenumber                  => $_->chargenumber,
-      unit                          => $_->unit,
-    }, $item->delivery_order_stock_entries
-  ]);
-}
-
-sub setup_order_from_cv {
-  my ($order) = @_;
-
-  $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
-
-  $order->intnotes($order->customervendor->notes);
-
-  if ($order->is_sales) {
-    $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
-    $order->taxincluded(defined($order->customer->taxincluded_checked)
-                        ? $order->customer->taxincluded_checked
-                        : $::myconfig{taxincluded_checked});
-  }
-
-}
-
 # setup custom shipto from form
 #
 # The dialog returns form variables starting with 'shipto' and cvars starting
@@ -1518,10 +1827,22 @@ sub setup_custom_shipto_from_form {
   if ($order->shipto) {
     $self->is_custom_shipto_to_delete(1);
   } else {
-    my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
-
-    my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
-    my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
+    my $custom_shipto =
+         $order->custom_shipto
+      || $order->custom_shipto(
+           SL::DB::Shipto->new(module => 'DO', custom_variables => [])
+         );
+
+    my $shipto_cvars  = {
+      map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}}
+      grep { m{^shiptocvar_} }
+      keys %$form
+    };
+    my $shipto_attrs  = {
+      map { $_ => delete $form->{$_}}
+      grep { m{^shipto} }
+      keys %$form
+    };
 
     $custom_shipto->assign_attributes(%$shipto_attrs);
     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
@@ -1544,187 +1865,79 @@ sub get_unalterable_data {
   }
 }
 
-# delete the order
-#
-# And remove related files in the spool directory
-sub delete {
-  my ($self) = @_;
-
-  my $errors = [];
-  my $db     = $self->order->db;
-
-  $db->with_transaction(
-    sub {
-      my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
-      $self->order->delete;
-      my $spool = $::lx_office_conf{paths}->{spool};
-      unlink map { "$spool/$_" } @spoolfiles if $spool;
-
-      $self->save_history('DELETED');
-
-      1;
-  }) || push(@{$errors}, $db->error);
-
-  return $errors;
-}
-
 # save the order
 #
 # And delete items that are deleted in the form.
 sub save {
   my ($self) = @_;
 
-  my $errors = [];
-  my $db     = $self->order->db;
-
-  $db->with_transaction(sub {
-    # delete custom shipto if it is to be deleted or if it is empty
-    if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
-      $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
-      $self->order->custom_shipto(undef);
-    }
-
-    SL::DB::DeliveryOrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
-    $self->order->save(cascade => 1);
-
-    # link records
-    if ($::form->{converted_from_oe_id}) {
-      my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
-      foreach my $converted_from_oe_id (@converted_from_oe_ids) {
-        my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
-        $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/ && $self->order->is_type(PURCHASE_DELIVERY_ORDER_TYPE);
-        $src->link_to_record($self->order);
-      }
-      if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
-        my $idx = 0;
-        foreach (@{ $self->order->items_sorted }) {
-          my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
-          next if !$from_id;
-          SL::DB::RecordLink->new(from_table => 'orderitems',
-                                  from_id    => $from_id,
-                                  to_table   => 'orderitems',
-                                  to_id      => $_->id
-          )->save;
-          $idx++;
-        }
-      }
-    }
+  set_record_link_conversions($self->order,
+    delete $::form->{RECORD_TYPE_REF()}
+      => delete $::form->{RECORD_ID()},
+    delete $::form->{RECORD_ITEM_TYPE_REF()}
+      => delete $::form->{RECORD_ITEM_ID()},
+  );
 
-    $self->save_history('SAVED');
+  my $items_to_delete  = scalar @{ $self->item_ids_to_delete || [] }
+                       ? SL::DB::Manager::DeliveryOrderItem->get_all(where => [id => $self->item_ids_to_delete])
+                       : undef;
+
+  SL::Model::Record->save($self->order,
+    with_validity_token        => {
+      scope => SL::DB::ValidityToken::SCOPE_DELIVERY_ORDER_SAVE(),
+      token => $::form->{form_validity_token}
+    },
+    delete_custom_shipto       => $self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty),
+    items_to_delete            => $items_to_delete,
+  );
 
-    1;
-  }) || push(@{$errors}, $db->error);
+  if ($::form->{email_journal_id}) {
+    my $email_journal = SL::DB::EmailJournal->new(
+      id => delete $::form->{email_journal_id}
+    )->load;
+    $email_journal->link_to_record_with_attachment(
+      $self->order,
+      delete $::form->{email_attachment_id}
+    );
+  }
 
-  return $errors;
+  delete $::form->{form_validity_token};
 }
 
-sub workflow_sales_or_request_for_quotation {
+sub reinit_after_new_order {
   my ($self) = @_;
 
-  # always save
-  my $errors = $self->save();
-
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) for @{ $errors };
-    return $self->js->render();
-  }
-
-  my $destination_type = $self->type_data->workflow("to_quotation_type");
-
-  $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
-  $self->{converted_from_oe_id} = delete $::form->{id};
-
-  # set item ids to new fake id, to identify them as new items
-  foreach my $item (@{$self->order->items_sorted}) {
-    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
-  }
-
   # change form type
-  $::form->{type} = $destination_type;
+  $::form->{type} = $self->order->type;
   $self->type($self->init_type);
-  $self->cv  ($self->init_cv);
+  $self->type_data($self->init_type_data);
+  $self->cv($self->init_cv);
   $self->check_auth;
 
-  $self->get_unalterable_data();
-  $self->pre_render();
+  $self->setup_custom_shipto_from_form($self->order, $::form);
 
-  # trigger rendering values for second row as hidden, because they
-  # are loaded only on demand. So we need to keep the values from the
-  # source.
-  $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
-
-  $self->render(
-    'delivery_order/form',
-    title => $self->get_title_for('edit'),
-    %{$self->{template_args}}
-  );
-}
-
-sub workflow_sales_or_purchase_order {
-  my ($self) = @_;
-
-  # always save
-  my $errors = $self->save();
-
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
-  }
-
-  my $destination_type = $self->type_data->workflow("to_order_type");
-
-  # check for direct delivery
-  # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
-  my $custom_shipto;
-  if ($self->type_data->workflow("to_order_copy_shipto") && $::form->{use_shipto} && $self->order->shipto) {
-    $custom_shipto = $self->order->shipto->clone('SL::DB::DeliveryOrder');
-  }
-
-  $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
-  $self->{converted_from_oe_id} = delete $::form->{id};
-
-  # set item ids to new fake id, to identify them as new items
   foreach my $item (@{$self->order->items_sorted}) {
+    # set item ids to new fake id, to identify them as new items
     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
-  }
 
-  if ($self->type_data->workflow("to_order_copy_shipto")) {
-    if ($::form->{use_shipto}) {
-      $self->order->custom_shipto($custom_shipto) if $custom_shipto;
-    } else {
-      # remove any custom shipto if not wanted
-      $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
-    }
+    # trigger rendering values for second row as hidden, because they
+    # are loaded only on demand. So we need to keep the values from the
+    # source.
+    $item->{render_second_row} = 1;
   }
 
-  # change form type
-  $::form->{type} = $destination_type;
-  $self->type($self->init_type);
-  $self->cv  ($self->init_cv);
-  $self->check_auth;
-
+  $self->order->prepare_stock_info($_) for $self->order->items;
   $self->get_unalterable_data();
-  $self->pre_render();
-
-  # trigger rendering values for second row as hidden, because they
-  # are loaded only on demand. So we need to keep the values from the
-  # source.
-  $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
-
-  $self->render(
-    'delivery_order/form',
-    title => $self->get_title_for('edit'),
-    %{$self->{template_args}}
-  );
 }
 
+
 sub pre_render {
   my ($self) = @_;
 
   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
-  $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted();
+  $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
                                                                                               deleted => 0 ] ],
                                                                            sort_by => 'name');
@@ -1757,7 +1970,7 @@ sub pre_render {
     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
   }
 
-  if ($self->order->${\ $self->type_data->nr_key } && $::instance_conf->get_webdav) {
+  if ($self->order->${\ $self->type_data->properties("nr_key") } && $::instance_conf->get_webdav) {
     my $webdav = SL::Webdav->new(
       type     => $self->type,
       number   => $self->order->number,
@@ -1769,12 +1982,15 @@ sub pre_render {
                                                 } } @all_objects;
   }
 
-  $self->{template_args}{in_out} = $self->type_data->transfer;
+  $self->{template_args}{in_out}                                 = $self->type_data->properties("transfer");
+  $self->{template_args}{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
 
   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
-  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.DeliveryOrder kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
-                                                         calculate_qty kivi.Validator follow_up show_history);
+  $::request->{layout}->use_javascript("${_}.js") for qw(
+    kivi.SalesPurchase kivi.DeliveryOrder kivi.File calculate_qty kivi.Validator
+    follow_up show_history
+    );
   $self->setup_edit_action_bar;
 }
 
@@ -1782,20 +1998,66 @@ sub setup_edit_action_bar {
   my ($self, %params) = @_;
 
   my $deletion_allowed = $self->type_data->show_menu("delete");
+  my $may_edit_create  = $::auth->assert(
+    $self->type_data->rights('edit') || 'DOES_NOT_EXIST', 1
+  );
+
+  my $confirmation_on_workflow = $self->order->delivered ? undef
+    : ( $self->order->is_sales && $::instance_conf->get_sales_delivery_order_check_stocked)    ? t8('This record has not been stocked out. Proceed?')
+    : (!$self->order->is_sales && $::instance_conf->get_purchase_delivery_order_check_stocked) ? t8('This record has not been stocked in. Proceed?')
+    : undef;
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
       combobox => [
         action => [
           t8('Save'),
-          call      => [ 'kivi.DeliveryOrder.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
-                                                    $::instance_conf->get_order_warn_no_deliverydate,
-                                                                                                      ],
+          id       => 'save_action',
+          call     => [ 'kivi.DeliveryOrder.save', {
+              action             => 'save',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+            }],
+          disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
+                    : $self->order->delivered ? t8('This record has already been delivered.')
+                    :                           undef,
+        ],
+        action => [
+          t8('Save and Close'),
+          id       => 'save_and_close',
+          call     => [ 'kivi.DeliveryOrder.save', {
+              action             => 'save',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+              form_params        => [
+                { name => 'back_to_caller', value => 1 },
+              ],
+            }],
+          disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
+                    : $self->order->delivered ? t8('This record has already been delivered.')
+                    :                           undef,
+        ],
+        action => [
+          t8('Mark as closed'),
+          id       => 'close_order',
+          call     => [ 'kivi.DeliveryOrder.close_order' ],
+          confirm  => t8('This will remove the delivery order from showing as open even if contents are not delivered. Proceed?'),
+          disabled => !$may_edit_create    ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id    ? t8('This object has not been saved yet.')
+                    : $self->order->closed ? t8('This record has already been closed.')
+                    :                        undef,
         ],
         action => [
           t8('Save as new'),
-          call      => [ 'kivi.DeliveryOrder.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
-          disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          call     => [ 'kivi.DeliveryOrder.save', {
+              action             => 'save_as_new',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+            }],
+          disabled => !$may_edit_create                        ? t8('You do not have the permissions to access this function.')
+                    : $self->type eq 'supplier_delivery_order' ? t8('Need a workflow for Supplier Delivery Order')
+                    : $self->type eq 'rma_delivery_order'      ? t8('Need a workflow for RMA Delivery Order.')
+                    : !$self->order->id                        ? t8('This object has not been saved yet.')
+                    :                                            undef,
         ],
       ], # end of combobox "Save"
 
@@ -1804,41 +2066,33 @@ sub setup_edit_action_bar {
           t8('Workflow'),
         ],
         action => [
-          t8('Save and Quotation'),
-          submit   => [ '#order_form', { action => "DeliveryOrder/sales_quotation" } ],
-          only_if  => $self->type_data->show_menu("save_and_quotation"),
-        ],
-        action => [
-          t8('Save and RFQ'),
-          submit   => [ '#order_form', { action => "DeliveryOrder/request_for_quotation" } ],
-          only_if  => $self->type_data->show_menu("save_and_rfq"),
-        ],
-        action => [
-          t8('Save and Sales Order'),
-          submit   => [ '#order_form', { action => "DeliveryOrder/sales_order" } ],
-          only_if  => $self->type_data->show_menu("save_and_sales_order"),
-        ],
-        action => [
-          t8('Save and Purchase Order'),
-          call      => [ 'kivi.DeliveryOrder.purchase_order_check_for_direct_delivery' ],
-          only_if  => $self->type_data->show_menu("save_and_purchase_order"),
-        ],
-        action => [
-          t8('Save and Delivery Order'),
-          call      => [ 'kivi.DeliveryOrder.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
-                                                                       $::instance_conf->get_order_warn_no_deliverydate,
-                                                                                                                        ],
-          only_if  => $self->type_data->show_menu("save_and_delivery_order"),
-        ],
-        action => [
-          t8('Save and Invoice'),
-          call      => [ 'kivi.DeliveryOrder.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
-          only_if  => $self->type_data->show_menu("save_and_invoice"),
+          t8('Create Invoice'),
+          call     => [ 'kivi.DeliveryOrder.save', {
+              action             => 'workflow_invoice',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+            }],
+          only_if  => $self->type_data->show_menu("workflow_invoice"),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id ? t8('This object has not been saved yet.')
+                    : undef,
+          confirm  =>   $confirmation_on_workflow,
         ],
         action => [
-          t8('Save and AP Transaction'),
-          call      => [ 'kivi.DeliveryOrder.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
-          only_if  => $self->type_data->show_menu("save_and_ap_transaction"),
+          t8('Create Reclamation'),
+          call      => [ 'kivi.DeliveryOrder.save', {
+              action             => 'workflow_new_record',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              form_params        => [
+                { name => 'to_type',
+                  value => $self->order->is_sales ? SALES_RECLAMATION_TYPE()
+                                                  : PURCHASE_RECLAMATION_TYPE() },
+              ],
+            }],
+          only_if  => $self->type_data->show_menu('workflow_reclamation'),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id ? t8('This object has not been saved yet.')
+                    : undef,
+          confirm  => $confirmation_on_workflow,
         ],
 
       ], # end of combobox "Workflow"
@@ -1849,28 +2103,39 @@ sub setup_edit_action_bar {
         ],
         action => [
           t8('Save and preview PDF'),
-           call => [ 'kivi.DeliveryOrder.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
-                                                       $::instance_conf->get_order_warn_no_deliverydate,
-                                                                                                         ],
+           call    => [ 'kivi.DeliveryOrder.save', {
+               action             => 'preview_pdf',
+               warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+               warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+             }],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
         ],
         action => [
           t8('Save and print'),
-          call => [ 'kivi.DeliveryOrder.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
-                                                     $::instance_conf->get_order_warn_no_deliverydate,
-                                                                                                      ],
+          call     => [ 'kivi.DeliveryOrder.show_print_options', {
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate },
+          ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
         ],
         action => [
           t8('Save and E-mail'),
-          id   => 'save_and_email_action',
-          call => [ 'kivi.DeliveryOrder.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
-                                                                     $::instance_conf->get_order_warn_no_deliverydate,
-                  ],
-          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          id       => 'save_and_email_action',
+          call     => [ 'kivi.DeliveryOrder.save', {
+              action             => 'save_and_show_email_dialog',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+            }],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id ? t8('This object has not been saved yet.')
+                    :                     undef,
         ],
         action => [
           t8('Download attachments of all parts'),
           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
-          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id ? t8('This object has not been saved yet.')
+                    :                     undef,
           only_if  => $::instance_conf->get_doc_storage,
         ],
       ], # end of combobox "Export"
@@ -1880,30 +2145,78 @@ sub setup_edit_action_bar {
         id       => 'delete_action',
         call     => [ 'kivi.DeliveryOrder.delete_order' ],
         confirm  => $::locale->text('Do you really want to delete this object?'),
-        disabled => !$self->order->id       ? t8('This object has not been saved yet.') :
-                    $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
+        disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
+                  : !$self->order->id       ? t8('This object has not been saved yet.')
+                  : $self->order->delivered ? t8('The parts for this order have already been transferred')
+                  :                           undef,
         only_if  => $self->type_data->show_menu("delete"),
       ],
 
       combobox => [
         action => [
           t8('Transfer out'),
-          id   => 'transfer_out_action',
-          call   => [ 'kivi.DeliveryOrder.save', 'transfer_stock' ],
-          disabled => !$self->order->id ? t8('This object has not been saved yet.') :
-                      $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
-          only_if => $self->type_data->properties('transfer') eq 'out',
+          id       => 'transfer_out_action',
+          call     => [ 'kivi.DeliveryOrder.save', {
+              action => 'transfer_stock',
+            }],
+          disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id       ? t8('This object has not been saved yet.')
+                    : $self->order->delivered ? t8('The parts for this order have already been transferred')
+                    :                           undef,
+          only_if  => $self->type_data->properties('transfer') eq 'out',
+          confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
+        ],
+        action => [
+          t8('Transfer out via default'),
+          id       => 'transfer_out_default_action',
+          call     => [ 'kivi.DeliveryOrder.save', {
+              action => 'transfer_stock_default',
+            }],
+          disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id       ? t8('This object has not been saved yet.')
+                    : $self->order->delivered ? t8('The parts for this order have already been transferred')
+                    :                           undef,
+          only_if  => $self->type_data->properties('transfer') eq 'out',
           confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
         ],
         action => [
           t8('Transfer in'),
-          id   => 'transfer_in_action',
-          call   => [ 'kivi.DeliveryOrder.save', 'transfer_stock' ],
-          disabled => !$self->order->id ? t8('This object has not been saved yet.') :
-                      $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
-          only_if => $self->type_data->properties('transfer') eq 'in',
+          id       => 'transfer_in_action',
+          call     => [ 'kivi.DeliveryOrder.save', {
+              action => 'transfer_stock',
+            }],
+          disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id       ? t8('This object has not been saved yet.')
+                    : $self->order->delivered ? t8('The parts for this order have already been transferred')
+                    :                           undef,
+          only_if  => $self->type_data->properties('transfer') eq 'in',
+          confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
+        ],
+        action => [
+          t8('Transfer in via default'),
+          id       => 'transfer_in_default_action',
+          call     => [ 'kivi.DeliveryOrder.save', {
+              action => 'transfer_stock_default',
+            }],
+          disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id       ? t8('This object has not been saved yet.')
+                    : $self->order->delivered ? t8('The parts for this order have already been transferred')
+                    :                           undef,
+          only_if  => $self->type_data->properties('transfer') eq 'in',
           confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
         ],
+        action => [
+          t8('Undo Transfer'),
+          id       => 'undo_transfer_action',
+          call     => [ 'kivi.DeliveryOrder.save', {
+              action => 'undo_transfers',
+            }],
+          disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id       ? t8('This object has not been saved yet.')
+                    : undef,
+          disabled => !$self->order->delivered,
+          confirm => t8('Do you really want undo transfers the stock and set this order to undelivered?'),
+        ],
       ],
 
       combobox => [
@@ -1960,7 +2273,10 @@ sub generate_pdf {
   );
 
   if (!defined $template_file) {
-    push @errors, $::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files);
+    push @errors, $::locale->text(
+      'Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.',
+      join ', ', map { "'$_'"} @template_files
+    );
   }
 
   return @errors if scalar @errors;
@@ -2034,10 +2350,14 @@ sub get_item_cvpartnumber {
   return if !$self->order->customervendor;
 
   if ($self->cv eq 'vendor') {
-    my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
+    my @mms =
+      grep { $_->make eq $self->order->customervendor->id }
+      @{$item->part->makemodels};
     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
   } elsif ($self->cv eq 'customer') {
-    my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
+    my @cps =
+      grep { $_->customer_id eq $self->order->customervendor->id }
+      @{$item->part->customerprices};
     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
   }
 }
@@ -2067,22 +2387,7 @@ sub get_part_texts {
 }
 
 sub nr_key {
-  return $_[0]->type_data->nr_key;
-}
-
-sub save_and_redirect_to {
-  my ($self, %params) = @_;
-
-  my $errors = $self->save();
-
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
-  }
-
-  flash_later('info', $self->type_data->text("saved"));
-
-  $self->redirect_to(%params, id => $self->order->id);
+  return $_[0]->type_data->properties("nr_key");
 }
 
 sub save_history {
@@ -2101,7 +2406,7 @@ sub save_history {
 }
 
 sub store_pdf_to_webdav_and_filemanagement {
-  my($order, $content, $filename) = @_;
+  my($order, $content, $filename, $variant) = @_;
 
   my @errors;
 
@@ -2130,7 +2435,8 @@ sub store_pdf_to_webdav_and_filemanagement {
                      source        => 'created',
                      file_type     => 'document',
                      file_name     => $filename,
-                     file_contents => $content);
+                     file_contents => $content,
+                     print_variant => $variant);
       1;
     } or do {
       push @errors, t8('Storing PDF in storage backend failed: #1', $@);
@@ -2151,7 +2457,7 @@ sub calculate_stock_in_out_from_stock_info {
     $units_by_name{$_->{unit}}->convert_to($_->{qty}, $units_by_name{$unit})
   } @$stock_info;
 
-  my $content  = _format_number_units($sum, 2, $units_by_name{$unit}, $units_by_name{$unit});
+  my $content  = _format_number($sum, 2) . ' ' . $unit;
 
   return $content;
 }
@@ -2165,13 +2471,14 @@ sub calculate_stock_in_out {
     $_->unit_obj->convert_to($_->qty, $item->unit_obj)
   } $item->delivery_order_stock_entries;
 
-  my $content  = _format_number_units($sum, 2, $item->unit_obj, $item->part->unit_obj);
+  my $content  = _format_number($sum, 2);
 
   return $content;
 }
 
 sub init_type_data {
-  SL::Controller::DeliveryOrder::TypeData->new($_[0]);
+  my ($self) = @_;
+  SL::DB::Helper::TypeDataProxy->new('SL::DB::DeliveryOrder', $self->order->record_type);
 }
 
 sub init_valid_types {
@@ -2369,7 +2676,7 @@ editor or on text processing application).
 
 =item *
 
-A warning when leaving the page without saveing unchanged inputs.
+A warning when leaving the page without saving unchanged inputs.
 
 
 =back