]> wagnertech.de Git - mfinanz.git/blobdiff - SL/Controller/Order.pm
Merge branch 'master' of http://wagnertech.de/git/mfinanz
[mfinanz.git] / SL / Controller / Order.pm
index b51802f629835ba7f9852e650fe6e1b7d3f9c876..79c03102113df31e4d8f965126ce0942a3806e84 100644 (file)
@@ -3,38 +3,58 @@ package SL::Controller::Order;
 use strict;
 use parent qw(SL::Controller::Base);
 
 use strict;
 use parent qw(SL::Controller::Base);
 
-use SL::Helper::Flash qw(flash_later);
+use SL::Helper::Flash qw(flash flash_later);
+use SL::HTML::Util;
 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
 use SL::Locale::String qw(t8);
 use SL::SessionFile::Random;
 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
 use SL::Locale::String qw(t8);
 use SL::SessionFile::Random;
+use SL::IMAPClient;
 use SL::PriceSource;
 use SL::Webdav;
 use SL::File;
 use SL::MIME;
 use SL::Util qw(trim);
 use SL::YAML;
 use SL::PriceSource;
 use SL::Webdav;
 use SL::File;
 use SL::MIME;
 use SL::Util qw(trim);
 use SL::YAML;
+use SL::DB::AdditionalBillingAddress;
+use SL::DB::AuthUser;
+use SL::DB::History;
 use SL::DB::Order;
 use SL::DB::Order;
+use SL::DB::OrderItem;
 use SL::DB::Default;
 use SL::DB::Unit;
 use SL::DB::Part;
 use SL::DB::Default;
 use SL::DB::Unit;
 use SL::DB::Part;
+use SL::DB::PartClassification;
 use SL::DB::PartsGroup;
 use SL::DB::Printer;
 use SL::DB::PartsGroup;
 use SL::DB::Printer;
+use SL::DB::Note;
 use SL::DB::Language;
 use SL::DB::Language;
+use SL::DB::Reclamation;
 use SL::DB::RecordLink;
 use SL::DB::Shipto;
 use SL::DB::Translation;
 use SL::DB::RecordLink;
 use SL::DB::Shipto;
 use SL::DB::Translation;
+use SL::DB::EmailJournal;
+use SL::DB::ValidityToken;
+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::Model::Record;
+use SL::DB::Order::TypeData qw(:types);
+use SL::DB::DeliveryOrder::TypeData qw(:types);
+use SL::DB::Reclamation::TypeData qw(:types);
 
 use SL::Helper::CreatePDF qw(:all);
 use SL::Helper::PrintOptions;
 use SL::Helper::ShippedQty;
 
 use SL::Helper::CreatePDF qw(:all);
 use SL::Helper::PrintOptions;
 use SL::Helper::ShippedQty;
+use SL::Helper::UserPreferences::DisplayPreferences;
 use SL::Helper::UserPreferences::PositionsScrollbar;
 use SL::Helper::UserPreferences::UpdatePositions;
 use SL::Helper::UserPreferences::PositionsScrollbar;
 use SL::Helper::UserPreferences::UpdatePositions;
+use SL::Helper::UserPreferences::ItemInputPosition;
 
 use SL::Controller::Helper::GetModels;
 
 use List::Util qw(first sum0);
 use List::UtilsBy qw(sort_by uniq_by);
 
 use SL::Controller::Helper::GetModels;
 
 use List::Util qw(first sum0);
 use List::UtilsBy qw(sort_by uniq_by);
-use List::MoreUtils qw(any none pairwise first_index);
+use List::MoreUtils qw(uniq any none pairwise first_index);
 use English qw(-no_match_vars);
 use File::Spec;
 use Cwd;
 use English qw(-no_match_vars);
 use File::Spec;
 use Cwd;
@@ -43,20 +63,21 @@ use Sort::Naturally;
 use Rose::Object::MakeMethods::Generic
 (
  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 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 multi_items_models all_price_factors search_cvpartnumber show_update_button) ],
+ 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors
+                              search_cvpartnumber show_update_button
+                              part_picker_classification_ids
+                              is_final_version type_data) ],
 );
 
 
 # safety
 );
 
 
 # safety
-__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('check_auth',
+                        except => [ qw(close_quotations) ]);
 
 
-__PACKAGE__->run_before('recalc',
-                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
-                                     print send_email) ]);
-
-__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) ]);
+__PACKAGE__->run_before('check_auth_for_edit',
+                        except => [ qw(edit price_popup load_second_rows close_quotations) ]);
+__PACKAGE__->run_before('get_basket_info_from_from',
+                        except => [ qw(close_quotations) ]);
 
 #
 # actions
 
 #
 # actions
@@ -66,45 +87,104 @@ __PACKAGE__->run_before('get_unalterable_data',
 sub action_add {
   my ($self) = @_;
 
 sub action_add {
   my ($self) = @_;
 
-  $self->order->transdate(DateTime->now_local());
-  my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
-                   $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
-  $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
+  $self->pre_render();
 
 
+  if (!$::form->{form_validity_token}) {
+    $::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_ORDER_SAVE())->token;
+  }
 
 
-  $self->pre_render();
   $self->render(
     'order/form',
   $self->render(
     'order/form',
-    title => $self->get_title_for('add'),
+    title => $self->type_data->text('add'),
     %{$self->{template_args}}
   );
 }
 
     %{$self->{template_args}}
   );
 }
 
+sub action_add_from_record {
+  my ($self) = @_;
+  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()}};
+    }
+  }
+
+  my $record = SL::Model::Record->get_record($from_type, $from_id);
+  my $order = SL::Model::Record->new_from_workflow($record, $self->type, %flags);
+  $self->order($order);
+
+  $self->reinit_after_new_order();
+
+  $self->action_add();
+}
+
+sub action_add_from_purchase_basket {
+  my ($self) = @_;
+
+  my $basket_item_ids = $::form->{basket_item_ids} || [];
+  my $vendor_item_ids = $::form->{vendor_item_ids} || [];
+  my $vendor_id       = $::form->{vendor_id};
+
+
+  unless (scalar @{ $basket_item_ids} || scalar @{ $vendor_item_ids}) {
+    $self->js->flash('error', t8('There are no items selected'));
+    return $self->js->render();
+  }
+
+  my $order = SL::DB::Order->create_from_purchase_basket(
+    $basket_item_ids, $vendor_item_ids, $vendor_id
+  );
+
+  $self->order($order);
+
+  $self->reinit_after_new_order();
+
+  $self->action_add();
+}
+
+sub action_add_from_email_journal {
+  my ($self) = @_;
+  die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
+
+  $self->action_add();
+}
+
+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};
+
+  $self->action_edit();
+}
+
 # edit an existing order
 sub action_edit {
   my ($self) = @_;
 # edit an existing order
 sub action_edit {
   my ($self) = @_;
+  die "No 'id' was given." unless $::form->{id};
 
 
-  if ($::form->{id}) {
-    $self->load_order;
-
-  } else {
-    # this is to edit an order from an unsaved order object
+  $self->load_order;
 
 
-    # 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);
+  if ($self->order->is_sales && $::lx_office_conf{imap_client}->{enabled}) {
+    my $imap_client = SL::IMAPClient->new(%{$::lx_office_conf{imap_client}});
+    if ($imap_client) {
+      $imap_client->update_email_files_for_record(record => $self->order);
     }
     }
-    # 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->recalc();
   $self->pre_render();
   $self->render(
     'order/form',
   $self->pre_render();
   $self->render(
     'order/form',
-    title => $self->get_title_for('edit'),
+    title => $self->type_data->text('edit'),
     %{$self->{template_args}}
   );
 }
     %{$self->{template_args}}
   );
 }
@@ -133,27 +213,26 @@ sub action_edit_collective {
 
   # make new order from given orders
   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
   # make new order from given orders
   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
-  $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
-  $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
+  my $target_type = SALES_ORDER_TYPE();
+  my $order = SL::Model::Record->new_from_workflow_multi(\@multi_orders, $target_type, sort_sources_by => 'transdate');
+  $self->order($order);
+  $self->reinit_after_new_order();
 
 
-  $self->action_edit();
+  $self->action_add();
 }
 
 # delete the order
 sub action_delete {
   my ($self) = @_;
 
 }
 
 # 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();
-  }
-
-  my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
-           : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
-           : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
-           : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
+  SL::Model::Record->delete($self->order);
+  my $text = $self->type eq SALES_ORDER_INTAKE_TYPE()        ? $::locale->text('The order intake has been deleted')
+           : $self->type eq SALES_ORDER_TYPE()               ? $::locale->text('The order confirmation has been deleted')
+           : $self->type eq PURCHASE_ORDER_TYPE()            ? $::locale->text('The order has been deleted')
+           : $self->type eq PURCHASE_ORDER_CONFIRMATION_TYPE() ? $::locale->text('The order confirmation has been deleted')
+           : $self->type eq SALES_QUOTATION_TYPE()           ? $::locale->text('The quotation has been deleted')
+           : $self->type eq REQUEST_QUOTATION_TYPE()         ? $::locale->text('The rfq has been deleted')
+           : $self->type eq PURCHASE_QUOTATION_INTAKE_TYPE() ? $::locale->text('The quotation intake has been deleted')
            : '';
   flash_later('info', $text);
 
            : '';
   flash_later('info', $text);
 
@@ -169,30 +248,46 @@ sub action_delete {
 sub action_save {
   my ($self) = @_;
 
 sub action_save {
   my ($self) = @_;
 
-  my $errors = $self->save();
+  $self->save();
 
 
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
+  flash_later('info', $self->type_data->text('saved'));
+
+  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},
+    );
   }
 
   }
 
-  my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
-           : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
-           : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
-           : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
-           : '';
-  flash_later('info', $text);
+  $self->redirect_to(@redirect_params);
+}
 
 
-  my @redirect_params = (
-    action => 'edit',
-    type   => $self->type,
-    id     => $self->order->id,
+# create new version and set version number
+sub action_add_subversion {
+  my ($self) = @_;
+
+  SL::DB->client->with_transaction(
+    sub {
+      SL::Model::Record->increment_subversion($self->order);
+      $self->save();
+      1;
+    }
   );
 
   );
 
-  $self->redirect_to(@redirect_params);
+  $self->redirect_to(action => 'edit',
+                     type   => $self->type,
+                     id     => $self->order->id,
+  );
 }
 
 }
 
-# save the order as new document an open it for edit
+# save the order as new document and open it for edit
 sub action_save_as_new {
   my ($self) = @_;
 
 sub action_save_as_new {
   my ($self) = @_;
 
@@ -203,38 +298,25 @@ sub action_save_as_new {
     return $self->js->render();
   }
 
     return $self->js->render();
   }
 
-  # load order from db to check if values changed
   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
   my $saved_order = SL::DB::Order->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 ($order->reqdate == $saved_order->reqdate) {
-    my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
-                     $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
-    $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
-  } else {
-    $new_attrs{reqdate} = $order->reqdate;
-  }
+  # Create new record from current one
+  my $new_order = SL::Model::Record->clone_for_save_as_new($saved_order, $order);
+  $self->order($new_order);
 
 
-  # Update employee
-  $new_attrs{employee}  = SL::DB::Manager::Employee->current;
+  # Warn on obsolete items
+  my @obsolete_positions = map { $_->position } grep { $_->part->obsolete } @{ $self->order->items_sorted };
+  flash_later('warning', t8('This record contains obsolete items at position #1', join ', ', @obsolete_positions)) if @obsolete_positions;
 
 
-  # Create new record from current one
-  $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
+  # Warn on order locked items if they are not wanted for this record type
+  if ($self->type_data->no_order_locked_parts) {
+    my @order_locked_positions = map { $_->position } grep { $_->part->order_locked } @{ $self->order->items_sorted };
+    flash_later('warning', t8('This record contains not orderable items at position #1', join ', ', @order_locked_positions)) if @order_locked_positions;
+  }
 
 
-  # 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_ORDER_SAVE())->token;
+  }
 
   # save
   $self->action_save();
 
   # save
   $self->action_save();
@@ -248,15 +330,16 @@ sub action_save_as_new {
 sub action_print {
   my ($self) = @_;
 
 sub action_print {
   my ($self) = @_;
 
-  my $errors = $self->save();
-
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
-  }
+  $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};
   my $formname    = $::form->{print_options}->{formname};
   my $format      = $::form->{print_options}->{format};
   my $media       = $::form->{print_options}->{media};
   my $formname    = $::form->{print_options}->{formname};
@@ -264,14 +347,16 @@ sub action_print {
   my $groupitems  = $::form->{print_options}->{groupitems};
   my $printer_id  = $::form->{print_options}->{printer_id};
 
   my $groupitems  = $::form->{print_options}->{groupitems};
   my $printer_id  = $::form->{print_options}->{printer_id};
 
-  # 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;
+  # only PDF, OpenDocument & HTML for now
+  if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
+    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)) {
   }
 
   # 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
   }
 
   # create a form for generate_attachment_filename
@@ -281,25 +366,27 @@ sub action_print {
   $form->{format}           = $format;
   $form->{formname}         = $formname;
   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
   $form->{format}           = $format;
   $form->{formname}         = $formname;
   $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 $doc_filename          = $form->generate_attachment_filename();
+
+  my $doc;
+  my @errors = $self->generate_doc(\$doc, { media      => $media,
+                                            format     => $format,
+                                            formname   => $formname,
+                                            language   => $self->order->language,
+                                            printer_id => $printer_id,
+                                            groupitems => $groupitems });
   if (scalar @errors) {
   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
   }
 
   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(
     $self->send_file(
-      \$pdf,
-      type         => SL::MIME->mime_type_from_ext($pdf_filename),
-      name         => $pdf_filename,
+      \$doc,
+      type         => SL::MIME->mime_type_from_ext($doc_filename),
+      name         => $doc_filename,
       js_no_render => 1,
     );
 
       js_no_render => 1,
     );
 
@@ -308,118 +395,179 @@ sub action_print {
     my $printer_id = $::form->{print_options}->{printer_id};
     SL::DB::Printer->new(id => $printer_id)->load->print_document(
       copies  => $copies,
     my $printer_id = $::form->{print_options}->{printer_id};
     SL::DB::Printer->new(id => $printer_id)->load->print_document(
       copies  => $copies,
-      content => $pdf,
+      content => $doc,
     );
 
     );
 
-    $self->js->flash('info', t8('The PDF has been printed'));
+    flash_later('info', t8('The document has been printed.'));
   }
 
   }
 
-  # copy file to webdav folder
-  if ($self->order->number && $::instance_conf->get_webdav_documents) {
-    my $webdav = SL::Webdav->new(
-      type     => $self->type,
-      number   => $self->order->number,
-    );
-    my $webdav_file = SL::Webdav::File->new(
-      webdav   => $webdav,
-      filename => $pdf_filename,
-    );
-    eval {
-      $webdav_file->store(data => \$pdf);
-      1;
-    } or do {
-      $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
-    }
-  }
-  if ($self->order->number && $::instance_conf->get_doc_storage) {
-    eval {
-      SL::File->save(object_id     => $self->order->id,
-                     object_type   => $self->type,
-                     mime_type     => 'application/pdf',
-                     source        => 'created',
-                     file_type     => 'document',
-                     file_name     => $pdf_filename,
-                     file_contents => $pdf);
-      1;
-    } or do {
-      $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
-    }
+  my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
+  if (scalar @warnings) {
+    flash_later('warning', $_) for @warnings;
   }
   }
-  $self->js->render;
+
+  $self->save_history('PRINTED');
+
+  $self->js->redirect_to($redirect_url)->render;
 }
 
 }
 
-# open the email dialog
-sub action_save_and_show_email_dialog {
+sub action_preview_pdf {
   my ($self) = @_;
 
   my ($self) = @_;
 
-  my $errors = $self->save();
+  $self->save();
 
 
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
+  $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';
+  my $formname    = $self->type;
+
+  # only pdf
+  # create a form for generate_attachment_filename
+  my $form   = Form->new;
+  $form->{$self->nr_key()}  = $self->order->number;
+  $form->{type}             = $self->type;
+  $form->{format}           = $format;
+  $form->{formname}         = $formname;
+  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
+  my $pdf_filename          = $form->generate_attachment_filename();
+
+  my $pdf;
+  my @errors = $self->generate_doc(\$pdf, { media      => $media,
+                                            format     => $format,
+                                            formname   => $formname,
+                                            language   => $self->order->language,
+                                          });
+  if (scalar @errors) {
+    flash_later('error', t8('Conversion to PDF failed: #1', $errors[0]));
+    return $self->js->redirect_to($redirect_url)->render;
   }
 
   }
 
-  my $cv_method = $self->cv;
+  $self->save_history('PREVIEWED');
+
+  flash_later('info', t8('The PDF has been previewed'));
 
 
-  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);
+  # screen/download
+  $self->send_file(
+    \$pdf,
+    type         => SL::MIME->mime_type_from_ext($pdf_filename),
+    name         => $pdf_filename,
+    js_no_render => 1,
+  );
+
+  $self->js->redirect_to($redirect_url)->render;
+}
+
+# open the email dialog
+sub action_save_and_show_email_dialog {
+  my ($self) = @_;
+
+  if (!$self->is_final_version) {
+    $self->save();
+    $self->js_reset_order_and_item_ids_after_save;
   }
 
   }
 
-  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->type_data->properties('is_customer') ?
+          t8('Cannot send E-mail without customer given')
+        : t8('Cannot send E-mail without vendor given')
+    )->render($self);
 
   my $form = Form->new;
 
   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->{format}           = 'pdf';
+  $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->{format}                  = 'pdf';
+  $form->{cp_id}                   = $self->order->contact->cp_id if $self->order->contact;
+  $form->{transaction_description} = $self->order->transaction_description;
 
 
+  my $email_form;
+  $email_form->{to} =
+       ($self->order->contact ? $self->order->contact->cp_email : 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.Order.send_email()';
 
   my %files = $self->get_files_for_email_dialog();
   $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.Order.send_email()';
 
   my %files = $self->get_files_for_email_dialog();
-  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->cv eq 'customer',
+
+  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(),
+    is_final_version => $self->is_final_version,
   );
 
   $self->js
   );
 
   $self->js
-      ->run('kivi.Order.show_email_dialog', $dialog_html)
-      ->reinit_widgets
-      ->render($self);
+    ->run('kivi.Order.show_email_dialog', $dialog_html)
+    ->reinit_widgets
+    ->render($self);
 }
 
 # send email
 }
 
 # send email
-#
-# Todo: handling error messages: flash is not displayed in dialog, but in the main form
 sub action_send_email {
   my ($self) = @_;
 
 sub action_send_email {
   my ($self) = @_;
 
-  my $errors = $self->save();
-
-  if (scalar @{ $errors }) {
-    $self->js->run('kivi.Order.close_email_dialog');
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
+  if (!$self->is_final_version) {
+    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 $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
   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
   # for Form::cleanup which may be called in Form::send_email
@@ -429,49 +577,98 @@ sub action_send_email {
   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
   $::form->{media}  = 'email';
 
   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
   $::form->{media}  = 'email';
 
-  if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
-    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}});
+  $::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   => $self->type,
+      print_variant => $::form->{formname},
+    );
+  }
+
+  if ($self->is_final_version && $::form->{attachment_policy} eq 'old_file' && !$attfile) {
+    $::form->error(t8('Re-sending a final version was requested, but the latest version of the document could not be found'));
+  }
+
+  if ( !$self->is_final_version
+    &&   $::form->{attachment_policy} ne 'no_file'
+    && !($::form->{attachment_policy} eq 'old_file' && $attfile)
+  ) {
+    my $doc;
+    my @errors = $self->generate_doc(\$doc, {
+        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) {
     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 = $self->store_doc_to_webdav_and_filemanagement(
+      $doc, $::form->{attachment_filename}, $::form->{formname}
+    );
+    if (scalar @warnings) {
+      flash_later('warning', $_) for @warnings;
     }
 
     my $sfile = SL::SessionFile::Random->new(mode => "w");
     }
 
     my $sfile = SL::SessionFile::Random->new(mode => "w");
-    $sfile->fh->print($pdf);
+    $sfile->fh->print($doc);
     $sfile->fh->close;
 
     $::form->{tmpfile} = $sfile->file_name;
     $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->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);
+  $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a
+                                    # linked record to the mail
+  $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
 
   flash_later('info', t8('The email has been sent.'));
 
   flash_later('info', t8('The email has been sent.'));
+  $self->save_history('MAILED');
+
+  # 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);
+  }
+
+  if ($::instance_conf->get_lock_oe_subversions && !$self->is_final_version) {
+    my $file_id;
+    if ($::instance_conf->get_doc_storage && $::form->{attachment_policy} ne 'no_file') {
+      # self is generated on the fly. form is a file from the dms
+      # TODO: for the case Filesystem and Webdav we want the real file from the filesystem
+      #       for the nyi case DMS/CMIS we need a gloid or whatever the system offers (elo_id for ELO)
+      #       DMS kivi version should have a record_link to email_journal
+      #       the record link has to refer to the correct version -> helper table file <-> file_version
+      $file_id = $self->{file_id} || $::form->{file_id};
+      $::form->error("No file id") unless $file_id;
+    }
 
 
-  my @redirect_params = (
-    action => 'edit',
-    type   => $self->type,
-    id     => $self->order->id,
-  );
+    # email is sent -> set this version to final and link to journal and file
+    my $current_version = SL::DB::Manager::OrderVersion->get_all(where => [oe_id => $self->order->id, final_version => 0]);
+    $::form->error("Invalid version state") unless scalar @{ $current_version } == 1;
+    $current_version->[0]->update_attributes(file_id          => $file_id,
+                                             email_journal_id => $::form->{email_journal_id},
+                                             final_version    => 1);
+  }
 
   $self->redirect_to(@redirect_params);
 }
 
   $self->redirect_to(@redirect_params);
 }
@@ -494,9 +691,32 @@ sub action_show_periodic_invoices_config_dialog {
                                                                                 language_id      => $::form->{language_id},
                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
                                                    email_body              => GenericTranslations->get(
                                                                                 language_id      => $::form->{language_id},
                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
                                                    email_body              => GenericTranslations->get(
+                                                                                language_id      => $::form->{language_id},
+                                                                                translation_type => "salutation_general")
+                                                                            . GenericTranslations->get(
+                                                                                language_id      => $::form->{language_id},
+                                                                                translation_type => "salutation_punctuation_mark") . "\n\n"
+                                                                            . GenericTranslations->get(
                                                                                 language_id      => $::form->{language_id},
                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
   );
                                                                                 language_id      => $::form->{language_id},
                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
   );
+  # for older configs, replace email preset text if not yet set.
+  $config->email_subject(GenericTranslations->get(
+                                              language_id      => $::form->{language_id},
+                                              translation_type =>"preset_text_periodic_invoices_email_subject")
+                        ) unless $config->email_subject;
+
+  $config->email_body(GenericTranslations->get(
+                                              language_id      => $::form->{language_id},
+                                              translation_type => "salutation_general")
+                    . GenericTranslations->get(
+                                              language_id      => $::form->{language_id},
+                                              translation_type => "salutation_punctuation_mark") . "\n\n"
+                    . GenericTranslations->get(
+                                              language_id      => $::form->{language_id},
+                                              translation_type =>"preset_text_periodic_invoices_email_body")
+                     ) unless $config->email_body;
+
   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
@@ -508,7 +728,10 @@ sub action_show_periodic_invoices_config_dialog {
 
   if ($::form->{customer_id}) {
     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
   if ($::form->{customer_id}) {
     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
-    $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail;
+    my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
+    $::form->{postal_invoice}                  = $customer_object->postal_invoice;
+    $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
+    $config->send_email(0) if $::form->{postal_invoice};
   }
 
   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
   }
 
   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
@@ -567,7 +790,7 @@ sub action_get_has_active_periodic_invoices {
   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
   my $has_active_periodic_invoices =
   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
   my $has_active_periodic_invoices =
-       $self->type eq sales_order_type()
+       $self->type eq SALES_ORDER_TYPE()
     && $config
     && $config->active
     && (!$config->end_date || ($config->end_date > DateTime->today_local))
     && $config
     && $config->active
     && (!$config->end_date || ($config->end_date > DateTime->today_local))
@@ -576,14 +799,31 @@ sub action_get_has_active_periodic_invoices {
   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 }
 
   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 }
 
-# save the order and redirect to the frontend subroutine for a new
-# delivery order
-sub action_save_and_delivery_order {
+sub action_save_and_new_record {
   my ($self) = @_;
   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);
+
+  $self->save();
+  flash_later('info', $self->type_data->text('saved'));
+
+  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;
+  }
+
+  $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,
   );
 }
 
   );
 }
 
@@ -595,17 +835,51 @@ sub action_save_and_invoice {
   $self->save_and_redirect_to(
     controller => 'oe.pl',
     action     => 'oe_invoice_from_order',
   $self->save_and_redirect_to(
     controller => 'oe.pl',
     action     => 'oe_invoice_from_order',
+    email_journal_id    => $::form->{workflow_email_journal_id},
+    email_attachment_id => $::form->{workflow_email_attachment_id},
+    callback            => $::form->{workflow_email_callback},
   );
 }
 
   );
 }
 
-# workflow from sales quotation to sales order
-sub action_sales_order {
-  $_[0]->workflow_sales_or_purchase_order();
+sub action_save_and_invoice_for_advance_payment {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller       => 'oe.pl',
+    action           => 'oe_invoice_from_order',
+    new_invoice_type => 'invoice_for_advance_payment',
+    email_journal_id    => $::form->{workflow_email_journal_id},
+    email_attachment_id => $::form->{workflow_email_attachment_id},
+    callback            => $::form->{workflow_email_callback},
+  );
+}
+
+sub action_save_and_final_invoice {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller       => 'oe.pl',
+    action           => 'oe_invoice_from_order',
+    new_invoice_type => 'final_invoice',
+    email_journal_id    => $::form->{workflow_email_journal_id},
+    email_attachment_id => $::form->{workflow_email_attachment_id},
+    callback            => $::form->{workflow_email_callback},
+  );
 }
 
 }
 
-# workflow from rfq to purchase order
-sub action_purchase_order {
-  $_[0]->workflow_sales_or_purchase_order();
+# workflows to all types of this controller
+sub action_save_and_order_workflow {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    action     => 'order_workflow',
+    type       => $self->type,
+    to_type    => $::form->{to_type},
+    use_shipto => $::form->{use_shipto},
+    email_journal_id    => $::form->{workflow_email_journal_id},
+    email_attachment_id => $::form->{workflow_email_attachment_id},
+    callback            => $::form->{workflow_email_callback},
+  );
 }
 
 # workflow from purchase order to ap transaction
 }
 
 # workflow from purchase order to ap transaction
@@ -615,16 +889,64 @@ sub action_save_and_ap_transaction {
   $self->save_and_redirect_to(
     controller => 'ap.pl',
     action     => 'add_from_purchase_order',
   $self->save_and_redirect_to(
     controller => 'ap.pl',
     action     => 'add_from_purchase_order',
+    email_journal_id    => $::form->{workflow_email_journal_id},
+    email_attachment_id => $::form->{workflow_email_attachment_id},
+    callback            => $::form->{workflow_email_callback},
   );
 }
 
   );
 }
 
+sub action_order_workflow {
+  my ($self) = @_;
+
+  $self->load_order;
+
+  my $destination_type = $::form->{to_type} ? $::form->{to_type} : '';
+
+  my $from_side        = $self->order->is_sales ? 'sales' : 'purchase';
+  my $to_side          = (any { $destination_type eq $_ } (SALES_ORDER_INTAKE_TYPE(), SALES_ORDER_TYPE(), SALES_QUOTATION_TYPE())) ? 'sales' : 'purchase';
+
+  # check for direct delivery
+  # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
+  my $custom_shipto;
+  if (   $from_side eq 'sales' && $to_side eq 'purchase'
+      && $::form->{use_shipto} && $self->order->shipto) {
+    $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
+  }
+
+  my $no_linked_records =    (any { $destination_type eq $_ } (SALES_QUOTATION_TYPE(), REQUEST_QUOTATION_TYPE()))
+                          && $from_side eq $to_side;
+
+  $self->order(SL::Model::Record->new_from_workflow($self->order, $destination_type, no_linked_records => $no_linked_records));
+
+  delete $::form->{id};
+
+  if (!$no_linked_records) {
+    $self->{converted_from_oe_id}         = $self->order->{ RECORD_ID()      };
+    $_   ->{converted_from_orderitems_id} = $_          ->{ RECORD_ITEM_ID() } for @{ $self->order->items_sorted };
+  }
+
+  if ($from_side eq 'sales' && $to_side eq 'purchase') {
+    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 => []));
+    }
+  }
+
+  $self->reinit_after_new_order();
+
+  $self->action_add;
+}
+
 # set form elements in respect to a changed customer or vendor
 #
 # This action is called on an change of the customer/vendor picker.
 sub action_customer_vendor_changed {
   my ($self) = @_;
 
 # set form elements in respect to a changed customer or vendor
 #
 # This action is called on an change of the customer/vendor picker.
 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));
+
   $self->recalc();
 
   my $cv_method = $self->cv;
   $self->recalc();
 
   my $cv_method = $self->cv;
@@ -641,20 +963,26 @@ sub action_customer_vendor_changed {
     $self->js->hide('#shipto_selection');
   }
 
     $self->js->hide('#shipto_selection');
   }
 
+  if ($cv_method eq 'customer') {
+    my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
+    $self->js->$show_hide('#billing_address_row');
+  }
+
   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
   $self->js
   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
   $self->js
-    ->replaceWith('#order_cp_id',            $self->build_contact_select)
-    ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
-    ->replaceWith('#shipto_inputs  ',        $self->build_shipto_inputs)
-    ->replaceWith('#business_info_row',      $self->build_business_info_row)
-    ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
-    ->val(        '#order_taxincluded',      $self->order->taxincluded)
-    ->val(        '#order_currency_id',      $self->order->currency_id)
-    ->val(        '#order_payment_id',       $self->order->payment_id)
-    ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
-    ->val(        '#order_intnotes',         $self->order->intnotes)
-    ->val(        '#order_language_id',      $self->order->$cv_method->language_id)
+    ->replaceWith('#order_cp_id',              $self->build_contact_select)
+    ->replaceWith('#order_shipto_id',          $self->build_shipto_select)
+    ->replaceWith('#shipto_inputs  ',          $self->build_shipto_inputs)
+    ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
+    ->replaceWith('#business_info_row',        $self->build_business_info_row)
+    ->val(        '#order_taxzone_id',         $self->order->taxzone_id)
+    ->val(        '#order_taxincluded',        $self->order->taxincluded)
+    ->val(        '#order_currency_id',        $self->order->currency_id)
+    ->val(        '#order_payment_id',         $self->order->payment_id)
+    ->val(        '#order_delivery_term_id',   $self->order->delivery_term_id)
+    ->val(        '#order_intnotes',           $self->order->intnotes)
+    ->val(        '#order_language_id',        $self->order->$cv_method->language_id)
     ->focus(      '#order_' . $self->cv . '_id')
     ->run('kivi.Order.update_exchangerate');
 
     ->focus(      '#order_' . $self->cv . '_id')
     ->run('kivi.Order.update_exchangerate');
 
@@ -663,40 +991,6 @@ sub action_customer_vendor_changed {
   $self->js->render();
 }
 
   $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) = @_;
 # called if a unit in an existing item row is changed
 sub action_unit_changed {
   my ($self) = @_;
@@ -716,10 +1010,43 @@ sub action_unit_changed {
   $self->js->render();
 }
 
   $self->js->render();
 }
 
+# update item input row when a part ist picked
+sub action_update_item_input_row {
+  my ($self) = @_;
+
+  delete $::form->{add_item}->{$_} for qw(create_part_type sellprice_as_number discount_as_percent);
+
+  my $form_attr = $::form->{add_item};
+
+  return unless $form_attr->{parts_id};
+
+  my $record       = $self->order;
+  my $item         = SL::DB::OrderItem->new(%$form_attr);
+  $item->qty(1) if !$item->qty;
+  $item->unit($item->part->unit);
+
+  my ($price_src, $discount_src) = SL::Model::Record->get_best_price_and_discount_source($record, $item, ignore_given => 0);
+
+  my $texts = get_part_texts($item->part, $record->language_id);
+
+  $self->js
+    ->val     ('#add_item_unit',                $item->unit)
+    ->val     ('#add_item_description',         $texts->{description})
+    ->val     ('#add_item_sellprice_as_number', '')
+    ->attr    ('#add_item_sellprice_as_number', 'placeholder', $price_src->price_as_number)
+    ->attr    ('#add_item_sellprice_as_number', 'title',       $price_src->source_description)
+    ->val     ('#add_item_discount_as_percent', '')
+    ->attr    ('#add_item_discount_as_percent', 'placeholder', $discount_src->discount_as_percent)
+    ->attr    ('#add_item_discount_as_percent', 'title',       $discount_src->source_description)
+    ->render;
+}
+
 # add an item row for a new item entered in the input row
 sub action_add_item {
   my ($self) = @_;
 
 # add an item row for a new item entered in the input row
 sub action_add_item {
   my ($self) = @_;
 
+  delete $::form->{add_item}->{create_part_type};
+
   my $form_attr = $::form->{add_item};
 
   return unless $form_attr->{parts_id};
   my $form_attr = $::form->{add_item};
 
   return unless $form_attr->{parts_id};
@@ -744,7 +1071,7 @@ sub action_add_item {
       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
   } else {
     $self->js
       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
   } else {
     $self->js
-      ->append('#row_table_id', $row_as_html);
+      ->before('#row_table_footer', $row_as_html);
   }
 
   if ( $item->part->is_assortment ) {
   }
 
   if ( $item->part->is_assortment ) {
@@ -774,55 +1101,37 @@ sub action_add_item {
           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
       } else {
         $self->js
           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
       } else {
         $self->js
-          ->append('#row_table_id', $row_as_html);
+          ->before('#row_table_footer', $row_as_html);
       }
     };
   };
 
   $self->js
     ->val('.add_item_input', '')
       }
     };
   };
 
   $self->js
     ->val('.add_item_input', '')
+    ->attr('.add_item_input', 'placeholder', '')
+    ->attr('.add_item_input', 'title', '')
+    ->attr('#add_item_qty_as_number', 'placeholder', '1')
     ->run('kivi.Order.init_row_handlers')
     ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
 
   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
     ->run('kivi.Order.init_row_handlers')
     ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
 
   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
+  # alternate scroll behaviour if item input below positions and unlimited scroll height
+  $self->js->run('kivi.Order.scroll_page_after_row_insert', $item_id)
+    if 0 == SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height
+    && SL::Helper::UserPreferences::ItemInputPosition->new()->get_order_item_input_position
+       // $::instance_conf->get_order_item_input_position;
+
   $self->js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
 
   $self->js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
 
-# open the dialog for entering multiple items at once
-sub action_show_multi_items_dialog {
-  $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
-                all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
-}
-
-# update the filter results in the multi item dialog
-sub action_multi_items_update_result {
-  my $max_count = 100;
-
-  $::form->{multi_items}->{filter}->{obsolete} = 0;
-
-  my $count = $_[0]->multi_items_models->count;
-
-  if ($count == 0) {
-    my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
-    $_[0]->render($text, { layout => 0 });
-  } elsif ($count > $max_count) {
-    my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
-    $_[0]->render($text, { layout => 0 });
-  } else {
-    my $multi_items = $_[0]->multi_items_models->get;
-    $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
-                  multi_items => $multi_items);
-  }
-}
-
 # add item rows for multiple items at once
 sub action_add_multi_items {
   my ($self) = @_;
 
 # add item rows for multiple items at once
 sub action_add_multi_items {
   my ($self) = @_;
 
-  my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
+  my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
   return $self->js->render() unless scalar @form_attr;
 
   my @items;
   return $self->js->render() unless scalar @form_attr;
 
   my @items;
@@ -867,7 +1176,7 @@ sub action_add_multi_items {
   }
 
   $self->js
   }
 
   $self->js
-    ->run('kivi.Order.close_multi_items_dialog')
+    ->run('kivi.Part.close_picker_dialogs')
     ->run('kivi.Order.init_row_handlers')
     ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
     ->run('kivi.Order.init_row_handlers')
     ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
@@ -946,6 +1255,55 @@ sub action_price_popup {
   $self->render_price_dialog($item);
 }
 
   $self->render_price_dialog($item);
 }
 
+# save the order in a session variable and redirect to the part controller
+sub action_create_part {
+  my ($self) = @_;
+
+  my $previousform = $::auth->save_form_in_session(non_scalars => 1);
+
+  my $callback     = $self->url_for(
+    action       => 'return_from_create_part',
+    type         => $self->type, # type is needed for check_auth on return
+    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.'));
+
+  my @redirect_params = (
+    controller    => 'Part',
+    action        => 'add',
+    part_type     => $::form->{add_item}->{create_part_type},
+    callback      => $callback,
+    inline_create => 1,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
+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};
+
+  $::auth->restore_form_from_session(delete $::form->{previousform});
+
+  $self->order($self->init_order);
+  $self->reinit_after_new_order();
+
+  if ($self->order->id) {
+    $self->pre_render();
+    $self->render(
+      'order/form',
+      title => $self->type_data->text('edit'),
+      %{$self->{template_args}}
+    );
+  } else {
+    $self->action_add;
+  }
+}
+
 # load the second row for one or more items
 #
 # This action gets the html code for all items second rows by rendering a template for
 # load the second row for one or more items
 #
 # This action gets the html code for all items second rows by rendering a template for
@@ -955,66 +1313,171 @@ sub action_load_second_rows {
 
   $self->recalc() if $self->order->is_sales; # for margin calculation
 
 
   $self->recalc() if $self->order->is_sales; # for margin calculation
 
-  foreach my $item_id (@{ $::form->{item_ids} }) {
-    my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
-    my $item = $self->order->items_sorted->[$idx];
+  foreach my $item_id (@{ $::form->{item_ids} }) {
+    my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
+    my $item = $self->order->items_sorted->[$idx];
+
+    $self->js_load_second_row($item, $item_id, 0);
+  }
+
+  $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
+
+  $self->js->render();
+}
+
+# update description, notes and sellprice from master data
+sub action_update_row_from_master_data {
+  my ($self) = @_;
+
+  foreach my $item_id (@{ $::form->{item_ids} }) {
+    my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
+    my $item  = $self->order->items_sorted->[$idx];
+    my $texts = get_part_texts($item->part, $self->order->language_id);
+
+    $item->description($texts->{description});
+    $item->longdescription($texts->{longdescription});
+
+    my ($price_src, $discount_src) = 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);
+    $item->discount($discount_src->discount);
+    $item->active_discount_source($discount_src);
+
+    my $price_editable = $self->order->is_sales ? $::auth->assert('sales_edit_prices', 1) : $::auth->assert('purchase_edit_prices', 1);
+
+    $self->js
+      ->run('kivi.Order.set_price_and_source_text',    $item_id, $price_src   ->source, $price_src   ->source_description, $item->sellprice_as_number, $price_editable)
+      ->run('kivi.Order.set_discount_and_source_text', $item_id, $discount_src->source, $discount_src->source_description, $item->discount_as_percent, $price_editable)
+      ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
+      ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
+      ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
+
+    if ($self->search_cvpartnumber) {
+      $self->get_item_cvpartnumber($item);
+      $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
+    }
+  }
+
+  $self->recalc();
+  $self->js_redisplay_line_values;
+  $self->js_redisplay_amounts_and_taxes;
+
+  $self->js->render();
+}
+
+sub action_save_phone_note {
+  my ($self) = @_;
+
+  my $phone_note = $self->parse_phone_note;
+  my $is_new     = !$phone_note->id;
+
+  $phone_note->save;
+  $self->order(SL::DB::Order->new(id => $self->order->id)->load);
+
+  my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
+
+  return $self->js
+    ->replaceWith('#phone-notes', $tab_as_html)
+    ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
+    ->flash('info', $is_new ? t8('Phone note has been created.') : t8('Phone note has been updated.'))
+    ->reinit_widgets
+    ->render;
+}
+
+sub action_delete_phone_note {
+  my ($self) = @_;
+
+  my $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
+
+  return $self->js->flash('error', t8('Phone note not found for this order.'))->render if !$phone_note;
 
 
-    $self->js_load_second_row($item, $item_id, 0);
-  }
+  $phone_note->delete;
+  $self->order(SL::DB::Order->new(id => $self->order->id)->load);
 
 
-  $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
+  my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
 
 
-  $self->js->render();
+  return $self->js
+    ->replaceWith('#phone-notes', $tab_as_html)
+    ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
+    ->flash('info', t8('Phone note has been deleted.'))
+    ->reinit_widgets
+    ->render;
 }
 
 }
 
-# update description, notes and sellprice from master data
-sub action_update_row_from_master_data {
+sub action_close_quotations {
   my ($self) = @_;
 
   my ($self) = @_;
 
-  foreach my $item_id (@{ $::form->{item_ids} }) {
-    my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
-    my $item  = $self->order->items_sorted->[$idx];
-    my $texts = get_part_texts($item->part, $self->order->language_id);
+  my @redirect_params = $::form->{callback} ? ($::form->{callback})
+                                            : (controller => 'LoginScreen', action => 'user_login');
 
 
-    $item->description($texts->{description});
-    $item->longdescription($texts->{longdescription});
+  if (!$::form->{ids} || !@{$::form->{ids}}) {
+    flash_later('info', t8('Nothing selected!'));
+    $self->redirect_to(@redirect_params);
+    $::dispatcher->end_request;
+  }
 
 
-    my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
+  my $sales_quotations   = SL::DB::Manager::Order->get_all(where => [id            => $::form->{ids},
+                                                                     or             => [closed => 0, closed => undef],
+                                                                     record_type    => SALES_QUOTATION_TYPE()]);
 
 
-    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 $request_quotations = SL::DB::Manager::Order->get_all(where => [id            => $::form->{ids},
+                                                                     or             => [closed => 0, closed => undef],
+                                                                     record_type    => REQUEST_QUOTATION_TYPE()]);
+
+  $::auth->assert('sales_quotation_edit')   if scalar @$sales_quotations;
+  $::auth->assert('request_quotation_edit') if scalar @$request_quotations;
+
+  my $employee_id = SL::DB::Manager::Employee->current->id;
+  SL::DB->client->with_transaction(sub {
+    SL::DB::Manager::Order->update_all(set   => {closed => 1},
+                                       where => [id => $::form->{ids}]);
+
+    foreach my $quotation (@$sales_quotations, @$request_quotations) {
+      SL::DB::History->new(
+        trans_id    => $quotation->id,
+        employee_id => $employee_id,
+        what_done   => $quotation->type,
+        snumbers    => 'quonumber_' . $quotation->number,
+        addition    => 'SAVED',
+      )->save;
     }
 
     }
 
+    1;
+  }) || do {
+    $::form->error(t8('Closing the selected quotations failed: #1', SL::DB->client->error));
+  };
 
 
-    $item->sellprice($price_src->price);
-    $item->active_price_source($price_src);
+  flash_later('info', t8('The selected quotations where closed.'));
+  $self->redirect_to(@redirect_params);
+}
 
 
-    $self->js
-      ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
-      ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
-      ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
-      ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
+sub action_show_conversion_to_purchase_delivery_order_item_selection {
+  my ($self) = @_;
 
 
-    if ($self->search_cvpartnumber) {
-      $self->get_item_cvpartnumber($item);
-      $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
+  my $items = $self->order->items_sorted;
+
+  if (@$items) {
+    my @part_ids          = uniq map { $_->{parts_id} } @$items;
+    my %parts_by_id       = map { ($_->id => $_) } @{ SL::DB::Manager::Part->get_all(where => [ id => \@part_ids ]) };
+    my %make_models_by_id = map { ($_->parts_id => $_->model) } @{
+      SL::DB::Manager::MakeModel->get_all(
+        where => [
+          parts_id => \@part_ids,
+          make     => $::form->{order}->{vendor_id},
+        ])
+    };
+
+    foreach my $item (@$items) {
+      $item->{partnumber}        = $parts_by_id{ $item->{parts_id} }->partnumber;
+      $item->{vendor_partnumber} = $make_models_by_id{ $item->{parts_id} };
     }
   }
 
     }
   }
 
-  $self->recalc();
-  $self->js_redisplay_line_values;
-  $self->js_redisplay_amounts_and_taxes;
-
-  $self->js->render();
+  $self->render(
+    'order/tabs/_purchase_delivery_order_item_selection',
+    { layout => 0 },
+    ITEMS => $items,
+  );
 }
 
 sub js_load_second_row {
 }
 
 sub js_load_second_row {
@@ -1113,7 +1576,8 @@ sub js_reset_order_and_item_ids_after_save {
 
   $self->js
     ->val('#id', $self->order->id)
 
   $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;
     ->val('#order_' . $self->nr_key(), $self->order->number);
 
   my $idx = 0;
@@ -1127,7 +1591,9 @@ sub js_reset_order_and_item_ids_after_save {
   } continue {
     $idx++;
   }
   } 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[+]"]', '');
+  $self->js->val('[name="basket_item_ids[+]"]', '');
 }
 
 #
 }
 
 #
@@ -1135,27 +1601,24 @@ sub js_reset_order_and_item_ids_after_save {
 #
 
 sub init_valid_types {
 #
 
 sub init_valid_types {
-  [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
+  $_[0]->type_data->valid_types;
 }
 
 sub init_type {
   my ($self) = @_;
 
 }
 
 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 order";
   }
 
     die "Not a valid type for order";
   }
 
-  $self->type($::form->{type});
+  $self->type($type);
 }
 
 sub init_cv {
   my ($self) = @_;
 
 }
 
 sub init_cv {
   my ($self) = @_;
 
-  my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
-         : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
-         : die "Not a valid type for order";
-
-  return $cv;
+  return $self->type_data->properties('customervendor');
 }
 
 sub init_search_cvpartnumber {
 }
 
 sub init_search_cvpartnumber {
@@ -1183,37 +1646,39 @@ sub init_order {
   $_[0]->make_order;
 }
 
   $_[0]->make_order;
 }
 
-# model used to filter/display the parts in the multi-items dialog
-sub init_multi_items_models {
-  SL::Controller::Helper::GetModels->new(
-    controller     => $_[0],
-    model          => 'Part',
-    with_objects   => [ qw(unit_obj) ],
-    disable_plugin => 'paginated',
-    source         => $::form->{multi_items},
-    sorted         => {
-      _default    => {
-        by  => 'partnumber',
-        dir => 1,
-      },
-      partnumber  => t8('Partnumber'),
-      description => t8('Description')}
-  );
-}
-
 sub init_all_price_factors {
   SL::DB::Manager::PriceFactor->get_all;
 }
 
 sub init_all_price_factors {
   SL::DB::Manager::PriceFactor->get_all;
 }
 
-sub check_auth {
-  my ($self) = @_;
+sub init_part_picker_classification_ids {
+  my ($self)    = @_;
+
+  return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(
+    where => $self->type_data->part_classification_query()) } ];
+}
 
 
-  my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
+sub init_is_final_version {
+  # VALID States for current Sales Version
+  # 1. save create version without email_id             -> open
+  # 2. send email set email_id for version 1            -> final
+  # 3. save and subversion new version without email_id -> open
+  # 4. send email set email_id for current subversion   -> final
+  # for all versions > 1 set postfix -2 .. -n for recordnumber
+  return $::instance_conf->get_lock_oe_subversions    ?  # conf enabled
+         $_[0]->order->id                             ?  # is saved
+         $_[0]->order->is_final_version               :  # is final
+         undef                                        :  # is not final
+         undef;                                          # conf disabled
+}
 
 
-  my $right   = $right_for->{ $self->type };
-  $right    ||= 'DOES_NOT_EXIST';
+sub check_auth {
+  my ($self) = @_;
+  $::auth->assert($self->type_data->rights('view'));
+}
 
 
-  $::auth->assert($right);
+sub check_auth_for_edit {
+  my ($self) = @_;
+  $::auth->assert($self->type_data->rights('edit'));
 }
 
 # build the selection box for contacts
 }
 
 # build the selection box for contacts
@@ -1231,6 +1696,24 @@ sub build_contact_select {
   );
 }
 
   );
 }
 
+# build the selection box for the additional billing address
+#
+# Needed, if customer/vendor changed.
+sub build_billing_address_select {
+  my ($self) = @_;
+
+  return '' if $self->cv ne 'customer';
+
+  select_tag('order.billing_address_id',
+             [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
+             value_key  => 'id',
+             title_key  => 'displayable_id',
+             default    => $self->order->billing_address_id,
+             with_empty => 0,
+             style      => 'width: 300px',
+  );
+}
+
 # build the selection box for shiptos
 #
 # Needed, if customer/vendor changed.
 # build the selection box for shiptos
 #
 # Needed, if customer/vendor changed.
@@ -1278,7 +1761,13 @@ sub build_tax_rows {
 
   my $rows_as_html;
   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
   my $rows_as_html;
   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
-    $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
+    $rows_as_html .= $self->p->render(
+      'order/tabs/_tax_row',
+      SELF => $self,
+      TAX => $tax,
+      TAXINCLUDED => $self->order->taxincluded,
+      QUOTATION => $self->order->quotation
+    );
   }
   return $rows_as_html;
 }
   }
   return $rows_as_html;
 }
@@ -1312,9 +1801,7 @@ sub load_order {
 
   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
 
   $self->order(SL::DB::Order->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->reinit_after_new_order();
 
   return $self->order;
 }
 
   return $self->order;
 }
@@ -1332,27 +1819,53 @@ sub make_order {
   # be retrieved via items until the order is saved. Adding empty items to new
   # order here solves this problem.
   my $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::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
-  $order ||= SL::DB::Order->new(orderitems  => [],
-                                quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
-                                currency_id => $::instance_conf->get_currency_id(),);
+  if ($::form->{id}) {
+    $order   = SL::DB::Order->new(
+      id => $::form->{id}
+    )->load(
+      with => [
+        'orderitems',
+        'orderitems.part',
+      ]
+    );
+  } else {
+    $order = SL::DB::Order->new(
+      orderitems  => [],
+      record_type => $::form->{type},
+      currency_id => $::instance_conf->get_currency_id(),
+    );
+    $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});
   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};
-  my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
+  # don't assign hashes as objects
+  my $form_orderitems               = delete $::form->{order}->{orderitems};
+  my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
 
   $order->assign_attributes(%{$::form->{order}});
 
 
   $order->assign_attributes(%{$::form->{order}});
 
+  # restore form values
+  $::form->{order}->{orderitems}               = $form_orderitems;
+  $::form->{order}->{periodic_invoices_config} = $form_periodic_invoices_config;
+
   $self->setup_custom_shipto_from_form($order, $::form);
 
   $self->setup_custom_shipto_from_form($order, $::form);
 
-  if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
-    my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
-    $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
+  if (
+    my $periodic_invoices_config_attrs = $form_periodic_invoices_config ?
+        SL::YAML::Load($form_periodic_invoices_config)
+      : undef
+  ) {
+    my $periodic_invoices_config =
+         $order->periodic_invoices_config
+      || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
+    $periodic_invoices_config->assign_attributes(
+      %$periodic_invoices_config_attrs
+    );
   }
 
   # remove deleted items
   }
 
   # remove deleted items
@@ -1423,78 +1936,46 @@ sub new_item {
   }
 
   $item->assign_attributes(%$attr);
   }
 
   $item->assign_attributes(%$attr);
+  $item->qty(1.0)                   if !$item->qty;
+  $item->unit($item->part->unit)    if !$item->unit;
 
 
-  my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
-  my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
-
-  $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($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
-    $price_src->price(0) if !$price_source->best_price;
-  }
+  my ($price_src, $discount_src) = SL::Model::Record->get_best_price_and_discount_source($record, $item, ignore_given => 0);
 
 
-  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 $texts = get_part_texts($item->part, $record->language_id);
 
   my %new_attr;
 
   my %new_attr;
-  $new_attr{part}                   = $part;
-  $new_attr{description}            = $part->description     if ! $item->description;
-  $new_attr{qty}                    = 1.0                    if ! $item->qty;
-  $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
+  $new_attr{description}            = $texts->{description}        if ! $item->description;
+  $new_attr{qty}                    = 1.0                          if ! $item->qty;
+  $new_attr{price_factor_id}        = $item->part->price_factor_id if ! $item->price_factor_id;
   $new_attr{sellprice}              = $price_src->price;
   $new_attr{discount}               = $discount_src->discount;
   $new_attr{active_price_source}    = $price_src;
   $new_attr{active_discount_source} = $discount_src;
   $new_attr{sellprice}              = $price_src->price;
   $new_attr{discount}               = $discount_src->discount;
   $new_attr{active_price_source}    = $price_src;
   $new_attr{active_discount_source} = $discount_src;
-  $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
+  $new_attr{longdescription}        = $texts->{longdescription}    if ! defined $attr->{longdescription};
   $new_attr{project_id}             = $record->globalproject_id;
   $new_attr{project_id}             = $record->globalproject_id;
-  $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
+  $new_attr{lastcost}               = $record->is_sales ? $item->part->lastcost : 0;
 
   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
   # they cannot be retrieved via custom_variables until the order/orderitem is
   # saved. Adding empty custom_variables to new orderitem here solves this problem.
   $new_attr{custom_variables} = [];
 
 
   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
   # they cannot be retrieved via custom_variables until the order/orderitem is
   # 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});
-
-  $item->assign_attributes(%new_attr, %{ $texts });
+  $item->assign_attributes(%new_attr);
 
   return $item;
 }
 
 
   return $item;
 }
 
-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);
+sub get_basket_info_from_from {
+  my ($self) = @_;
 
 
-  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});
+  my $order = $self->order;
+  my $basket_item_ids = $::form->{basket_item_ids};
+  if (scalar @{ $basket_item_ids || [] }) {
+    for my $idx (0 .. $#{ $order->items_sorted }) {
+      my $order_item = $order->items_sorted->[$idx];
+      $order_item->{basket_item_id} = $basket_item_ids->[$idx];
+    }
   }
   }
-
 }
 
 # setup custom shipto from form
 }
 
 # setup custom shipto from form
@@ -1511,7 +1992,11 @@ sub setup_custom_shipto_from_form {
   if ($order->shipto) {
     $self->is_custom_shipto_to_delete(1);
   } else {
   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 $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 $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};
@@ -1556,139 +2041,185 @@ sub get_unalterable_data {
   }
 }
 
   }
 }
 
-# delete the order
+# parse new or updated phone note
 #
 #
-# And remove related files in the spool directory
-sub delete {
+# And put them into the order object.
+sub parse_phone_note {
   my ($self) = @_;
 
   my ($self) = @_;
 
-  my $errors = [];
-  my $db     = $self->order->db;
+  if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
+    die t8('Phone note needs a subject and a body.');
+  }
+
+  my $phone_note;
+  if ($::form->{phone_note}->{id}) {
+    $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
+    die t8('Phone note not found for this order.') if !$phone_note;
+  }
 
 
-  $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;
+  $phone_note = SL::DB::Note->new() if !$phone_note;
+  my $is_new  = !$phone_note->id;
 
 
-      1;
-  }) || push(@{$errors}, $db->error);
+  $phone_note->assign_attributes(%{ $::form->{phone_note} },
+                                 trans_id     => $self->order->id,
+                                 trans_module => 'oe',
+                                 employee     => SL::DB::Manager::Employee->current);
 
 
-  return $errors;
+  $self->order->add_phone_notes($phone_note) if $is_new;
+  return $phone_note;
 }
 
 }
 
-# save the order
-#
-# And delete items that are deleted in the form.
-sub save {
+sub check_if_periodic_invoices_contact_matches_customer {
   my ($self) = @_;
 
   my ($self) = @_;
 
-  my $errors = [];
-  my $db     = $self->order->db;
+  return if !$self->order->is_type(SL::DB::Order::SALES_ORDER_TYPE());
 
 
-  $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::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
-    $self->order->save(cascade => 1);
+  my $cfg = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $self->order->id);
+  return if !$cfg || !$cfg->email_recipient_contact_id;
 
 
-    # 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$/;
-        $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++;
-        }
-      }
-    }
-    1;
-  }) || push(@{$errors}, $db->error);
+  my $contact = SL::DB::Manager::Contact->find_by(cp_id => $cfg->email_recipient_contact_id);
+  return if !$contact;
 
 
-  return $errors;
+  if ($contact->cp_cv_id != $self->order->customer_id) {
+    $cfg->update_attributes(email_recipient_contact_id => undef);
+  }
 }
 
 }
 
-sub workflow_sales_or_purchase_order {
+# save the order
+#
+# And delete items that are deleted in the form.
+sub save {
   my ($self) = @_;
 
   my ($self) = @_;
 
-  # always save
-  my $errors = $self->save();
+  my $is_new = !$self->order->id;
 
 
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
+  $self->parse_phone_note if $::form->{phone_note}->{subject} || $::form->{phone_note}->{body};
+
+  # Test for order locked items if they are not wanted for this record type.
+  if ($self->type_data->no_order_locked_parts) {
+    my @order_locked_positions = map { $_->position } grep { $_->part->order_locked } @{ $self->order->items_sorted };
+    die t8('This record contains not orderable items at position #1', join ', ', @order_locked_positions) if @order_locked_positions;
   }
 
   }
 
-  my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
-                       : $::form->{type} eq request_quotation_type() ? purchase_order_type()
-                       : $::form->{type} eq purchase_order_type()    ? sales_order_type()
-                       : $::form->{type} eq sales_order_type()       ? purchase_order_type()
-                       : '';
+  # create first version if none exists
+  $self->order->add_order_version(SL::DB::OrderVersion->new(version => 1)) if !$self->order->order_version;
 
 
-  # check for direct delivery
-  # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
-  my $custom_shipto;
-  if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
-      && $::form->{use_shipto} && $self->order->shipto) {
-    $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
+  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()},
+  );
+
+  my @converted_from_oe_ids;
+  if ($self->order->{RECORD_TYPE_REF()} eq 'SL::DB::Order'
+      && $self->order->{RECORD_ID()}) {
+    @converted_from_oe_ids = split ' ', $self->order->{RECORD_ID()};
+  }
+
+  # check for purchase basket items
+  my %basket_item_id_to_orderitem =
+    map { $_->{basket_item_id} => $_ }
+    grep { $_->{basket_item_id} ne '' }
+    $self->order->orderitems;
+  my @basket_item_ids = keys %basket_item_id_to_orderitem;
+  if (scalar @basket_item_ids) {
+    my $basket_items = SL::DB::Manager::PurchaseBasketItem->get_all(
+      where => [ id => \@basket_item_ids ]);
+    if (scalar @$basket_items != scalar @basket_item_ids) {
+      my %basket_item_exists = map { $_->id => 1 } @$basket_items;
+      my @missing_for_positions =
+        map { $_->position }
+        map { $basket_item_id_to_orderitem{$_} }
+        grep { !$basket_item_exists{$_} }
+        @basket_item_ids;
+      return [t8('Purchase basket item not existing any more for position(s): #1.',
+                 join(',', @missing_for_positions))];
+    }
   }
 
   }
 
-  $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
-  $self->{converted_from_oe_id} = delete $::form->{id};
+  my $objects_to_close = scalar @converted_from_oe_ids
+                       ? SL::DB::Manager::Order->get_all(where => [
+                           id => \@converted_from_oe_ids,
+                           or => [  record_type => SALES_QUOTATION_TYPE(),
+                                    record_type => REQUEST_QUOTATION_TYPE(),
+                                   (record_type => PURCHASE_QUOTATION_INTAKE_TYPE()) x $self->order->is_type(PURCHASE_ORDER_TYPE()),
+                                   (record_type => PURCHASE_ORDER_TYPE())            x $self->order->is_type(PURCHASE_ORDER_CONFIRMATION_TYPE())  ]
+                           ])
+                       : undef;
+
+  my $items_to_delete  = scalar @{ $self->item_ids_to_delete || [] }
+                       ? SL::DB::Manager::OrderItem->get_all(where => [id => $self->item_ids_to_delete])
+                       : undef;
+
+  SL::Model::Record->save($self->order,
+                          with_validity_token  => { scope => SL::DB::ValidityToken::SCOPE_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,
+                          objects_to_close     => $objects_to_close,
+                          link_requirement_specs_linking_to_created_from_objects => \@converted_from_oe_ids,
+                          set_project_in_linked_requirement_specs                => 1,
+  );
 
 
-  # 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);
+  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}
+    );
   }
 
   }
 
-  if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
-    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 => []));
+  if ($is_new && $self->order->is_sales && $::lx_office_conf{imap_client}->{enabled}) {
+    my $imap_client = SL::IMAPClient->new(%{$::lx_office_conf{imap_client}});
+    if ($imap_client) {
+      $imap_client->create_folder_for_record(record => $self->order);
     }
   }
 
     }
   }
 
+  $self->check_if_periodic_invoices_contact_matches_customer;
+
+  delete $::form->{form_validity_token};
+}
+
+sub reinit_after_new_order {
+  my ($self) = @_;
+
   # change form type
   # change form type
-  $::form->{type} = $destination_type;
+  $::form->{type} = $self->order->type;
   $self->type($self->init_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->check_auth;
 
-  $self->recalc();
-  $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 };
+  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);
 
 
-  $self->render(
-    'order/form',
-    title => $self->get_title_for('edit'),
-    %{$self->{template_args}}
-  );
-}
+    # 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;
+  }
 
 
+  # Warn on order locked items if they are not wanted for this record type
+  if ($self->type_data->no_order_locked_parts) {
+    my @order_locked_positions =
+      map { $_->position }
+      grep { $_->part->order_locked }
+      @{ $self->order->items_sorted };
+    flash('warning', t8(
+        'This record contains not orderable items at position #1',
+        join ', ', @order_locked_positions)
+    ) if @order_locked_positions;
+  }
+
+  $self->get_unalterable_data();
+  $self->recalc();
+}
 
 sub pre_render {
   my ($self) = @_;
 
 sub pre_render {
   my ($self) = @_;
@@ -1696,7 +2227,7 @@ sub pre_render {
   $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_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');
   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
                                                                                               deleted => 0 ] ],
                                                                            sort_by => 'name');
@@ -1705,7 +2236,9 @@ sub pre_render {
                                                                            sort_by => 'name');
   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
                                                                                                         obsolete => 0 ] ]);
                                                                            sort_by => 'name');
   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
                                                                                                         obsolete => 0 ] ]);
-  $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
+  $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_valid($self->order->delivery_term_id);
+  $self->{all_statuses}               = SL::DB::Manager::OrderStatus->get_all_sorted(where => [ or => [ id => $self->order->order_status_id,
+                                                                                                        obsolete => 0,  ] ] );
   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
@@ -1721,7 +2254,7 @@ sub pre_render {
                 no_queue           => 1,
                 no_postscript      => 1,
                 no_opendocument    => 0,
                 no_queue           => 1,
                 no_postscript      => 1,
                 no_opendocument    => 0,
-                no_html            => 1},
+                no_html            => 0},
   );
 
   foreach my $item (@{$self->order->orderitems}) {
   );
 
   foreach my $item (@{$self->order->orderitems}) {
@@ -1730,9 +2263,11 @@ sub pre_render {
     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
   }
 
     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
   }
 
-  if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
-    # calculate shipped qtys here to prevent calling calculate for every item via the items method
-    SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects;
+  if (any { $self->type eq $_ } (SALES_ORDER_INTAKE_TYPE(), SALES_ORDER_TYPE(), PURCHASE_ORDER_TYPE(), PURCHASE_ORDER_CONFIRMATION_TYPE())) {
+    # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
+    # Do not use write_to_objects to prevent order->delivered to be set, because this should be
+    # the value from db, which can be set manually or is set when linked delivery orders are saved.
+    SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
   }
 
   if ($self->order->number && $::instance_conf->get_webdav) {
   }
 
   if ($self->order->number && $::instance_conf->get_webdav) {
@@ -1747,35 +2282,102 @@ sub pre_render {
                                                 } } @all_objects;
   }
 
                                                 } } @all_objects;
   }
 
+  if (   (any { $self->type eq $_ } (SALES_QUOTATION_TYPE(), SALES_ORDER_INTAKE_TYPE(), SALES_ORDER_TYPE()))
+      && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
+    $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
+  }
+  $self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
+  $self->{template_args}->{order_item_input_position} = SL::Helper::UserPreferences::ItemInputPosition->new()->get_order_item_input_position
+                                                      // $::instance_conf->get_order_item_input_position;
+
   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
-  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
-                                                         edit_periodic_invoices_config calculate_qty kivi.Validator follow_up);
+  $self->{template_args}->{num_phone_notes} = scalar @{ $self->order->phone_notes || [] };
+
+  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File
+                                                         edit_periodic_invoices_config calculate_qty follow_up show_history);
   $self->setup_edit_action_bar;
 }
 
 sub setup_edit_action_bar {
   my ($self, %params) = @_;
 
   $self->setup_edit_action_bar;
 }
 
 sub setup_edit_action_bar {
   my ($self, %params) = @_;
 
-  my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
-                      || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
-                      || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
+  my @valid = qw(
+    kivi.Order.check_cv
+  );
+  push @valid, "kivi.Order.check_duplicate_parts" if $::instance_conf->get_order_warn_duplicate_parts;
+  push @valid, "kivi.Order.check_valid_reqdate"   if $::instance_conf->get_order_warn_no_deliverydate;
+  my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
+  my @req_cusordnumber   = qw(kivi.Order.check_cusordnumber_presence)           x(( any {$self->type eq $_} (SALES_ORDER_INTAKE_TYPE(), SALES_ORDER_TYPE()) ) && $::instance_conf->get_order_warn_no_cusordnumber);
+
+  my $has_invoice_for_advance_payment;
+  if ($self->order->id && $self->type eq SALES_ORDER_TYPE()) {
+    my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
+    $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
+  }
+
+  my $has_final_invoice;
+  if ($self->order->id && $self->type eq SALES_ORDER_TYPE()) {
+    my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
+    $has_final_invoice               = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
+  }
+
+  my $may_edit_create   = $::auth->assert($self->type_data->rights('edit'), 'may fail');
+
+  my $is_final_version = $self->is_final_version;
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
       combobox => [
         action => [
           t8('Save'),
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
       combobox => [
         action => [
           t8('Save'),
-          call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
-                                                    $::instance_conf->get_order_warn_no_deliverydate,
-                                                                                                      ],
-          checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'] ],
+          call      => [ 'kivi.Order.save', {
+              action             => 'save',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+            }],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : $is_final_version ? t8('This record is the final version. Please create a new sub-version') : undef,
+        ],
+        action => [
+          t8('Save and Close'),
+          call      => [ 'kivi.Order.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 },
+              ],
+            }],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : $is_final_version ? t8('This record is the final version. Please create a new sub-version') : undef,
+        ],
+        action => [
+          t8('Create Sub-Version'),
+          call      => [ 'kivi.Order.save', { action => 'add_subversion' } ],
+          only_if   => $::instance_conf->get_lock_oe_subversions,
+          disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
+                    : !$is_final_version ? t8('This sub-version is not yet finalized')
+                    :                      undef,
         ],
         action => [
           t8('Save as new'),
         ],
         action => [
           t8('Save as new'),
-          call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
-          checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
-          disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          call      => [ 'kivi.Order.save', {
+              action             => 'save_as_new',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+            }],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          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,
         ],
       ], # end of combobox "Save"
 
         ],
       ], # end of combobox "Save"
 
@@ -1784,32 +2386,179 @@ sub setup_edit_action_bar {
           t8('Workflow'),
         ],
         action => [
           t8('Workflow'),
         ],
         action => [
-          t8('Save and Sales Order'),
-          submit   => [ '#order_form', { action => "Order/sales_order" } ],
-          only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
+          t8('Save and Quotation'),
+          call     => [ 'kivi.submit_ajax_form', $self->url_for(action => "save_and_order_workflow", to_type => SALES_QUOTATION_TYPE()), '#order_form' ],
+          checks   => [ @valid, @req_trans_cost_art, @req_cusordnumber ],
+          only_if  => $self->type_data->show_menu('save_and_quotation'),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and RFQ'),
+          call     => [ 'kivi.Order.purchase_check_for_direct_delivery', { to_type => REQUEST_QUOTATION_TYPE() } ],
+          checks   => [ @valid ],
+          only_if  => $self->type_data->show_menu('save_and_rfq'),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Purchase Quotation Intake'),
+          call     => [ 'kivi.submit_ajax_form', $self->url_for(action => "save_and_order_workflow", to_type => PURCHASE_QUOTATION_INTAKE_TYPE()), '#order_form' ],
+          only_if  => $self->type_data->show_menu('save_and_purchase_quotation_intake'),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Sales Order Intake'),
+          call     => [ 'kivi.submit_ajax_form', $self->url_for(action => "save_and_order_workflow", to_type => SALES_ORDER_INTAKE_TYPE()), '#order_form' ],
+          only_if  => $self->type_data->show_menu('save_and_sales_order_intake'),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Sales Order Confirmation'),
+          call     => [ 'kivi.submit_ajax_form', $self->url_for(action => "save_and_order_workflow", to_type => SALES_ORDER_TYPE()), '#order_form' ],
+          checks   => [ @valid, @req_trans_cost_art ],
+          only_if  => $self->type_data->show_menu('save_and_sales_order'),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
         ],
         action => [
           t8('Save and Purchase Order'),
         ],
         action => [
           t8('Save and Purchase Order'),
-          call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
-          only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
+          call      => [ 'kivi.Order.purchase_check_for_direct_delivery', { to_type => PURCHASE_ORDER_TYPE() } ],
+          checks    => [ @valid, @req_trans_cost_art, @req_cusordnumber ],
+          only_if   => $self->type_data->show_menu('save_and_purchase_order'),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Purchase Order Confirmation'),
+          call      => [ 'kivi.Order.purchase_check_for_direct_delivery', { to_type => PURCHASE_ORDER_CONFIRMATION_TYPE() } ],
+          checks    => [ @valid, @req_trans_cost_art, @req_cusordnumber ],
+          only_if   => $self->type_data->show_menu('save_and_purchase_order_confirmation'),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Sales Delivery Order'),
+          call      => [ 'kivi.Order.save', {
+              action             => 'save_and_new_record',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+              form_params        => [
+                { name => 'to_type', value => SALES_DELIVERY_ORDER_TYPE() },
+              ],
+            }],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          only_if   => $self->type_data->show_menu('save_and_sales_delivery_order'),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Purchase Delivery Order'),
+          call      => [ 'kivi.Order.save', {
+              action             => 'save_and_new_record',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+              form_params        => [
+                { name => 'to_type', value => PURCHASE_DELIVERY_ORDER_TYPE() },
+              ],
+            }],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          only_if   => $self->type_data->show_menu('save_and_purchase_delivery_order'),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Purchase Delivery Order with item selection'),
+          call      => [
+            'kivi.Order.show_purchase_delivery_order_select_items', {
+              action             => 'save_and_new_record',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+              form_params        => [
+                { name => 'to_type', value => PURCHASE_DELIVERY_ORDER_TYPE() },
+              ],
+            }],
+          checks    => [ @req_trans_cost_art, @req_cusordnumber ],
+          only_if   => $self->type_data->show_menu('save_and_purchase_delivery_order'),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Supplier Delivery Order'),
+          call      => [ 'kivi.Order.save', {
+              action             => 'save_and_new_record',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+              form_params        => [
+                { name => 'to_type', value => SUPPLIER_DELIVERY_ORDER_TYPE() },
+              ],
+            }],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          only_if   => $self->type_data->show_menu('save_and_supplier_delivery_order'),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
         ],
         action => [
         ],
         action => [
-          t8('Save and Delivery Order'),
-          call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
-                                                                       $::instance_conf->get_order_warn_no_deliverydate,
-                                                                                                                        ],
-          checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
-          only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
+          t8('Save and Reclamation'),
+          call      => [ 'kivi.Order.save', {
+              action             => 'save_and_new_record',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+              form_params        => [
+                { name => 'to_type',
+                  value => $self->order->is_sales ? SALES_RECLAMATION_TYPE()
+                                                  : PURCHASE_RECLAMATION_TYPE() },
+              ],
+            }],
+          only_if   => $self->type_data->show_menu('save_and_reclamation'),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
         ],
         action => [
           t8('Save and Invoice'),
         ],
         action => [
           t8('Save and Invoice'),
-          call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
-          checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
+          call      => [ 'kivi.Order.save', {
+              action             => 'save_and_invoice',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+            }],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+          only_if   => $self->type_data->show_menu('save_and_invoice'),
+        ],
+        action => [
+          ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
+          call      => [ 'kivi.Order.save', {
+              action             => 'save_and_invoice_for_advance_payment',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+            }],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
+                     : $has_final_invoice ? t8('This order has already a final invoice.')
+                     :                      undef,
+          only_if   => $self->type_data->show_menu('save_and_invoice_for_advance_payment'),
+        ],
+        action => [
+          t8('Save and Final Invoice'),
+          call      => [ 'kivi.Order.save', {
+              action             => 'save_and_final_invoice',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+            }],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
+                     : $has_final_invoice ? t8('This order has already a final invoice.')
+                     :                      undef,
+          only_if   => $self->type_data->show_menu('save_and_final_invoice') && $has_invoice_for_advance_payment,
         ],
         action => [
           t8('Save and AP Transaction'),
         ],
         action => [
           t8('Save and AP Transaction'),
-          call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
-          only_if   => (any { $self->type eq $_ } (purchase_order_type()))
+          call      => [ 'kivi.Order.save', {
+              action             => 'save_and_ap_transaction',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+            }],
+          only_if   => $self->type_data->show_menu('save_and_ap_transaction'),
+          disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
         ],
 
       ], # end of combobox "Workflow"
         ],
 
       ], # end of combobox "Workflow"
@@ -1818,14 +2567,40 @@ sub setup_edit_action_bar {
         action => [
           t8('Export'),
         ],
         action => [
           t8('Export'),
         ],
+        action => [
+          t8('Save and preview PDF'),
+          call     => [ 'kivi.Order.save', {
+              action             => 'preview_pdf',
+              warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+              warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
+            }],
+          checks   => [ @req_trans_cost_art, @req_cusordnumber ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : $is_final_version ? t8('This record is the final version. Please create a new sub-version') : undef,
+          only_if  => $self->type_data->show_menu('save_and_print'),
+        ],
         action => [
           t8('Save and print'),
         action => [
           t8('Save and print'),
-          call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ],
+          call     => [ 'kivi.Order.show_print_options', { warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
+                                                           warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate },
+          ],
+          checks   => [ @req_trans_cost_art, @req_cusordnumber ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : $is_final_version ? t8('This record is the final version. Please create a new sub-version') : undef,
+          only_if  => $self->type_data->show_menu('save_and_print'),
         ],
         action => [
         ],
         action => [
-          t8('Save and E-mail'),
-          call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts ],
-          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          ($is_final_version ? t8('E-mail') : t8('Save and E-mail')),
+          id       => 'save_and_email_action',
+          call     => [ 'kivi.Order.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,
+          only_if  => $self->type_data->show_menu('save_and_email'),
         ],
         action => [
           t8('Download attachments of all parts'),
         ],
         action => [
           t8('Download attachments of all parts'),
@@ -1839,14 +2614,21 @@ sub setup_edit_action_bar {
         t8('Delete'),
         call     => [ 'kivi.Order.delete_order' ],
         confirm  => $::locale->text('Do you really want to delete this object?'),
         t8('Delete'),
         call     => [ 'kivi.Order.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.') : undef,
-        only_if  => $deletion_allowed,
+        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  => $self->type_data->show_menu('delete'),
       ],
 
       combobox => [
         action => [
           t8('more')
         ],
       ],
 
       combobox => [
         action => [
           t8('more')
         ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $self->order->id, 'id' ],
+          disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
+        ],
         action => [
           t8('Follow-Up'),
           call     => [ 'kivi.Order.follow_up_window' ],
         action => [
           t8('Follow-Up'),
           call     => [ 'kivi.Order.follow_up_window' ],
@@ -1858,9 +2640,10 @@ sub setup_edit_action_bar {
   }
 }
 
   }
 }
 
-sub generate_pdf {
-  my ($order, $pdf_ref, $params) = @_;
+sub generate_doc {
+  my ($self, $doc_ref, $params) = @_;
 
 
+  my $order  = $self->order;
   my @errors = ();
 
   my $print_form = Form->new('');
   my @errors = ();
 
   my $print_form = Form->new('');
@@ -1880,6 +2663,9 @@ sub generate_pdf {
   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
     $template_ext  = 'odt';
     $template_type = 'OpenDocument';
   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
     $template_ext  = 'odt';
     $template_type = 'OpenDocument';
+  } elsif ($print_form->{format} =~ m{html}i) {
+    $template_ext  = 'html';
+    $template_type = 'HTML';
   }
 
   # search for the template
   }
 
   # search for the template
@@ -1901,7 +2687,7 @@ sub generate_pdf {
     eval {
       $print_form->prepare_for_printing;
 
     eval {
       $print_form->prepare_for_printing;
 
-      $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
+      $$doc_ref = SL::Helper::CreatePDF->create_pdf(
         format        => $print_form->{format},
         template_type => $template_type,
         template      => $template_file,
         format        => $print_form->{format},
         template_type => $template_type,
         template      => $template_file,
@@ -1910,6 +2696,7 @@ sub generate_pdf {
           longdescription => 'html',
           partnotes       => 'html',
           notes           => 'html',
           longdescription => 'html',
           partnotes       => 'html',
           notes           => 'html',
+          $::form->get_variable_content_types_for_cvars,
         },
       );
       1;
         },
       );
       1;
@@ -1930,6 +2717,7 @@ sub get_files_for_email_dialog {
     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
+    $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
   }
 
   my @parts =
   }
 
   my @parts =
@@ -1964,7 +2752,7 @@ sub make_periodic_invoices_config_from_yaml {
 sub get_periodic_invoices_status {
   my ($self, $config) = @_;
 
 sub get_periodic_invoices_status {
   my ($self, $config) = @_;
 
-  return                      if $self->type ne sales_order_type();
+  return                      if $self->type ne SALES_ORDER_TYPE();
   return t8('not configured') if !$config;
 
   my $active = ('HASH' eq ref $config)                           ? $config->{active}
   return t8('not configured') if !$config;
 
   my $active = ('HASH' eq ref $config)                           ? $config->{active}
@@ -1974,29 +2762,6 @@ sub get_periodic_invoices_status {
   return $active ? t8('active') : t8('inactive');
 }
 
   return $active ? t8('active') : t8('inactive');
 }
 
-sub get_title_for {
-  my ($self, $action) = @_;
-
-  return '' if none { lc($action)} qw(add edit);
-
-  # for locales:
-  # $::locale->text("Add Sales Order");
-  # $::locale->text("Add Purchase Order");
-  # $::locale->text("Add Quotation");
-  # $::locale->text("Add Request for Quotation");
-  # $::locale->text("Edit Sales Order");
-  # $::locale->text("Edit Purchase Order");
-  # $::locale->text("Edit Quotation");
-  # $::locale->text("Edit Request for Quotation");
-
-  $action = ucfirst(lc($action));
-  return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
-       : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
-       : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
-       : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
-       : '';
-}
-
 sub get_item_cvpartnumber {
   my ($self, $item) = @_;
 
 sub get_item_cvpartnumber {
   my ($self, $item) = @_;
 
@@ -2036,48 +2801,85 @@ sub get_part_texts {
   return $texts;
 }
 
   return $texts;
 }
 
-sub sales_order_type {
-  'sales_order';
-}
+sub nr_key {
+  my ($self) = @_;
 
 
-sub purchase_order_type {
-  'purchase_order';
+  return $self->type_data->properties('nr_key');
 }
 
 }
 
-sub sales_quotation_type {
-  'sales_quotation';
-}
+sub save_and_redirect_to {
+  my ($self, %params) = @_;
+
+  $self->save();
 
 
-sub request_quotation_type {
-  'request_quotation';
+  flash_later('info', $self->type_data->text('saved'));
+
+  $self->redirect_to(%params, id => $self->order->id);
 }
 
 }
 
-sub nr_key {
-  return $_[0]->type eq sales_order_type()       ? 'ordnumber'
-       : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
-       : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
-       : $_[0]->type eq request_quotation_type() ? 'quonumber'
-       : '';
+sub save_history {
+  my ($self, $addition) = @_;
+
+  my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
+  my $snumbers    = $number_type . '_' . $self->order->$number_type;
+
+  SL::DB::History->new(
+    trans_id    => $self->order->id,
+    employee_id => SL::DB::Manager::Employee->current->id,
+    what_done   => $self->order->type,
+    snumbers    => $snumbers,
+    addition    => $addition,
+  )->save;
 }
 
 }
 
-sub save_and_redirect_to {
-  my ($self, %params) = @_;
+sub store_doc_to_webdav_and_filemanagement {
+  my ($self, $content, $filename, $variant) = @_;
 
 
-  my $errors = $self->save();
+  my $order = $self->order;
+  my @errors;
 
 
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
+  # copy file to webdav folder
+  if ($order->number && $::instance_conf->get_webdav_documents) {
+    my $webdav = SL::Webdav->new(
+      type     => $order->type,
+      number   => $order->number,
+    );
+    my $webdav_file = SL::Webdav::File->new(
+      webdav   => $webdav,
+      filename => $filename,
+    );
+    eval {
+      $webdav_file->store(data => \$content);
+      1;
+    } or do {
+      push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
+    };
+  }
+  my $file_obj;
+  if ($order->id && $::instance_conf->get_doc_storage) {
+    eval {
+      $file_obj = SL::File->save(object_id     => $order->id,
+                                 object_type   => $order->type,
+                                 mime_type     => SL::MIME->mime_type_from_ext($filename),
+                                 source        => 'created',
+                                 file_type     => 'document',
+                                 file_name     => $filename,
+                                 file_contents => $content,
+                                 print_variant => $variant);
+
+      $self->{file_id}  = $file_obj->id;
+      1;
+    } or do {
+      push @errors, t8('Storing the document in the storage backend failed: #1', $@);
+    };
   }
 
   }
 
-  my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
-           : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
-           : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
-           : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
-           : '';
-  flash_later('info', $text);
+  return @errors;
+}
 
 
-  $self->redirect_to(%params, id => $self->order->id);
+sub init_type_data {
+  my ($self) = @_;
+  SL::DB::Helper::TypeDataProxy->new('SL::DB::Order', $self->order->record_type);
 }
 
 1;
 }
 
 1;
@@ -2175,14 +2977,6 @@ One row for already entered items
 
 Displaying tax information
 
 
 Displaying tax information
 
-=item * C<template/webpages/order/tabs/_multi_items_dialog.html>
-
-Dialog for entering more than one item at once
-
-=item * C<template/webpages/order/tabs/_multi_items_result.html>
-
-Results for the filter in the multi items dialog
-
 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
 Dialog for selecting price and discount sources
 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
 Dialog for selecting price and discount sources
@@ -2201,10 +2995,6 @@ java script functions
 
 =item * testing
 
 
 =item * testing
 
-=item * credit limit
-
-=item * more workflows (quotation, rfq)
-
 =item * price sources: little symbols showing better price / better discount
 
 =item * select units in input row?
 =item * price sources: little symbols showing better price / better discount
 
 =item * select units in input row?
@@ -2215,14 +3005,12 @@ java script functions
 
 =item * display weights
 
 
 =item * display weights
 
-=item * history
-
 =item * mtime check
 
 =item * optional client/user behaviour
 
 (transactions has to be set - department has to be set -
 =item * mtime check
 
 =item * optional client/user behaviour
 
 (transactions has to be set - department has to be set -
- force project if enabled in client config - transport cost reminder)
+ force project if enabled in client config)
 
 =back
 
 
 =back
 
@@ -2232,21 +3020,10 @@ java script functions
 
 =item *
 
 
 =item *
 
-Customer discount is not displayed as a valid discount in price source popup
-(this might be a bug in price sources)
-
-(I cannot reproduce this (Bernd))
-
-=item *
-
 No indication that <shift>-up/down expands/collapses second row.
 
 =item *
 
 No indication that <shift>-up/down expands/collapses second row.
 
 =item *
 
-Inline creation of parts is not currently supported
-
-=item *
-
 Table header is not sticky in the scrolling area.
 
 =item *
 Table header is not sticky in the scrolling area.
 
 =item *
@@ -2256,11 +3033,6 @@ Sorting does not include C<position>, neither does reordering.
 This behavior was implemented intentionally. But we can discuss, which behavior
 should be implemented.
 
 This behavior was implemented intentionally. But we can discuss, which behavior
 should be implemented.
 
-=item *
-
-C<show_multi_items_dialog> does not use the currently inserted string for
-filtering.
-
 =back
 
 =head1 To discuss / Nice to have
 =back
 
 =head1 To discuss / Nice to have
@@ -2274,10 +3046,6 @@ How to expand/collapse second row. Now it can be done clicking the icon or
 
 =item *
 
 
 =item *
 
-Possibility to select PriceSources in input row?
-
-=item *
-
 This controller uses a (changed) copy of the template for the PriceSource
 dialog. Maybe there could be used one code source.
 
 This controller uses a (changed) copy of the template for the PriceSource
 dialog. Maybe there could be used one code source.
 
@@ -2294,7 +3062,7 @@ editor or on text processing application).
 
 =item *
 
 
 =item *
 
-A warning when leaving the page without saveing unchanged inputs.
+A warning when leaving the page without saving unchanged inputs.
 
 
 =back
 
 
 =back