Order-Controller: TODO »credit limit« entfernt
[kivitendo-erp.git] / SL / Controller / Order.pm
index fe71271..72a27ae 100644 (file)
@@ -4,48 +4,59 @@ 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::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::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::PartsGroup;
 use SL::DB::Printer;
 use SL::DB::Language;
 use SL::DB::RecordLink;
 use SL::DB::Printer;
 use SL::DB::Language;
 use SL::DB::RecordLink;
+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::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) ],
 );
 
 
 # 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('recalc',
+                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
+                                     print send_email) ]);
 
 
-__PACKAGE__->run_before('_get_unalterable_data',
-                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
+__PACKAGE__->run_before('get_unalterable_data',
+                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
+                                     print send_email) ]);
 
 #
 # actions
 
 #
 # actions
@@ -56,13 +67,15 @@ 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;
+  my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
+                   $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
   $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
 
   $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
 
-  $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 +84,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,17 +169,17 @@ 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);
 
@@ -162,7 +220,8 @@ sub action_save_as_new {
 
   # Set new reqdate unless changed
   if ($order->reqdate == $saved_order->reqdate) {
 
   # Set new reqdate unless changed
   if ($order->reqdate == $saved_order->reqdate) {
-    my $extra_days = $self->type eq _sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1;
+    my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
+                     $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
     $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
   } else {
     $new_attrs{reqdate} = $order->reqdate;
     $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
   } else {
     $new_attrs{reqdate} = $order->reqdate;
@@ -184,22 +243,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 and opendocument by now
+  if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
   }
 
     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
   }
 
@@ -208,39 +274,34 @@ 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 $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 $pdf;
-  my @errors = _create_pdf($self->order, \$pdf, { format     => $format,
-                                                  formname   => $formname,
-                                                  language   => $language,
-                                                  groupitems => $groupitems });
+  my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
+                                                   formname   => $formname,
+                                                   language   => $self->order->language,
+                                                   printer_id => $printer_id,
+                                                   groupitems => $groupitems });
   if (scalar @errors) {
     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
   }
 
   if ($media eq 'screen') {
     # screen/download
   if (scalar @errors) {
     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
   }
 
   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 PDF has been created'));
+    $self->send_file(
+      \$pdf,
+      type         => SL::MIME->mime_type_from_ext($pdf_filename),
+      name         => $pdf_filename,
+      js_no_render => 1,
+    );
 
   } elsif ($media eq 'printer') {
     # printer
 
   } elsif ($media eq 'printer') {
     # printer
@@ -254,10 +315,10 @@ sub action_print {
   }
 
   # copy file to webdav folder
   }
 
   # copy file to webdav folder
-  if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
+  if ($self->order->number && $::instance_conf->get_webdav_documents) {
     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 $webdav_file = SL::Webdav::File->new(
       webdav   => $webdav,
     );
     my $webdav_file = SL::Webdav::File->new(
       webdav   => $webdav,
@@ -270,7 +331,7 @@ sub action_print {
       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
     }
   }
       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
     }
   }
-  if ($self->order->ordnumber && $::instance_conf->get_doc_storage) {
+  if ($self->order->number && $::instance_conf->get_doc_storage) {
     eval {
       SL::File->save(object_id     => $self->order->id,
                      object_type   => $self->type,
     eval {
       SL::File->save(object_id     => $self->order->id,
                      object_type   => $self->type,
@@ -287,24 +348,16 @@ sub action_print {
   $self->js->render;
 }
 
   $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 {
+# open the email dialog
+sub action_save_and_show_email_dialog {
   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();
 
 
-# open the email dialog
-sub action_show_email_dialog {
-  my ($self) = @_;
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
 
   my $cv_method = $self->cv;
 
 
   my $cv_method = $self->cv;
 
@@ -321,18 +374,20 @@ 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';
 
   $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 $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
                                   email_form  => $email_form,
                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
                                   email_form  => $email_form,
                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
@@ -352,6 +407,16 @@ 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 %field_names = (to => 'email');
 
   my $email_form  = delete $::form->{email_form};
   my %field_names = (to => 'email');
 
@@ -364,16 +429,14 @@ sub action_send_email {
   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
   $::form->{media}  = 'email';
 
   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
   $::form->{media}  = 'email';
 
-  if (($::form->{attachment_policy} // '') eq 'normal') {
-    my $language;
-    $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
-
+  if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
     my $pdf;
     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}});
+    my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
+                                                    format     => $::form->{print_options}->{format},
+                                                    formname   => $::form->{print_options}->{formname},
+                                                    language   => $self->order->language,
+                                                    printer_id => $::form->{print_options}->{printer_id},
+                                                    groupitems => $::form->{print_options}->{groupitems}});
     if (scalar @errors) {
       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
     }
     if (scalar @errors) {
       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
     }
@@ -386,6 +449,7 @@ sub action_send_email {
     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
   }
 
     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
   }
 
+  $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
   $::form->send_email(\%::myconfig, 'pdf');
 
   # internal notes
   $::form->send_email(\%::myconfig, 'pdf');
 
   # internal notes
@@ -399,11 +463,17 @@ sub action_send_email {
   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
   $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
   $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
-  $self->js
-      ->val('#order_intnotes', $intnotes)
-      ->run('kivi.Order.close_email_dialog')
-      ->flash('info', t8('The email has been sent.'))
-      ->render($self);
+  $self->order->update_attributes(intnotes => $intnotes);
+
+  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
@@ -413,11 +483,11 @@ 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(
                                                    extend_automatically_by => 12,
                                                    active                  => 1,
                                                    email_subject           => GenericTranslations->get(
@@ -438,6 +508,7 @@ sub action_show_periodic_invoices_config_dialog {
 
   if ($::form->{customer_id}) {
     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
   if ($::form->{customer_id}) {
     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
+    $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail;
   }
 
   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
   }
 
   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
@@ -455,19 +526,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},
@@ -476,9 +547,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')
@@ -492,11 +563,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))
@@ -510,27 +581,10 @@ 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);
 }
 
 # save the order and redirect to the frontend subroutine for a new
 }
 
 # save the order and redirect to the frontend subroutine for a new
@@ -538,37 +592,40 @@ sub action_save_and_delivery_order {
 sub action_save_and_invoice {
   my ($self) = @_;
 
 sub action_save_and_invoice {
   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_invoice_from_order',
     controller => 'oe.pl',
     action     => 'oe_invoice_from_order',
-    id         => $self->order->id,
   );
   );
+}
 
 
-  $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
@@ -577,8 +634,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;
 
@@ -589,9 +646,9 @@ 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');
   }
 
   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
   }
 
   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
@@ -599,15 +656,20 @@ sub action_customer_vendor_changed {
   $self->js
     ->replaceWith('#order_cp_id',            $self->build_contact_select)
     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
   $self->js
     ->replaceWith('#order_cp_id',            $self->build_contact_select)
     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
+    ->replaceWith('#shipto_inputs  ',        $self->build_shipto_inputs)
     ->replaceWith('#business_info_row',      $self->build_business_info_row)
     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
     ->val(        '#order_taxincluded',      $self->order->taxincluded)
     ->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_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');
+    ->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_amounts_and_taxes;
+  $self->js_redisplay_cvpartnumbers;
   $self->js->render();
 }
 
   $self->js->render();
 }
 
@@ -655,12 +717,12 @@ 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();
 }
 
@@ -672,22 +734,28 @@ sub action_add_item {
 
   return unless $form_attr->{parts_id};
 
 
   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};
@@ -697,74 +765,52 @@ 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', '')
     ->run('kivi.Order.init_row_handlers')
     };
   };
 
   $self->js
     ->val('.add_item_input', '')
     ->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} ) {
@@ -773,7 +819,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;
@@ -783,28 +829,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();
 }
 
@@ -812,31 +865,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)
@@ -853,22 +929,6 @@ 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;
-
-  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;
-  }
-  $_[0]->render(\ $longdescription, { type => 'text' });
-}
-
 # load the second row for one or more items
 #
 # This action gets the html code for all items second rows by rendering a template for
 # load the second row for one or more items
 #
 # This action gets the html code for all items second rows by rendering a template for
@@ -876,13 +936,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
@@ -890,7 +950,57 @@ sub action_load_second_rows {
   $self->js->render();
 }
 
   $self->js->render();
 }
 
-sub _js_load_second_row {
+# update description, notes and sellprice from master data
+sub action_update_row_from_master_data {
+  my ($self) = @_;
+
+  foreach my $item_id (@{ $::form->{item_ids} }) {
+    my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
+    my $item  = $self->order->items_sorted->[$idx];
+    my $texts = get_part_texts($item->part, $self->order->language_id);
+
+    $item->description($texts->{description});
+    $item->longdescription($texts->{longdescription});
+
+    my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
+
+    my $price_src;
+    if ($item->part->is_assortment) {
+    # add assortment items with price 0, as the components carry the price
+      $price_src = $price_source->price_from_source("");
+      $price_src->price(0);
+    } else {
+      $price_src = $price_source->best_price
+                 ? $price_source->best_price
+                 : $price_source->price_from_source("");
+      $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
+      $price_src->price(0) if !$price_source->best_price;
+    }
+
+
+    $item->sellprice($price_src->price);
+    $item->active_price_source($price_src);
+
+    $self->js
+      ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
+      ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
+      ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
+      ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
+
+    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 js_load_second_row {
   my ($self, $item, $item_id, $do_parse) = @_;
 
   if ($do_parse) {
   my ($self, $item, $item_id, $do_parse) = @_;
 
   if ($do_parse) {
@@ -906,11 +1016,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;
@@ -935,7 +1045,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} }) {
@@ -950,6 +1060,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))
@@ -957,12 +1080,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 {
@@ -978,44 +1134,43 @@ 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 check_auth {
   my ($self) = @_;
 
   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
   my ($self) = @_;
 
   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
@@ -1047,15 +1202,31 @@ sub build_contact_select {
 sub build_shipto_select {
   my ($self) = @_;
 
 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.
@@ -1099,12 +1270,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
@@ -1112,31 +1289,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([]);
@@ -1151,7 +1333,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++;
@@ -1165,7 +1347,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;
@@ -1179,9 +1361,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;
 }
@@ -1189,10 +1375,18 @@ 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;
+
+  # 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 '';
+  }
+
   $item->assign_attributes(%$attr);
 
   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
   $item->assign_attributes(%$attr);
 
   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
@@ -1205,18 +1399,19 @@ sub _new_item {
     # add assortment items with price 0, as the components carry the price
     $price_src = $price_source->price_from_source("");
     $price_src->price(0);
     # 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) {
+  } elsif (defined $item->sellprice) {
     $price_src = $price_source->price_from_source("");
     $price_src->price($item->sellprice);
   } else {
     $price_src = $price_source->best_price
     $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_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;
     $price_src->price(0) if !$price_source->best_price;
   }
 
   my $discount_src;
-  if ($item->discount) {
+  if (defined $item->discount) {
     $discount_src = $price_source->discount_from_source("");
     $discount_src->discount($item->discount);
   } else {
     $discount_src = $price_source->discount_from_source("");
     $discount_src->discount($item->discount);
   } else {
@@ -1244,20 +1439,22 @@ sub _new_item {
   # saved. Adding empty custom_variables to new orderitem here solves this problem.
   $new_attr{custom_variables} = [];
 
   # 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($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));
 
   $order->intnotes($order->customervendor->notes);
 
   if ($order->is_sales) {
 
   $order->intnotes($order->customervendor->notes);
 
   if ($order->is_sales) {
-    $order->salesman_id($order->customer->salesman_id);
+    $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});
     $order->taxincluded(defined($order->customer->taxincluded_checked)
                         ? $order->customer->taxincluded_checked
                         : $::myconfig{taxincluded_checked});
@@ -1265,33 +1462,53 @@ sub _setup_order_from_cv {
 
 }
 
 
 }
 
+# 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 }) {
@@ -1307,7 +1524,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 = [];
@@ -1329,20 +1546,30 @@ 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;
 
   $db->with_transaction(sub {
   my ($self) = @_;
 
   my $errors = [];
   my $db     = $self->order->db;
 
   $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 }) {
@@ -1363,15 +1590,74 @@ sub _save {
   return $errors;
 }
 
   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));
+  $self->{converted_from_oe_id} = delete $::form->{id};
+
+  # set item ids to new fake id, to identify them as new items
+  foreach my $item (@{$self->order->items_sorted}) {
+    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  }
+
+  # change form type
+  $::form->{type} = $destination_type;
+  $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};
 
@@ -1380,62 +1666,69 @@ 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();
+  $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_opendocument    => 0,
                 no_html            => 1},
   );
 
                 no_html            => 1},
   );
 
@@ -1445,10 +1738,15 @@ 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
+    SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects;
+  }
+
+  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,
@@ -1457,16 +1755,19 @@ 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;
+  $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
+                                                         edit_periodic_invoices_config calculate_qty kivi.Validator follow_up);
+  $self->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);
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
@@ -1476,7 +1777,7 @@ sub _setup_edit_action_bar {
           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
                                                     $::instance_conf->get_order_warn_no_deliverydate,
                                                                                                       ],
           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'] ],
         ],
         action => [
           t8('Save as new'),
         ],
         action => [
           t8('Save as new'),
@@ -1484,37 +1785,51 @@ sub _setup_edit_action_bar {
           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' ],
           disabled  => !$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" } ],
+          only_if  => (any { $self->type eq $_ } (sales_order_type())),
+        ],
+        action => [
+          t8('Save and RFQ'),
+          submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
+          only_if  => (any { $self->type eq $_ } (purchase_order_type())),
+        ],
+        action => [
+          t8('Save and Sales Order'),
+          submit   => [ '#order_form', { action => "Order/sales_order" } ],
+          only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
+        ],
+        action => [
+          t8('Save and Purchase Order'),
+          call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
+          only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
+        ],
         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' ],
         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()))
+          only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
         ],
         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' ],
         ],
         ],
         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' ],
         ],
-      ], # end of combobox "Save"
-
-      combobox => [
-        action => [
-          t8('Workflow'),
-        ],
         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,
-        ],
-        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()))
         ],
         ],
+
       ], # end of combobox "Workflow"
 
       combobox => [
       ], # end of combobox "Workflow"
 
       combobox => [
@@ -1522,12 +1837,13 @@ sub _setup_edit_action_bar {
           t8('Export'),
         ],
         action => [
           t8('Export'),
         ],
         action => [
-          t8('Print'),
-          call => [ 'kivi.Order.show_print_options' ],
+          t8('Save and print'),
+          call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ],
         ],
         action => [
         ],
         action => [
-          t8('E-mail'),
-          call => [ 'kivi.Order.email' ],
+          t8('Save and E-mail'),
+          call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts ],
+          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
         ],
         action => [
           t8('Download attachments of all parts'),
         ],
         action => [
           t8('Download attachments of all parts'),
@@ -1544,11 +1860,23 @@ sub _setup_edit_action_bar {
         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
         only_if  => $deletion_allowed,
       ],
         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
         only_if  => $deletion_allowed,
       ],
+
+      combobox => [
+        action => [
+          t8('more')
+        ],
+        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 {
+sub generate_pdf {
   my ($order, $pdf_ref, $params) = @_;
 
   my @errors = ();
   my ($order, $pdf_ref, $params) = @_;
 
   my @errors = ();
@@ -1559,17 +1887,26 @@ 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';
+  }
+
   # 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) {
@@ -1583,8 +1920,10 @@ sub _create_pdf {
       $print_form->prepare_for_printing;
 
       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
       $print_form->prepare_for_printing;
 
       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
-        template  => $template_file,
-        variables => $print_form,
+        format        => $print_form->{format},
+        template_type => $template_type,
+        template      => $template_file,
+        variables     => $print_form,
         variable_content_types => {
           longdescription => 'html',
           partnotes       => 'html',
         variable_content_types => {
           longdescription => 'html',
           partnotes       => 'html',
@@ -1592,13 +1931,13 @@ sub _create_pdf {
         },
       );
       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);
@@ -1630,20 +1969,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}
@@ -1653,7 +1992,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);
@@ -1669,29 +2008,96 @@ 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 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);
+}
+
 1;
 
 __END__
 1;
 
 __END__
@@ -1707,9 +2113,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
 
@@ -1729,11 +2134,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.
 
@@ -1745,7 +2145,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 *
 
@@ -1793,14 +2193,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
@@ -1819,22 +2211,12 @@ 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
@@ -1880,15 +2262,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
@@ -1902,10 +2275,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 *
 Possibility to select PriceSources in input row?
 
 =item *
@@ -1928,12 +2297,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