Auftrags-Controller: E-Mail-Dialog: nach Speichern hiddens der ids wieder setzen
[kivitendo-erp.git] / SL / Controller / Order.pm
index d4395b5..de1fad5 100644 (file)
@@ -4,48 +4,70 @@ use strict;
 use parent qw(SL::Controller::Base);
 
 use SL::Helper::Flash qw(flash_later);
 use parent qw(SL::Controller::Base);
 
 use SL::Helper::Flash qw(flash_later);
-use SL::Presenter::Tag qw(select_tag hidden_tag);
+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::PriceSource;
 use SL::Webdav;
 use SL::File;
 use SL::Locale::String qw(t8);
 use SL::SessionFile::Random;
 use SL::PriceSource;
 use SL::Webdav;
 use SL::File;
+use SL::MIME;
 use SL::Util qw(trim);
 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::Default;
 use SL::DB::Unit;
 use SL::DB::Part;
 use SL::DB::Order;
 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::Printer;
+use SL::DB::Note;
 use SL::DB::Language;
 use SL::DB::RecordLink;
 use SL::DB::Language;
 use SL::DB::RecordLink;
+use SL::DB::RequirementSpec;
+use SL::DB::Shipto;
+use SL::DB::Translation;
 
 use SL::Helper::CreatePDF qw(:all);
 use SL::Helper::PrintOptions;
 
 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::Controller::Helper::GetModels;
 
 
 use SL::Controller::Helper::GetModels;
 
-use List::Util qw(first);
+use List::Util qw(first sum0);
 use List::UtilsBy qw(sort_by uniq_by);
 use List::MoreUtils qw(any none pairwise first_index);
 use English qw(-no_match_vars);
 use File::Spec;
 use Cwd;
 use List::UtilsBy qw(sort_by uniq_by);
 use List::MoreUtils qw(any none pairwise first_index);
 use English qw(-no_match_vars);
 use File::Spec;
 use Cwd;
+use Sort::Naturally;
 
 use Rose::Object::MakeMethods::Generic
 (
 
 use Rose::Object::MakeMethods::Generic
 (
- scalar => [ qw(item_ids_to_delete) ],
- 'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
+ scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
+ 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
 );
 
 
 # safety
 );
 
 
 # safety
-__PACKAGE__->run_before('_check_auth');
+__PACKAGE__->run_before('check_auth');
 
 
-__PACKAGE__->run_before('_recalc',
-                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
+__PACKAGE__->run_before('check_auth_for_edit',
+                        except => [ qw(edit show_customer_vendor_details_dialog price_popup load_second_rows) ]);
 
 
-__PACKAGE__->run_before('_get_unalterable_data',
-                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
+__PACKAGE__->run_before('recalc',
+                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_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_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
+                                     print send_email) ]);
 
 #
 # actions
 
 #
 # actions
@@ -56,13 +78,20 @@ sub action_add {
   my ($self) = @_;
 
   $self->order->transdate(DateTime->now_local());
   my ($self) = @_;
 
   $self->order->transdate(DateTime->now_local());
-  my $extra_days = $self->type eq _sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1;
-  $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
+  my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
+                   $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
+
+  if (   ($self->type eq sales_order_type()     &&  $::instance_conf->get_deliverydate_on)
+      || ($self->type eq sales_quotation_type() &&  $::instance_conf->get_reqdate_on)
+      && (!$self->order->reqdate)) {
+    $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
+  }
+
 
 
-  $self->_pre_render();
+  $self->pre_render();
   $self->render(
     'order/form',
   $self->render(
     'order/form',
-    title => $self->_get_title_for('add'),
+    title => $self->get_title_for('add'),
     %{$self->{template_args}}
   );
 }
     %{$self->{template_args}}
   );
 }
@@ -71,31 +100,76 @@ sub action_add {
 sub action_edit {
   my ($self) = @_;
 
 sub action_edit {
   my ($self) = @_;
 
-  $self->_load_order;
-  $self->_recalc();
-  $self->_pre_render();
+  if ($::form->{id}) {
+    $self->load_order;
+
+  } else {
+    # this is to edit an order from an unsaved order object
+
+    # set item ids to new fake id, to identify them as new items
+    foreach my $item (@{$self->order->items_sorted}) {
+      $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+    }
+    # trigger rendering values for second row as hidden, because they
+    # are loaded only on demand. So we need to keep the values from
+    # the source.
+    $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+  }
+
+  $self->recalc();
+  $self->pre_render();
   $self->render(
     'order/form',
   $self->render(
     'order/form',
-    title => $self->_get_title_for('edit'),
+    title => $self->get_title_for('edit'),
     %{$self->{template_args}}
   );
 }
 
     %{$self->{template_args}}
   );
 }
 
+# edit a collective order (consisting of one or more existing orders)
+sub action_edit_collective {
+  my ($self) = @_;
+
+  # collect order ids
+  my @multi_ids = map {
+    $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
+  } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
+
+  # fall back to add if no ids are given
+  if (scalar @multi_ids == 0) {
+    $self->action_add();
+    return;
+  }
+
+  # fall back to save as new if only one id is given
+  if (scalar @multi_ids == 1) {
+    $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
+    $self->action_save_as_new();
+    return;
+  }
+
+  # 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'));
+
+  $self->action_edit();
+}
+
 # delete the order
 sub action_delete {
   my ($self) = @_;
 
 # delete the order
 sub action_delete {
   my ($self) = @_;
 
-  my $errors = $self->_delete();
+  my $errors = $self->delete();
 
   if (scalar @{ $errors }) {
     $self->js->flash('error', $_) foreach @{ $errors };
     return $self->js->render();
   }
 
 
   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')
+  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')
            : '';
   flash_later('info', $text);
 
            : '';
   flash_later('info', $text);
 
@@ -111,25 +185,33 @@ sub action_delete {
 sub action_save {
   my ($self) = @_;
 
 sub action_save {
   my ($self) = @_;
 
-  my $errors = $self->_save();
+  my $errors = $self->save();
 
   if (scalar @{ $errors }) {
     $self->js->flash('error', $_) foreach @{ $errors };
     return $self->js->render();
   }
 
 
   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 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')
+  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);
 
            : '';
   flash_later('info', $text);
 
-  my @redirect_params = (
-    action => 'edit',
-    type   => $self->type,
-    id     => $self->order->id,
-  );
+  my @redirect_params;
+  if ($::form->{back_to_caller}) {
+    @redirect_params = $::form->{callback} ? ($::form->{callback})
+                                           : (controller => 'LoginScreen', action => 'user_login');
+
+  } else {
+    @redirect_params = (
+      action   => 'edit',
+      type     => $self->type,
+      id       => $self->order->id,
+      callback => $::form->{callback},
+    );
+  }
 
   $self->redirect_to(@redirect_params);
 }
 
   $self->redirect_to(@redirect_params);
 }
@@ -160,10 +242,17 @@ sub action_save_as_new {
                         ? DateTime->today_local
                         : $order->transdate;
 
                         ? DateTime->today_local
                         : $order->transdate;
 
-  # Set new reqdate unless changed
+  # Set new reqdate unless changed if it is enabled in client config
   if ($order->reqdate == $saved_order->reqdate) {
   if ($order->reqdate == $saved_order->reqdate) {
-    my $extra_days = $self->type eq _sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1;
-    $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
+    my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
+                     $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
+
+    if (   ($self->type eq sales_order_type()     &&  !$::instance_conf->get_deliverydate_on)
+        || ($self->type eq sales_quotation_type() &&  !$::instance_conf->get_reqdate_on)) {
+      $new_attrs{reqdate} = '';
+    } else {
+      $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
+    }
   } else {
     $new_attrs{reqdate} = $order->reqdate;
   }
   } else {
     $new_attrs{reqdate} = $order->reqdate;
   }
@@ -171,6 +260,10 @@ sub action_save_as_new {
   # Update employee
   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
   # Update employee
   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
+  # Warn on obsolete items
+  my @obsolete_positions = map { $_->position } grep { $_->part->obsolete } @{ $order->items_sorted };
+  flash_later('warning', t8('This record containts 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));
 
   # Create new record from current one
   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
@@ -184,22 +277,29 @@ sub action_save_as_new {
 # print the order
 #
 # This is called if "print" is pressed in the print dialog.
 # print the order
 #
 # This is called if "print" is pressed in the print dialog.
-# If PDF creation was requested and succeeded, the pdf is stored in a session
-# file and the filename is stored as session value with an unique key. A
-# javascript function with this key is then called. This function calls the
-# download action below (action_download_pdf), which offers the file for
-# download.
+# If PDF creation was requested and succeeded, the pdf is offered for download
+# via send_file (which uses ajax in this case).
 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->js_reset_order_and_item_ids_after_save;
+
   my $format      = $::form->{print_options}->{format};
   my $media       = $::form->{print_options}->{media};
   my $formname    = $::form->{print_options}->{formname};
   my $copies      = $::form->{print_options}->{copies};
   my $groupitems  = $::form->{print_options}->{groupitems};
   my $format      = $::form->{print_options}->{format};
   my $media       = $::form->{print_options}->{media};
   my $formname    = $::form->{print_options}->{formname};
   my $copies      = $::form->{print_options}->{copies};
   my $groupitems  = $::form->{print_options}->{groupitems};
+  my $printer_id  = $::form->{print_options}->{printer_id};
 
 
-  # only pdf by now
-  if (none { $format eq $_ } qw(pdf)) {
+  # only PDF, OpenDocument & HTML for now
+  if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
   }
 
     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
   }
 
@@ -208,104 +308,116 @@ sub action_print {
     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
   }
 
     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
   }
 
-  my $language;
-  $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
-
   # create a form for generate_attachment_filename
   # create a form for generate_attachment_filename
-  my $form = Form->new;
-  $form->{ordnumber} = $self->order->ordnumber;
-  $form->{type}      = $self->type;
-  $form->{format}    = $format;
-  $form->{formname}  = $formname;
-  $form->{language}  = '_' . $language->template_code if $language;
-  my $pdf_filename   = $form->generate_attachment_filename();
-
-  my $pdf;
-  my @errors = _create_pdf($self->order, \$pdf, { format     => $format,
-                                                  formname   => $formname,
-                                                  language   => $language,
-                                                  groupitems => $groupitems });
+  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 $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;
+    return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
   }
 
   if ($media eq 'screen') {
     # screen/download
   }
 
   if ($media eq 'screen') {
     # screen/download
-    my $sfile = SL::SessionFile::Random->new(mode => "w");
-    $sfile->fh->print($pdf);
-    $sfile->fh->close;
-
-    my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
-    $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
-
-    $self->js
-    ->run('kivi.Order.download_pdf', $pdf_filename, $key)
-    ->flash('info', t8('The PDF has been created'));
+    $self->js->flash('info', t8('The document has been created.'));
+    $self->send_file(
+      \$doc,
+      type         => SL::MIME->mime_type_from_ext($doc_filename),
+      name         => $doc_filename,
+      js_no_render => 1,
+    );
 
   } elsif ($media eq 'printer') {
     # printer
     my $printer_id = $::form->{print_options}->{printer_id};
     SL::DB::Printer->new(id => $printer_id)->load->print_document(
       copies  => $copies,
 
   } elsif ($media eq 'printer') {
     # printer
     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'));
+    $self->js->flash('info', t8('The document has been printed.'));
   }
 
   }
 
-  # copy file to webdav folder
-  if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
-    my $webdav = SL::Webdav->new(
-      type     => $self->type,
-      number   => $self->order->ordnumber,
-    );
-    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->ordnumber && $::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) {
+    $self->js->flash('warning', $_) for @warnings;
   }
   }
-  $self->js->render;
-}
 
 
-# offer pdf for download
-#
-# It needs to get the key for the session value to get the pdf file.
-sub action_download_pdf {
+  $self->save_history('PRINTED');
+
+  $self->js
+    ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
+    ->render;
+}
+sub action_preview_pdf {
   my ($self) = @_;
 
   my ($self) = @_;
 
-  my $key = $::form->{key};
-  my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
-  return $self->send_file(
-    $tmp_filename,
-    type => 'application/pdf',
-    name => $::form->{pdf_filename},
+  my $errors = $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 $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) {
+    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
+  }
+  $self->save_history('PREVIEWED');
+  $self->js->flash('info', t8('The PDF has been previewed'));
+  # screen/download
+  $self->send_file(
+    \$pdf,
+    type         => SL::MIME->mime_type_from_ext($pdf_filename),
+    name         => $pdf_filename,
+    js_no_render => 0,
   );
 }
 
 # open the email dialog
   );
 }
 
 # open the email dialog
-sub action_show_email_dialog {
+sub action_save_and_show_email_dialog {
   my ($self) = @_;
 
   my ($self) = @_;
 
+  my $errors = $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 $cv_method = $self->cv;
 
   if (!$self->order->$cv_method) {
   my $cv_method = $self->cv;
 
   if (!$self->order->$cv_method) {
@@ -321,23 +433,37 @@ sub action_show_email_dialog {
   # Todo: get addresses from shipto, if any
 
   my $form = Form->new;
   # Todo: get addresses from shipto, if any
 
   my $form = Form->new;
-  $form->{ordnumber} = $self->order->ordnumber;
-  $form->{formname}  = $self->type;
-  $form->{type}      = $self->type;
-  $form->{language} = 'de';
-  $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;
 
   $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()';
 
 
   $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 %files = $self->get_files_for_email_dialog();
+
+  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 $all_partner_email_addresses = $self->order->customervendor->get_all_email_addresses();
+
   my $dialog_html = $self->render('common/_send_email_dialog', { output => 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->cv eq 'customer',
+                                  email_form    => $email_form,
+                                  show_bcc      => $::auth->assert('email_bcc', 'may fail'),
+                                  FILES         => \%files,
+                                  is_customer   => $self->cv eq 'customer',
+                                  ALL_EMPLOYEES => \@employees_with_email,
+                                  ALL_PARTNER_EMAIL_ADDRESSES => $all_partner_email_addresses,
   );
 
   $self->js
   );
 
   $self->js
@@ -352,7 +478,23 @@ sub action_show_email_dialog {
 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();
+  }
+
+  $self->js_reset_order_and_item_ids_after_save;
+
   my $email_form  = delete $::form->{email_form};
   my $email_form  = delete $::form->{email_form};
+
+  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 };
   my %field_names = (to => 'email');
 
   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
@@ -361,48 +503,74 @@ sub action_send_email {
   $::form->{cwd}    = getcwd();
   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
   $::form->{cwd}    = getcwd();
   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
+  $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
   $::form->{media}  = 'email';
 
   $::form->{media}  = 'email';
 
-  if (($::form->{attachment_policy} // '') eq 'normal') {
-    my $language;
-    $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
+  $::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,
+                                 file_type     => 'document',
+                                 print_variant => $::form->{formname});
+  }
 
 
-    my $pdf;
-    my @errors = _create_pdf($self->order, \$pdf, {media      => $::form->{media},
-                                                   format     => $::form->{print_options}->{format},
-                                                   formname   => $::form->{print_options}->{formname},
-                                                   language   => $language,
-                                                   groupitems => $::form->{print_options}->{groupitems}});
+  if ($::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);
+      return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
+    }
+
+    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;
     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
   }
 
     $sfile->fh->close;
 
     $::form->{tmpfile} = $sfile->file_name;
     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
   }
 
-  $::form->send_email(\%::myconfig, 'pdf');
+  $::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});
+
+  # 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);
+  }
 
 
-  # 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->save_history('MAILED');
 
 
-  $self->js
-      ->val('#order_intnotes', $intnotes)
-      ->run('kivi.Order.close_email_dialog')
-      ->flash('info', t8('The email has been sent.'))
-      ->render($self);
+  flash_later('info', t8('The email has been sent.'));
+
+  my @redirect_params = (
+    action => 'edit',
+    type   => $self->type,
+    id     => $self->order->id,
+  );
+
+  $self->redirect_to(@redirect_params);
 }
 
 # open the periodic invoices config dialog
 }
 
 # open the periodic invoices config dialog
@@ -412,20 +580,43 @@ sub action_send_email {
 sub action_show_periodic_invoices_config_dialog {
   my ($self) = @_;
 
 sub action_show_periodic_invoices_config_dialog {
   my ($self) = @_;
 
-  my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
+  my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
                                                    order_value_periodicity => 'p', # = same as periodicity
   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
                                                    order_value_periodicity => 'p', # = same as periodicity
-                                                   start_date_as_date      => $::form->{transdate} || $::form->current_date,
+                                                   start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
                                                    extend_automatically_by => 12,
                                                    active                  => 1,
                                                    email_subject           => GenericTranslations->get(
                                                                                 language_id      => $::form->{language_id},
                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
                                                    email_body              => GenericTranslations->get(
                                                    extend_automatically_by => 12,
                                                    active                  => 1,
                                                    email_subject           => 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);
 
@@ -437,6 +628,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} ]);
+    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 },
@@ -454,19 +649,19 @@ sub action_assign_periodic_invoices_config {
 
   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 
   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
-  my $config = { active                  => $::form->{active}     ? 1 : 0,
-                 terminated              => $::form->{terminated} ? 1 : 0,
-                 direct_debit            => $::form->{direct_debit} ? 1 : 0,
-                 periodicity             => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
-                 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
-                 start_date_as_date      => $::form->{start_date_as_date},
-                 end_date_as_date        => $::form->{end_date_as_date},
+  my $config = { active                     => $::form->{active}       ? 1 : 0,
+                 terminated                 => $::form->{terminated}   ? 1 : 0,
+                 direct_debit               => $::form->{direct_debit} ? 1 : 0,
+                 periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
+                 order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
+                 start_date_as_date         => $::form->{start_date_as_date},
+                 end_date_as_date           => $::form->{end_date_as_date},
                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
-                 print                   => $::form->{print} ? 1 : 0,
-                 printer_id              => $::form->{print} ? $::form->{printer_id} * 1 : undef,
-                 copies                  => $::form->{copies} * 1 ? $::form->{copies} : 1,
-                 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
-                 ar_chart_id             => $::form->{ar_chart_id} * 1,
+                 print                      => $::form->{print}      ? 1                         : 0,
+                 printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
+                 copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
+                 extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
+                 ar_chart_id                => $::form->{ar_chart_id} * 1,
                  send_email                 => $::form->{send_email} ? 1 : 0,
                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
                  email_recipient_address    => $::form->{email_recipient_address},
                  send_email                 => $::form->{send_email} ? 1 : 0,
                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
                  email_recipient_address    => $::form->{email_recipient_address},
@@ -475,9 +670,9 @@ sub action_assign_periodic_invoices_config {
                  email_body                 => $::form->{email_body},
                };
 
                  email_body                 => $::form->{email_body},
                };
 
-  my $periodic_invoices_config = YAML::Dump($config);
+  my $periodic_invoices_config = SL::YAML::Dump($config);
 
 
-  my $status = $self->_get_periodic_invoices_status($config);
+  my $status = $self->get_periodic_invoices_status($config);
 
   $self->js
     ->remove('#order_periodic_invoices_config')
 
   $self->js
     ->remove('#order_periodic_invoices_config')
@@ -491,11 +686,11 @@ sub action_assign_periodic_invoices_config {
 sub action_get_has_active_periodic_invoices {
   my ($self) = @_;
 
 sub action_get_has_active_periodic_invoices {
   my ($self) = @_;
 
-  my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
+  my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
   $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))
@@ -509,27 +704,20 @@ sub action_get_has_active_periodic_invoices {
 sub action_save_and_delivery_order {
   my ($self) = @_;
 
 sub action_save_and_delivery_order {
   my ($self) = @_;
 
-  my $errors = $self->_save();
-
-  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 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);
-
-  my @redirect_params = (
+  $self->save_and_redirect_to(
     controller => 'oe.pl',
     action     => 'oe_delivery_order_from_order',
     controller => 'oe.pl',
     action     => 'oe_delivery_order_from_order',
-    id         => $self->order->id,
   );
   );
+}
 
 
-  $self->redirect_to(@redirect_params);
+sub action_save_and_supplier_delivery_order {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller => 'controller.pl',
+    action     => 'DeliveryOrder/add_from_order',
+    type       => 'supplier_delivery_order',
+  );
 }
 
 # save the order and redirect to the frontend subroutine for a new
 }
 
 # save the order and redirect to the frontend subroutine for a new
@@ -537,37 +725,60 @@ sub action_save_and_delivery_order {
 sub action_save_and_invoice {
   my ($self) = @_;
 
 sub action_save_and_invoice {
   my ($self) = @_;
 
-  my $errors = $self->_save();
+  $self->save_and_redirect_to(
+    controller => 'oe.pl',
+    action     => 'oe_invoice_from_order',
+  );
+}
+
+sub action_save_and_invoice_for_advance_payment {
+  my ($self) = @_;
 
 
-  if (scalar @{ $errors }) {
-    $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render();
-  }
+  $self->save_and_redirect_to(
+    controller       => 'oe.pl',
+    action           => 'oe_invoice_from_order',
+    new_invoice_type => 'invoice_for_advance_payment',
+  );
+}
 
 
-  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);
+sub action_save_and_final_invoice {
+  my ($self) = @_;
 
 
-  my @redirect_params = (
-    controller => 'oe.pl',
-    action     => 'oe_invoice_from_order',
-    id         => $self->order->id,
+  $self->save_and_redirect_to(
+    controller       => 'oe.pl',
+    action           => 'oe_invoice_from_order',
+    new_invoice_type => 'final_invoice',
   );
   );
+}
 
 
-  $self->redirect_to(@redirect_params);
+# workflow from sales order to sales quotation
+sub action_sales_quotation {
+  $_[0]->workflow_sales_or_request_for_quotation();
+}
+
+# workflow from sales order to sales quotation
+sub action_request_for_quotation {
+  $_[0]->workflow_sales_or_request_for_quotation();
 }
 
 # workflow from sales quotation to sales order
 sub action_sales_order {
 }
 
 # workflow from sales quotation to sales order
 sub action_sales_order {
-  $_[0]->_workflow_sales_or_purchase_order();
+  $_[0]->workflow_sales_or_purchase_order();
 }
 
 # workflow from rfq to purchase order
 sub action_purchase_order {
 }
 
 # workflow from rfq to purchase order
 sub action_purchase_order {
-  $_[0]->_workflow_sales_or_purchase_order();
+  $_[0]->workflow_sales_or_purchase_order();
+}
+
+# workflow from purchase order to ap transaction
+sub action_save_and_ap_transaction {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller => 'ap.pl',
+    action     => 'add_from_purchase_order',
+  );
 }
 
 # set form elements in respect to a changed customer or vendor
 }
 
 # set form elements in respect to a changed customer or vendor
@@ -576,8 +787,8 @@ sub action_purchase_order {
 sub action_customer_vendor_changed {
   my ($self) = @_;
 
 sub action_customer_vendor_changed {
   my ($self) = @_;
 
-  _setup_order_from_cv($self->order);
-  $self->_recalc();
+  setup_order_from_cv($self->order);
+  $self->recalc();
 
   my $cv_method = $self->cv;
 
 
   my $cv_method = $self->cv;
 
@@ -588,25 +799,36 @@ sub action_customer_vendor_changed {
   }
 
   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
   }
 
   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
-    $self->js->show('#shipto_row');
+    $self->js->show('#shipto_selection');
   } else {
   } else {
-    $self->js->hide('#shipto_row');
+    $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('#business_info_row',      $self->build_business_info_row)
-    ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
-    ->val(        '#order_taxincluded',      $self->order->taxincluded)
-    ->val(        '#order_payment_id',       $self->order->payment_id)
-    ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
-    ->val(        '#order_intnotes',         $self->order->intnotes)
-    ->focus(      '#order_' . $self->cv . '_id');
-
-  $self->_js_redisplay_amounts_and_taxes;
+    ->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');
+
+  $self->js_redisplay_amounts_and_taxes;
+  $self->js_redisplay_cvpartnumbers;
   $self->js->render();
 }
 
   $self->js->render();
 }
 
@@ -631,6 +853,11 @@ sub action_show_customer_vendor_details_dialog {
   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
+  if ($is_customer) {
+    foreach my $entry (@{ $cv->additional_billing_addresses }) {
+      push @{ $details{ADDITIONAL_BILLING_ADDRESSES} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
+    }
+  }
   foreach my $entry (@{ $cv->shipto }) {
     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
   }
   foreach my $entry (@{ $cv->shipto }) {
     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
   }
@@ -654,39 +881,75 @@ sub action_unit_changed {
   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
-  $self->_recalc();
+  $self->recalc();
 
   $self->js
     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
   $self->js
     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
-  $self->_js_redisplay_line_values;
-  $self->_js_redisplay_amounts_and_taxes;
+  $self->js_redisplay_line_values;
+  $self->js_redisplay_amounts_and_taxes;
   $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->unit($item->part->unit);
+
+  my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
+
+  $self->js
+    ->val     ('#add_item_unit',                $item->unit)
+    ->val     ('#add_item_description',         $item->part->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};
 
-  my $item = _new_item($self->order, $form_attr);
+  my $item = new_item($self->order, $form_attr);
 
   $self->order->add_items($item);
 
 
   $self->order->add_items($item);
 
-  $self->_recalc();
+  $self->recalc();
+
+  $self->get_item_cvpartnumber($item);
 
   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
   my $row_as_html = $self->p->render('order/tabs/_row',
 
   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
   my $row_as_html = $self->p->render('order/tabs/_row',
-                                     ITEM              => $item,
-                                     ID                => $item_id,
-                                     TYPE              => $self->type,
-                                     ALL_PRICE_FACTORS => $self->all_price_factors
+                                     ITEM => $item,
+                                     ID   => $item_id,
+                                     SELF => $self,
   );
 
   );
 
-  $self->js
-    ->append('#row_table_id', $row_as_html);
+  if ($::form->{insert_before_item_id}) {
+    $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);
+  }
 
   if ( $item->part->is_assortment ) {
     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
   if ( $item->part->is_assortment ) {
     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
@@ -696,74 +959,54 @@ sub action_add_item {
                    unit     => $assortment_item->unit,
                    description => $assortment_item->part->description,
                  };
                    unit     => $assortment_item->unit,
                    description => $assortment_item->part->description,
                  };
-      my $item = _new_item($self->order, $attr);
+      my $item = new_item($self->order, $attr);
 
       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
       $item->discount(1) unless $assortment_item->charge;
 
       $self->order->add_items( $item );
 
       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
       $item->discount(1) unless $assortment_item->charge;
 
       $self->order->add_items( $item );
-      $self->_recalc();
+      $self->recalc();
+      $self->get_item_cvpartnumber($item);
       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
       my $row_as_html = $self->p->render('order/tabs/_row',
       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
       my $row_as_html = $self->p->render('order/tabs/_row',
-                                         ITEM              => $item,
-                                         ID                => $item_id,
-                                         TYPE              => $self->type,
-                                         ALL_PRICE_FACTORS => $self->all_price_factors
+                                         ITEM => $item,
+                                         ID   => $item_id,
+                                         SELF => $self,
       );
       );
-      $self->js
-        ->append('#row_table_id', $row_as_html);
+      if ($::form->{insert_before_item_id}) {
+        $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);
+      }
     };
   };
 
   $self->js
     ->val('.add_item_input', '')
     };
   };
 
   $self->js
     ->val('.add_item_input', '')
+    ->attr('.add_item_input', 'placeholder', '')
+    ->attr('.add_item_input', 'title', '')
     ->run('kivi.Order.init_row_handlers')
     ->run('kivi.Order.init_row_handlers')
-    ->run('kivi.Order.row_table_scroll_down')
     ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
 
     ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
 
-  $self->_js_redisplay_amounts_and_taxes;
-  $self->js->render();
-}
-
-# open the dialog for entering multiple items at once
-sub action_show_multi_items_dialog {
-  require SL::DB::PartsGroup;
-  $_[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;
+  $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 
-  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);
-  }
+  $self->js_redisplay_amounts_and_taxes;
+  $self->js->render();
 }
 
 # 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;
   foreach my $attr (@form_attr) {
   return $self->js->render() unless scalar @form_attr;
 
   my @items;
   foreach my $attr (@form_attr) {
-    my $item = _new_item($self->order, $attr);
+    my $item = new_item($self->order, $attr);
     push @items, $item;
     if ( $item->part->is_assortment ) {
       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
     push @items, $item;
     if ( $item->part->is_assortment ) {
       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
@@ -772,7 +1015,7 @@ sub action_add_multi_items {
                      unit     => $assortment_item->unit,
                      description => $assortment_item->part->description,
                    };
                      unit     => $assortment_item->unit,
                      description => $assortment_item->part->description,
                    };
-        my $item = _new_item($self->order, $attr);
+        my $item = new_item($self->order, $attr);
 
         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
         $item->discount(1) unless $assortment_item->charge;
 
         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
         $item->discount(1) unless $assortment_item->charge;
@@ -782,28 +1025,35 @@ sub action_add_multi_items {
   }
   $self->order->add_items(@items);
 
   }
   $self->order->add_items(@items);
 
-  $self->_recalc();
+  $self->recalc();
 
   foreach my $item (@items) {
 
   foreach my $item (@items) {
+    $self->get_item_cvpartnumber($item);
     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
     my $row_as_html = $self->p->render('order/tabs/_row',
     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
     my $row_as_html = $self->p->render('order/tabs/_row',
-                                       ITEM              => $item,
-                                       ID                => $item_id,
-                                       TYPE              => $self->type,
-                                       ALL_PRICE_FACTORS => $self->all_price_factors
+                                       ITEM => $item,
+                                       ID   => $item_id,
+                                       SELF => $self,
     );
 
     );
 
-    $self->js->append('#row_table_id', $row_as_html);
+    if ($::form->{insert_before_item_id}) {
+      $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);
+    }
   }
 
   $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.init_row_handlers')
-    ->run('kivi.Order.row_table_scroll_down')
     ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
 
     ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
 
-  $self->_js_redisplay_amounts_and_taxes;
+  $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
+
+  $self->js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
 
   $self->js->render();
 }
 
@@ -811,31 +1061,54 @@ sub action_add_multi_items {
 sub action_recalc_amounts_and_taxes {
   my ($self) = @_;
 
 sub action_recalc_amounts_and_taxes {
   my ($self) = @_;
 
-  $self->_recalc();
+  $self->recalc();
 
 
-  $self->_js_redisplay_line_values;
-  $self->_js_redisplay_amounts_and_taxes;
+  $self->js_redisplay_line_values;
+  $self->js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
 
   $self->js->render();
 }
 
+sub action_update_exchangerate {
+  my ($self) = @_;
+
+  my $data = {
+    is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
+    currency_name => $self->order->currency->name,
+    exchangerate  => $self->order->daily_exchangerate_as_null_number,
+  };
+
+  $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
+}
+
 # redisplay item rows if they are sorted by an attribute
 sub action_reorder_items {
   my ($self) = @_;
 
   my %sort_keys = (
 # redisplay item rows if they are sorted by an attribute
 sub action_reorder_items {
   my ($self) = @_;
 
   my %sort_keys = (
-    partnumber  => sub { $_[0]->part->partnumber },
-    description => sub { $_[0]->description },
-    qty         => sub { $_[0]->qty },
-    sellprice   => sub { $_[0]->sellprice },
-    discount    => sub { $_[0]->discount },
+    partnumber   => sub { $_[0]->part->partnumber },
+    description  => sub { $_[0]->description },
+    qty          => sub { $_[0]->qty },
+    sellprice    => sub { $_[0]->sellprice },
+    discount     => sub { $_[0]->discount },
+    cvpartnumber => sub { $_[0]->{cvpartnumber} },
   );
 
   );
 
+  $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
   my $method = $sort_keys{$::form->{order_by}};
   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
   if ($::form->{sort_dir}) {
   my $method = $sort_keys{$::form->{order_by}};
   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
   if ($::form->{sort_dir}) {
-    @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
+    if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
+      @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
+    } else {
+      @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
+    }
   } else {
   } else {
-    @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
+    if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
+      @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
+    } else {
+      @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
+    }
   }
   $self->js
     ->run('kivi.Order.redisplay_items', \@to_sort)
   }
   $self->js
     ->run('kivi.Order.redisplay_items', \@to_sort)
@@ -852,20 +1125,59 @@ sub action_price_popup {
   $self->render_price_dialog($item);
 }
 
   $self->render_price_dialog($item);
 }
 
-# get the longdescription for an item if the dialog to enter/change the
-# longdescription was opened and the longdescription is empty
-#
-# If this item is new, get the longdescription from Part.
-# Otherwise get it from OrderItem.
-sub action_get_item_longdescription {
-  my $longdescription;
+# 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);
 
 
-  if ($::form->{item_id}) {
-    $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
-  } elsif ($::form->{parts_id}) {
-    $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
+  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});
+
+  # 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);
   }
   }
-  $_[0]->render(\ $longdescription, { type => 'text' });
+
+  $self->recalc();
+  $self->get_unalterable_data();
+  $self->pre_render();
+
+  # trigger rendering values for second row/longdescription as hidden,
+  # because they are loaded only on demand. So we need to keep the values
+  # from the source.
+  $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
+  $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
+
+  $self->render(
+    'order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+
 }
 
 # load the second row for one or more items
 }
 
 # load the second row for one or more items
@@ -875,13 +1187,13 @@ sub action_get_item_longdescription {
 sub action_load_second_rows {
   my ($self) = @_;
 
 sub action_load_second_rows {
   my ($self) = @_;
 
-  $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_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->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
@@ -889,10 +1201,103 @@ sub action_load_second_rows {
   $self->js->render();
 }
 
   $self->js->render();
 }
 
-sub _js_load_second_row {
-  my ($self, $item, $item_id, $do_parse) = @_;
+# update description, notes and sellprice from master data
+sub action_update_row_from_master_data {
+  my ($self) = @_;
 
 
-  if ($do_parse) {
+  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) = get_best_price_and_discount_source($self->order, $item, 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) = @_;
+
+  if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
+    return $self->js->flash('error', t8('Phone note needs a subject and a body.'))->render;
+  }
+
+  my $phone_note;
+  if ($::form->{phone_note}->{id}) {
+    $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;
+  }
+
+  $phone_note = SL::DB::Note->new() if !$phone_note;
+  my $is_new  = !$phone_note->id;
+
+  $phone_note->assign_attributes(%{ $::form->{phone_note} },
+                                 trans_id     => $self->order->id,
+                                 trans_module => 'oe',
+                                 employee     => SL::DB::Manager::Employee->current);
+
+  $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.'))
+    ->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;
+
+  $phone_note->delete;
+  $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', t8('Phone note has been deleted.'))
+    ->render;
+}
+
+sub js_load_second_row {
+  my ($self, $item, $item_id, $do_parse) = @_;
+
+  if ($do_parse) {
     # Parse values from form (they are formated while rendering (template)).
     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
     # Parse values from form (they are formated while rendering (template)).
     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
@@ -905,11 +1310,11 @@ sub _js_load_second_row {
   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
   $self->js
   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
   $self->js
-    ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
-    ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
+    ->html('#second_row_' . $item_id, $row_as_html)
+    ->data('#second_row_' . $item_id, 'loaded', 1);
 }
 
 }
 
-sub _js_redisplay_line_values {
+sub js_redisplay_line_values {
   my ($self) = @_;
 
   my $is_sales = $self->order->is_sales;
   my ($self) = @_;
 
   my $is_sales = $self->order->is_sales;
@@ -934,7 +1339,7 @@ sub _js_redisplay_line_values {
     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 }
 
     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 }
 
-sub _js_redisplay_amounts_and_taxes {
+sub js_redisplay_amounts_and_taxes {
   my ($self) = @_;
 
   if (scalar @{ $self->{taxes} }) {
   my ($self) = @_;
 
   if (scalar @{ $self->{taxes} }) {
@@ -949,6 +1354,19 @@ sub _js_redisplay_amounts_and_taxes {
     $self->js->show('#subtotal_row_id');
   }
 
     $self->js->show('#subtotal_row_id');
   }
 
+  if ($self->order->is_sales) {
+    my $is_neg = $self->order->marge_total < 0;
+    $self->js
+      ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
+      ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
+      ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
+      ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
+      ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
+      ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
+      ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
+      ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
+  }
+
   $self->js
     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
   $self->js
     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
@@ -956,12 +1374,45 @@ sub _js_redisplay_amounts_and_taxes {
     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 }
 
     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 }
 
+sub js_redisplay_cvpartnumbers {
+  my ($self) = @_;
+
+  $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+  my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
+
+  $self->js
+    ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
+}
+
+sub js_reset_order_and_item_ids_after_save {
+  my ($self) = @_;
+
+  $self->js
+    ->val('#id', $self->order->id)
+    ->val('#converted_from_oe_id', '')
+    ->val('#order_' . $self->nr_key(), $self->order->number);
+
+  my $idx = 0;
+  foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
+    next if !$self->order->items_sorted->[$idx]->id;
+    next if $form_item_id !~ m{^new};
+    $self->js
+      ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
+      ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
+      ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
+  } continue {
+    $idx++;
+  }
+  $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
+}
+
 #
 # helpers
 #
 
 sub init_valid_types {
 #
 # helpers
 #
 
 sub init_valid_types {
-  [ _sales_order_type(), _purchase_order_type(), _sales_quotation_type(), _request_quotation_type() ];
+  [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 }
 
 sub init_type {
 }
 
 sub init_type {
@@ -977,44 +1428,61 @@ sub init_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'
+  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;
 }
 
          : die "Not a valid type for order";
 
   return $cv;
 }
 
+sub init_search_cvpartnumber {
+  my ($self) = @_;
+
+  my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
+  my $search_cvpartnumber;
+  $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
+  $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
+
+  return $search_cvpartnumber;
+}
+
+sub init_show_update_button {
+  my ($self) = @_;
+
+  !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
+}
+
 sub init_p {
   SL::Presenter->get;
 }
 
 sub init_order {
 sub init_p {
   SL::Presenter->get;
 }
 
 sub init_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')}
-  );
+  $_[0]->make_order;
 }
 
 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 {
+sub init_part_picker_classification_ids {
+  my ($self)    = @_;
+  my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
+
+  return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
+}
+
+sub check_auth {
+  my ($self) = @_;
+
+  my $right_for = { map { $_ => $_.'_edit' . ' | ' . $_.'_view' } @{$self->valid_types} };
+
+  my $right   = $right_for->{ $self->type };
+  $right    ||= 'DOES_NOT_EXIST';
+
+  $::auth->assert($right);
+}
+
+sub check_auth_for_edit {
   my ($self) = @_;
 
   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
   my ($self) = @_;
 
   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
@@ -1040,21 +1508,55 @@ 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.
 sub build_shipto_select {
   my ($self) = @_;
 
 # build the selection box for shiptos
 #
 # Needed, if customer/vendor changed.
 sub build_shipto_select {
   my ($self) = @_;
 
-  select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
-    value_key  => 'shipto_id',
-    title_key  => 'displayable_id',
-    default    => $self->order->shipto_id,
-    with_empty => 1,
-    style      => 'width: 300px',
+  select_tag('order.shipto_id',
+             [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
+             value_key  => 'shipto_id',
+             title_key  => 'displayable_id',
+             default    => $self->order->shipto_id,
+             with_empty => 0,
+             style      => 'width: 300px',
   );
 }
 
   );
 }
 
+# build the inputs for the cusom shipto dialog
+#
+# Needed, if customer/vendor changed.
+sub build_shipto_inputs {
+  my ($self) = @_;
+
+  my $content = $self->p->render('common/_ship_to_dialog',
+                                 vc_obj      => $self->order->customervendor,
+                                 cs_obj      => $self->order->custom_shipto,
+                                 cvars       => $self->order->custom_shipto->cvars_by_config,
+                                 id_selector => '#order_shipto_id');
+
+  div_tag($content, id => 'shipto_inputs');
+}
+
 # render the info line for business
 #
 # Needed, if customer/vendor changed.
 # render the info line for business
 #
 # Needed, if customer/vendor changed.
@@ -1098,12 +1600,18 @@ sub render_price_dialog {
   $self->js->render;
 }
 
   $self->js->render;
 }
 
-sub _load_order {
+sub load_order {
   my ($self) = @_;
 
   return if !$::form->{id};
 
   my ($self) = @_;
 
   return if !$::form->{id};
 
-  $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
+  $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;
+
+  return $self->order;
 }
 
 # load or create a new order object
 }
 
 # load or create a new order object
@@ -1111,31 +1619,36 @@ sub _load_order {
 # And assign changes from the form to this object.
 # If the order is loaded from db, check if items are deleted in the form,
 # remove them form the object and collect them for removing from db on saving.
 # And assign changes from the form to this object.
 # If the order is loaded from db, check if items are deleted in the form,
 # remove them form the object and collect them for removing from db on saving.
-# Then create/update items from form (via _make_item) and add them.
-sub _make_order {
+# Then create/update items from form (via make_item) and add them.
+sub make_order {
   my ($self) = @_;
 
   # add_items adds items to an order with no items for saving, but they cannot
   # be retrieved via items until the order is saved. Adding empty items to new
   # order here solves this problem.
   my $order;
   my ($self) = @_;
 
   # add_items adds items to an order with no items for saving, but they cannot
   # be retrieved via items until the order is saved. Adding empty items to new
   # order here solves this problem.
   my $order;
-  $order   = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
-  $order ||= SL::DB::Order->new(orderitems => [],
-                                quotation  => (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type())));
+  $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(),);
 
   my $cv_id_method = $self->cv . '_id';
   if (!$::form->{id} && $::form->{$cv_id_method}) {
     $order->$cv_id_method($::form->{$cv_id_method});
 
   my $cv_id_method = $self->cv . '_id';
   if (!$::form->{id} && $::form->{$cv_id_method}) {
     $order->$cv_id_method($::form->{$cv_id_method});
-    _setup_order_from_cv($order);
+    setup_order_from_cv($order);
   }
 
   }
 
-  my $form_orderitems               = delete $::form->{order}->{orderitems};
-  my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
+  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}});
 
-  my $periodic_invoices_config = _make_periodic_invoices_config_from_yaml($form_periodic_invoices_config);
-  $order->periodic_invoices_config($periodic_invoices_config) if $periodic_invoices_config;
+  $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);
+  }
 
   # remove deleted items
   $self->item_ids_to_delete([]);
 
   # remove deleted items
   $self->item_ids_to_delete([]);
@@ -1150,7 +1663,7 @@ sub _make_order {
   my @items;
   my $pos = 1;
   foreach my $form_attr (@{$form_orderitems}) {
   my @items;
   my $pos = 1;
   foreach my $form_attr (@{$form_orderitems}) {
-    my $item = _make_item($order, $form_attr);
+    my $item = make_item($order, $form_attr);
     $item->position($pos);
     push @items, $item;
     $pos++;
     $item->position($pos);
     push @items, $item;
     $pos++;
@@ -1164,7 +1677,7 @@ sub _make_order {
 #
 # Make item objects from form values. For items already existing read from db.
 # Create a new item else. And assign attributes.
 #
 # Make item objects from form values. For items already existing read from db.
 # Create a new item else. And assign attributes.
-sub _make_item {
+sub make_item {
   my ($record, $attr) = @_;
 
   my $item;
   my ($record, $attr) = @_;
 
   my $item;
@@ -1178,9 +1691,13 @@ sub _make_item {
   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
   $item->assign_attributes(%$attr);
   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
   $item->assign_attributes(%$attr);
-  $item->longdescription($item->part->notes)                     if $is_new && !defined $attr->{longdescription};
-  $item->project_id($record->globalproject_id)                   if $is_new && !defined $attr->{project_id};
-  $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
+
+  if ($is_new) {
+    my $texts = get_part_texts($item->part, $record->language_id);
+    $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
+    $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
+    $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
+  }
 
   return $item;
 }
 
   return $item;
 }
@@ -1188,109 +1705,113 @@ sub _make_item {
 # create a new item
 #
 # This is used to add one item
 # create a new item
 #
 # This is used to add one item
-sub _new_item {
+sub new_item {
   my ($record, $attr) = @_;
 
   my $item = SL::DB::OrderItem->new;
   my ($record, $attr) = @_;
 
   my $item = SL::DB::OrderItem->new;
-  $item->assign_attributes(%$attr);
-
-  my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
-  my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
 
-  $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 ($item->sellprice) {
-    $price_src = $price_source->price_from_source("");
-    $price_src->price($item->sellprice);
-  } else {
-    $price_src = $price_source->best_price
-           ? $price_source->best_price
-           : $price_source->price_from_source("");
-    $price_src->price(0) if !$price_source->best_price;
+  # Remove attributes where the user left or set the inputs empty.
+  # So these attributes will be undefined and we can distinguish them
+  # from zero later on.
+  for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
+    delete $attr->{$_} if $attr->{$_} eq '';
   }
 
   }
 
-  my $discount_src;
-  if ($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;
-  }
+  $item->assign_attributes(%$attr);
+  $item->qty(1.0)                   if !$item->qty;
+  $item->unit($item->part->unit)    if !$item->unit;
+
+  my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
 
   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}            = $item->part->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}        = $item->part->notes           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} = [];
 
-  $item->assign_attributes(%new_attr);
+  my $texts = get_part_texts($item->part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
+
+  $item->assign_attributes(%new_attr, %{ $texts });
 
   return $item;
 }
 
 
   return $item;
 }
 
-sub _setup_order_from_cv {
+sub setup_order_from_cv {
   my ($order) = @_;
 
   my ($order) = @_;
 
-  $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id));
+  $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id language_id));
 
   $order->intnotes($order->customervendor->notes);
 
 
   $order->intnotes($order->customervendor->notes);
 
-  if ($order->is_sales) {
-    $order->salesman_id($order->customer->salesman_id);
-    $order->taxincluded(defined($order->customer->taxincluded_checked)
-                        ? $order->customer->taxincluded_checked
-                        : $::myconfig{taxincluded_checked});
-  }
+  return 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 $address = $order->customer->default_billing_address;;
+  $order->billing_address_id($address ? $address->id : undef);
+}
+
+# setup custom shipto from form
+#
+# The dialog returns form variables starting with 'shipto' and cvars starting
+# with 'shiptocvar_'.
+# Mark it to be deleted if a shipto from master data is selected
+# (i.e. order has a shipto).
+# Else, update or create a new custom shipto. If the fields are empty, it
+# will not be saved on save.
+sub setup_custom_shipto_from_form {
+  my ($self, $order, $form) = @_;
+
+  if ($order->shipto) {
+    $self->is_custom_shipto_to_delete(1);
+  } else {
+    my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
+
+    my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
+    my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
 
+    $custom_shipto->assign_attributes(%$shipto_attrs);
+    $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
+  }
 }
 
 # recalculate prices and taxes
 #
 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 }
 
 # recalculate prices and taxes
 #
 # Using the PriceTaxCalculator. Store linetotals in the item objects.
-sub _recalc {
+sub recalc {
   my ($self) = @_;
 
   my ($self) = @_;
 
-  # bb: todo: currency later
-  $self->order->currency_id($::instance_conf->get_currency_id());
-
   my %pat = $self->order->calculate_prices_and_taxes();
   my %pat = $self->order->calculate_prices_and_taxes();
+
   $self->{taxes} = [];
   $self->{taxes} = [];
-  foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
-    my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
+  foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
+    my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
 
-    my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
-    push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
-                                netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
-                                tax       => $tax });
+    push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
+                                netamount => $netamount,
+                                tax       => SL::DB::Tax->new(id => $tax_id)->load });
   }
   }
-
-  pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
+  pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 }
 
 # get data for saving, printing, ..., that is not changed in the form
 #
 # Only cvars for now.
 }
 
 # get data for saving, printing, ..., that is not changed in the form
 #
 # Only cvars for now.
-sub _get_unalterable_data {
+sub get_unalterable_data {
   my ($self) = @_;
 
   foreach my $item (@{ $self->order->items }) {
   my ($self) = @_;
 
   foreach my $item (@{ $self->order->items }) {
@@ -1306,7 +1827,7 @@ sub _get_unalterable_data {
 # delete the order
 #
 # And remove related files in the spool directory
 # delete the order
 #
 # And remove related files in the spool directory
-sub _delete {
+sub delete {
   my ($self) = @_;
 
   my $errors = [];
   my ($self) = @_;
 
   my $errors = [];
@@ -1319,6 +1840,8 @@ sub _delete {
       my $spool = $::lx_office_conf{paths}->{spool};
       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
       my $spool = $::lx_office_conf{paths}->{spool};
       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
+      $self->save_history('DELETED');
+
       1;
   }) || push(@{$errors}, $db->error);
 
       1;
   }) || push(@{$errors}, $db->error);
 
@@ -1328,20 +1851,54 @@ sub _delete {
 # save the order
 #
 # And delete items that are deleted in the form.
 # save the order
 #
 # And delete items that are deleted in the form.
-sub _save {
+sub save {
   my ($self) = @_;
 
   my $errors = [];
   my $db     = $self->order->db;
 
   my ($self) = @_;
 
   my $errors = [];
   my $db     = $self->order->db;
 
+  # check for new or updated phone note
+  if ($::form->{phone_note}->{subject} || $::form->{phone_note}->{body}) {
+    if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
+      return [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};
+      return [t8('Phone note not found for this order.')] if !$phone_note;
+    }
+
+    $phone_note = SL::DB::Note->new() if !$phone_note;
+    my $is_new  = !$phone_note->id;
+
+    $phone_note->assign_attributes(%{ $::form->{phone_note} },
+                                   trans_id     => $self->order->id,
+                                   trans_module => 'oe',
+                                   employee     => SL::DB::Manager::Employee->current);
+
+    $self->order->add_phone_notes($phone_note) if $is_new;
+  }
+
   $db->with_transaction(sub {
   $db->with_transaction(sub {
-    SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
+    # 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);
 
     # link records
     if ($::form->{converted_from_oe_id}) {
     $self->order->save(cascade => 1);
 
     # link records
     if ($::form->{converted_from_oe_id}) {
-      SL::DB::Order->new(id => $::form->{converted_from_oe_id})->load->link_to_record($self->order);
+      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 }) {
       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
         my $idx = 0;
         foreach (@{ $self->order->items_sorted }) {
@@ -1355,22 +1912,91 @@ sub _save {
           $idx++;
         }
       }
           $idx++;
         }
       }
+
+      $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
     }
     }
+
+    $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
+
+    $self->save_history('SAVED');
+
     1;
   }) || push(@{$errors}, $db->error);
 
   return $errors;
 }
 
     1;
   }) || push(@{$errors}, $db->error);
 
   return $errors;
 }
 
-sub _workflow_sales_or_purchase_order {
+sub workflow_sales_or_request_for_quotation {
+  my ($self) = @_;
+
+  # always save
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) for @{ $errors };
+    return $self->js->render();
+  }
+
+  my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
+
+  $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
+  delete $::form->{id};
+
+  # no linked records from order to quotations
+  delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
+
+  # set item ids to new fake id, to identify them as new items
+  foreach my $item (@{$self->order->items_sorted}) {
+    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  }
+
+  # change form type
+  $::form->{type} = $destination_type;
+  $self->type($self->init_type);
+  $self->cv  ($self->init_cv);
+  $self->check_auth;
+
+  $self->recalc();
+  $self->get_unalterable_data();
+  $self->pre_render();
+
+  # trigger rendering values for second row as hidden, because they
+  # are loaded only on demand. So we need to keep the values from the
+  # source.
+  $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+
+  $self->render(
+    'order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+}
+
+sub workflow_sales_or_purchase_order {
   my ($self) = @_;
 
   my ($self) = @_;
 
-  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()
+  # always save
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  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()
                        : '';
 
                        : '';
 
+  # 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');
+  }
+
   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
   $self->{converted_from_oe_id} = delete $::form->{id};
 
   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
   $self->{converted_from_oe_id} = delete $::form->{id};
 
@@ -1379,63 +2005,70 @@ sub _workflow_sales_or_purchase_order {
     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
   }
 
     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
   }
 
+  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 => []));
+    }
+  }
+
   # change form type
   $::form->{type} = $destination_type;
   $self->type($self->init_type);
   $self->cv  ($self->init_cv);
   # change form type
   $::form->{type} = $destination_type;
   $self->type($self->init_type);
   $self->cv  ($self->init_cv);
-  $self->_check_auth;
+  $self->check_auth;
 
 
-  $self->_recalc();
-  $self->_get_unalterable_data();
-  $self->_pre_render();
+  $self->recalc();
+  $self->get_unalterable_data();
+  $self->pre_render();
 
 
-  # trigger rendering values for second row/longdescription as hidden,
-  # because they are loaded only on demand. So we need to keep the values
-  # from the source.
-  $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
-  $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
+  # trigger rendering values for second row as hidden, because they
+  # are loaded only on demand. So we need to keep the values from the
+  # source.
+  $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
   $self->render(
     'order/form',
 
   $self->render(
     'order/form',
-    title => $self->_get_title_for('edit'),
+    title => $self->get_title_for('edit'),
     %{$self->{template_args}}
   );
 }
 
 
     %{$self->{template_args}}
   );
 }
 
 
-sub _pre_render {
+sub pre_render {
   my ($self) = @_;
 
   my ($self) = @_;
 
-  $self->{all_taxzones}             = SL::DB::Manager::TaxZone->get_all_sorted();
-  $self->{all_departments}          = SL::DB::Manager::Department->get_all_sorted();
-  $self->{all_employees}            = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
-                                                                                            deleted => 0 ] ],
-                                                                         sort_by => 'name');
-  $self->{all_salesmen}             = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
-                                                                                            deleted => 0 ] ],
-                                                                         sort_by => 'name');
-  $self->{all_projects}             = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
-                                                                                           active => 1 ] ],
-                                                                        sort_by => 'projectnumber');
-  $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->{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->{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( 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_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
+                                                                                              deleted => 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->{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->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
   my $print_form = Form->new('');
 
   my $print_form = Form->new('');
-  $print_form->{type}      = $self->type;
-  $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
-  $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
-  $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
+  $print_form->{type}        = $self->type;
+  $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
+  $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
     form => $print_form,
     options => {dialog_name_prefix => 'print_options.',
                 show_headers       => 1,
                 no_queue           => 1,
                 no_postscript      => 1,
     form => $print_form,
     options => {dialog_name_prefix => 'print_options.',
                 show_headers       => 1,
                 no_queue           => 1,
                 no_postscript      => 1,
-                no_opendocument    => 1,
-                no_html            => 1},
+                no_opendocument    => 0,
+                no_html            => 0},
   );
 
   foreach my $item (@{$self->order->orderitems}) {
   );
 
   foreach my $item (@{$self->order->orderitems}) {
@@ -1444,10 +2077,17 @@ 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 ($self->order->ordnumber && $::instance_conf->get_webdav) {
+  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.
+    # 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) {
     my $webdav = SL::Webdav->new(
       type     => $self->type,
     my $webdav = SL::Webdav->new(
       type     => $self->type,
-      number   => $self->order->ordnumber,
+      number   => $self->order->number,
     );
     my @all_objects = $webdav->get_all_objects;
     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
     );
     my @all_objects = $webdav->get_all_objects;
     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
@@ -1456,16 +2096,47 @@ sub _pre_render {
                                                 } } @all_objects;
   }
 
                                                 } } @all_objects;
   }
 
-  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config calculate_qty);
-  $self->_setup_edit_action_bar;
+  if (   (any { $self->type eq $_ } (sales_quotation_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->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+  $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 ckeditor/ckeditor ckeditor/adapters/jquery
+                                                         edit_periodic_invoices_config calculate_qty follow_up show_history);
+  $self->setup_edit_action_bar;
 }
 
 }
 
-sub _setup_edit_action_bar {
+sub setup_edit_action_bar {
   my ($self, %params) = @_;
 
   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 $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 @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($self->type eq 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 $right_for         = { map { $_ => $_.'_edit' } @{$self->valid_types} };
+  my $right             = $right_for->{ $self->type };
+  $right              ||= 'DOES_NOT_EXIST';
+  my $may_edit_create   = $::auth->assert($right, 'may fail');
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
@@ -1474,46 +2145,125 @@ sub _setup_edit_action_bar {
           t8('Save'),
           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
                                                     $::instance_conf->get_order_warn_no_deliverydate,
           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' ],
+          ],
+          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.') : undef,
+        ],
+        action => [
+          t8('Save and Close'),
+          call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
+                                                    $::instance_conf->get_order_warn_no_deliverydate,
+                                                    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.') : undef,
         ],
         action => [
           t8('Save as new'),
           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
         ],
         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,
+          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"
+
+      combobox => [
+        action => [
+          t8('Workflow'),
+        ],
+        action => [
+          t8('Save and Quotation'),
+          submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
+          checks   => [ @req_trans_cost_art, @req_cusordnumber ],
+          only_if  => (any { $self->type eq $_ } (sales_order_type())),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and RFQ'),
+          submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
+          only_if  => (any { $self->type eq $_ } (purchase_order_type())),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Sales Order'),
+          submit   => [ '#order_form', { action => "Order/sales_order" } ],
+          checks   => [ @req_trans_cost_art ],
+          only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Purchase Order'),
+          call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
+          checks    => [ @req_trans_cost_art, @req_cusordnumber ],
+          only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
         ],
         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,
                                                                                                                         ],
         ],
         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()))
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())),
+          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', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
+                                                                       $::instance_conf->get_order_warn_no_deliverydate,
+                                                                                                                        ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          only_if   => (any { $self->type eq $_ } (purchase_order_type())),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
         ],
         action => [
           t8('Save and Invoice'),
           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
         ],
         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' ],
+          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,
         ],
         ],
-      ], # end of combobox "Save"
-
-      combobox => [
         action => [
         action => [
-          t8('Workflow'),
+          ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
+          call      => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::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   => (any { $self->type eq $_ } (sales_order_type())),
         ],
         action => [
         ],
         action => [
-          t8('Sales Order'),
-          submit   => [ '#order_form', { action => "Order/sales_order" } ],
-          only_if  => (any { $self->type eq $_ } (_sales_quotation_type(), _purchase_order_type())),
-          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          t8('Save and Final Invoice'),
+          call      => [ 'kivi.Order.save', 'save_and_final_invoice', $::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   => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
         ],
         action => [
         ],
         action => [
-          t8('Purchase Order'),
-          submit   => [ '#order_form', { action => "Order/purchase_order" } ],
-          only_if  => (any { $self->type eq $_ } (_sales_order_type(), _request_quotation_type())),
-          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          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())),
+          disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
         ],
         ],
+
       ], # end of combobox "Workflow"
 
       combobox => [
       ], # end of combobox "Workflow"
 
       combobox => [
@@ -1521,12 +2271,30 @@ sub _setup_edit_action_bar {
           t8('Export'),
         ],
         action => [
           t8('Export'),
         ],
         action => [
-          t8('Print'),
-          call => [ 'kivi.Order.show_print_options' ],
+          t8('Save and preview PDF'),
+          call     => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
+                                                          $::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.') : undef,
         ],
         action => [
         ],
         action => [
-          t8('E-mail'),
-          call => [ 'kivi.Order.email' ],
+          t8('Save and print'),
+          call     => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
+                                                         $::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.') : undef,
+        ],
+        action => [
+          t8('Save and E-mail'),
+          id       => 'save_and_email_action',
+          call     => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
+                                                                         $::instance_conf->get_order_warn_no_deliverydate,
+                      ],
+          disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id  ? t8('This object has not been saved yet.')
+                    :                      undef,
         ],
         action => [
           t8('Download attachments of all parts'),
         ],
         action => [
           t8('Download attachments of all parts'),
@@ -1540,16 +2308,36 @@ 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,
+        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  => $deletion_allowed,
       ],
         only_if  => $deletion_allowed,
       ],
+
+      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' ],
+          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          only_if  => $::auth->assert('productivity', 1),
+        ],
+      ], # end of combobox "more"
     );
   }
 }
 
     );
   }
 }
 
-sub _create_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('');
@@ -1558,17 +2346,29 @@ sub _create_pdf {
   $print_form->{format}      = $params->{format}   || 'pdf';
   $print_form->{media}       = $params->{media}    || 'file';
   $print_form->{groupitems}  = $params->{groupitems};
   $print_form->{format}      = $params->{format}   || 'pdf';
   $print_form->{media}       = $params->{media}    || 'file';
   $print_form->{groupitems}  = $params->{groupitems};
+  $print_form->{printer_id}  = $params->{printer_id};
   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
   $order->language($params->{language});
   $order->flatten_to_form($print_form, format_amounts => 1);
 
   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
   $order->language($params->{language});
   $order->flatten_to_form($print_form, format_amounts => 1);
 
+  my $template_ext;
+  my $template_type;
+  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
   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
     name        => $print_form->{formname},
   # search for the template
   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
     name        => $print_form->{formname},
+    extension   => $template_ext,
     email       => $print_form->{media} eq 'email',
     language    => $params->{language},
     email       => $print_form->{media} eq 'email',
     language    => $params->{language},
-    printer_id  => $print_form->{printer_id},  # todo
+    printer_id  => $print_form->{printer_id},
   );
 
   if (!defined $template_file) {
   );
 
   if (!defined $template_file) {
@@ -1581,23 +2381,26 @@ sub _create_pdf {
     eval {
       $print_form->prepare_for_printing;
 
     eval {
       $print_form->prepare_for_printing;
 
-      $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
-        template  => $template_file,
-        variables => $print_form,
+      $$doc_ref = SL::Helper::CreatePDF->create_pdf(
+        format        => $print_form->{format},
+        template_type => $template_type,
+        template      => $template_file,
+        variables     => $print_form,
         variable_content_types => {
           longdescription => 'html',
           partnotes       => 'html',
           notes           => 'html',
         variable_content_types => {
           longdescription => 'html',
           partnotes       => 'html',
           notes           => 'html',
+          $::form->get_variable_content_types_for_cvars,
         },
       );
       1;
         },
       );
       1;
-    } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
+    } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
   });
 
   return @errors;
 }
 
   });
 
   return @errors;
 }
 
-sub _get_files_for_email_dialog {
+sub get_files_for_email_dialog {
   my ($self) = @_;
 
   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
   my ($self) = @_;
 
   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
@@ -1608,6 +2411,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 =
@@ -1629,20 +2433,20 @@ sub _get_files_for_email_dialog {
   return %files;
 }
 
   return %files;
 }
 
-sub _make_periodic_invoices_config_from_yaml {
+sub make_periodic_invoices_config_from_yaml {
   my ($yaml_config) = @_;
 
   return if !$yaml_config;
   my ($yaml_config) = @_;
 
   return if !$yaml_config;
-  my $attr = YAML::Load($yaml_config);
+  my $attr = SL::YAML::Load($yaml_config);
   return if 'HASH' ne ref $attr;
   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 }
 
 
   return if 'HASH' ne ref $attr;
   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 }
 
 
-sub _get_periodic_invoices_status {
+sub get_periodic_invoices_status {
   my ($self, $config) = @_;
 
   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}
@@ -1652,7 +2456,7 @@ sub _get_periodic_invoices_status {
   return $active ? t8('active') : t8('inactive');
 }
 
   return $active ? t8('active') : t8('inactive');
 }
 
-sub _get_title_for {
+sub get_title_for {
   my ($self, $action) = @_;
 
   return '' if none { lc($action)} qw(add edit);
   my ($self, $action) = @_;
 
   return '' if none { lc($action)} qw(add edit);
@@ -1668,29 +2472,214 @@ sub _get_title_for {
   # $::locale->text("Edit Request for Quotation");
 
   $action = ucfirst(lc($action));
   # $::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")
+  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 _sales_order_type {
+sub get_item_cvpartnumber {
+  my ($self, $item) = @_;
+
+  return if !$self->search_cvpartnumber;
+  return if !$self->order->customervendor;
+
+  if ($self->cv eq 'vendor') {
+    my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
+    $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
+  } elsif ($self->cv eq 'customer') {
+    my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
+    $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
+  }
+}
+
+sub get_part_texts {
+  my ($part_or_id, $language_or_id, %defaults) = @_;
+
+  my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
+  my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
+  my $texts       = {
+    description     => $defaults{description}     // $part->description,
+    longdescription => $defaults{longdescription} // $part->notes,
+  };
+
+  return $texts unless $language_id;
+
+  my $translation = SL::DB::Manager::Translation->get_first(
+    where => [
+      parts_id    => $part->id,
+      language_id => $language_id,
+    ]);
+
+  $texts->{description}     = $translation->translation     if $translation && $translation->translation;
+  $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
+
+  return $texts;
+}
+
+sub get_best_price_and_discount_source {
+  my ($record, $item, $ignore_given) = @_;
+
+  my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+
+  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);
+  } elsif (!$ignore_given && 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 $discount_src;
+  if (!$ignore_given && 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;
+  }
+
+  return ($price_src, $discount_src);
+}
+
+sub sales_order_type {
   'sales_order';
 }
 
   'sales_order';
 }
 
-sub _purchase_order_type {
+sub purchase_order_type {
   'purchase_order';
 }
 
   'purchase_order';
 }
 
-sub _sales_quotation_type {
+sub sales_quotation_type {
   'sales_quotation';
 }
 
   'sales_quotation';
 }
 
-sub _request_quotation_type {
+sub request_quotation_type {
   'request_quotation';
 }
 
   'request_quotation';
 }
 
+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_and_redirect_to {
+  my ($self, %params) = @_;
+
+  my $errors = $self->save();
+
+  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 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(%params, id => $self->order->id);
+}
+
+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 store_doc_to_webdav_and_filemanagement {
+  my ($self, $content, $filename, $variant) = @_;
+
+  my $order = $self->order;
+  my @errors;
+
+  # 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', $@);
+    };
+  }
+  if ($order->id && $::instance_conf->get_doc_storage) {
+    eval {
+      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);
+      1;
+    } or do {
+      push @errors, t8('Storing the document in the storage backend failed: #1', $@);
+    };
+  }
+
+  return @errors;
+}
+
+sub link_requirement_specs_linking_to_created_from_objects {
+  my ($self, @converted_from_oe_ids) = @_;
+
+  return unless @converted_from_oe_ids;
+
+  my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
+  foreach my $rs_order (@{ $rs_orders }) {
+    SL::DB::RequirementSpecOrder->new(
+      order_id            => $self->order->id,
+      requirement_spec_id => $rs_order->requirement_spec_id,
+      version_id          => $rs_order->version_id,
+    )->save;
+  }
+}
+
+sub set_project_in_linked_requirement_specs {
+  my ($self) = @_;
+
+  my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
+  foreach my $rs_order (@{ $rs_orders }) {
+    next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
+
+    $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
+  }
+}
+
 1;
 
 __END__
 1;
 
 __END__
@@ -1706,9 +2695,8 @@ SL::Controller::Order - controller for orders
 This is a new form to enter orders, completely rewritten with the use
 of controller and java script techniques.
 
 This is a new form to enter orders, completely rewritten with the use
 of controller and java script techniques.
 
-The aim is to provide the user a better expirience and a faster flow
-of work. Also the code should be more readable, more reliable and
-better to maintain.
+The aim is to provide the user a better experience and a faster workflow. Also
+the code should be more readable, more reliable and better to maintain.
 
 =head2 Key Features
 
 
 =head2 Key Features
 
@@ -1728,11 +2716,6 @@ Possibility to enter more than one item at once.
 
 =item *
 
 
 =item *
 
-Save order only on "save" (and "save and delivery order"-workflow). No
-hidden save on "print" or "email".
-
-=item *
-
 Item list in a scrollable area, so that the workflow buttons stay at
 the bottom.
 
 Item list in a scrollable area, so that the workflow buttons stay at
 the bottom.
 
@@ -1744,7 +2727,7 @@ possible (by partnumber, description, qty, sellprice and discount for now).
 =item *
 
 No C<update> is necessary. All entries and calculations are managed
 =item *
 
 No C<update> is necessary. All entries and calculations are managed
-with ajax-calls and the page does only reload on C<save>.
+with ajax-calls and the page only reloads on C<save>.
 
 =item *
 
 
 =item *
 
@@ -1792,14 +2775,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
@@ -1818,34 +2793,22 @@ java script functions
 
 =item * testing
 
 
 =item * testing
 
-=item * currency
-
-=item * credit limit
-
-=item * more workflows (save as new, quotation, purchase order)
-
 =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?
 
-=item * custom shipto address
-
 =item * check for direct delivery (workflow sales order -> purchase order)
 
 =item * check for direct delivery (workflow sales order -> purchase order)
 
-=item * language / part translations
-
 =item * access rights
 
 =item * display weights
 
 =item * access rights
 
 =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
 
@@ -1855,21 +2818,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 *
@@ -1879,15 +2831,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.
-
-=item *
-
-The language selected in print or email dialog is not saved when the order is saved.
-
 =back
 
 =head1 To discuss / Nice to have
 =back
 
 =head1 To discuss / Nice to have
@@ -1901,14 +2844,6 @@ How to expand/collapse second row. Now it can be done clicking the icon or
 
 =item *
 
 
 =item *
 
-Possibility to change longdescription in input row?
-
-=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.
 
@@ -1927,12 +2862,6 @@ editor or on text processing application).
 
 A warning when leaving the page without saveing unchanged inputs.
 
 
 A warning when leaving the page without saveing unchanged inputs.
 
-=item *
-
-Workflows for delivery order and invoice are in the menu "Save", because the
-order is saved before opening the new document form. Nevertheless perhaps these
-workflow buttons should be put under "Workflows".
-
 
 =back
 
 
 =back