Order-Controller: TODO »credit limit« entfernt
[kivitendo-erp.git] / SL / Controller / Order.pm
index cd36a07..72a27ae 100644 (file)
@@ -4,13 +4,15 @@ 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::Order;
 use SL::DB::Default;
 use SL::DB::Unit;
@@ -19,24 +21,29 @@ 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::ShippedQty;
 
 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) ],
 );
 
 
 );
 
 
@@ -44,10 +51,12 @@ use Rose::Object::MakeMethods::Generic
 __PACKAGE__->run_before('check_auth');
 
 __PACKAGE__->run_before('recalc',
 __PACKAGE__->run_before('check_auth');
 
 __PACKAGE__->run_before('recalc',
-                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
+                        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',
 
 __PACKAGE__->run_before('get_unalterable_data',
-                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
+                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
+                                     print send_email) ]);
 
 #
 # actions
 
 #
 # actions
@@ -58,9 +67,11 @@ 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->render(
     'order/form',
   $self->pre_render();
   $self->render(
     'order/form',
@@ -83,11 +94,10 @@ sub action_edit {
     foreach my $item (@{$self->order->items_sorted}) {
       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
     }
     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/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->recalc();
   }
 
   $self->recalc();
@@ -99,6 +109,36 @@ sub action_edit {
   );
 }
 
   );
 }
 
+# edit a collective order (consisting of one or more existing orders)
+sub action_edit_collective {
+  my ($self) = @_;
+
+  # collect order ids
+  my @multi_ids = map {
+    $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
+  } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
+
+  # fall back to add if no ids are given
+  if (scalar @multi_ids == 0) {
+    $self->action_add();
+    return;
+  }
+
+  # fall back to save as new if only one id is given
+  if (scalar @multi_ids == 1) {
+    $self->order(SL::DB::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) = @_;
@@ -180,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;
@@ -202,19 +243,26 @@ 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 and opendocument by now
   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
 
   # only pdf and opendocument by now
   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
@@ -226,22 +274,20 @@ 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
   my $form   = Form->new;
   $form->{$self->nr_key()}  = $self->order->number;
   $form->{type}             = $self->type;
   $form->{format}           = $format;
   $form->{formname}         = $formname;
   # create a form for generate_attachment_filename
   my $form   = Form->new;
   $form->{$self->nr_key()}  = $self->order->number;
   $form->{type}             = $self->type;
   $form->{format}           = $format;
   $form->{formname}         = $formname;
-  $form->{language}         = '_' . $language->template_code if $language;
+  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
   my $pdf_filename          = $form->generate_attachment_filename();
 
   my $pdf;
   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
                                                    formname   => $formname,
   my $pdf_filename          = $form->generate_attachment_filename();
 
   my $pdf;
   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
                                                    formname   => $formname,
-                                                   language   => $language,
+                                                   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;
                                                    groupitems => $groupitems });
   if (scalar @errors) {
     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
@@ -249,16 +295,13 @@ sub action_print {
 
   if ($media eq 'screen') {
     # screen/download
 
   if ($media eq 'screen') {
     # screen/download
-    my $sfile = SL::SessionFile::Random->new(mode => "w");
-    $sfile->fh->print($pdf);
-    $sfile->fh->close;
-
-    my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
-    $::auth->set_session_value("Order::print-${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
@@ -305,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::print-${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;
 
@@ -340,9 +375,11 @@ sub action_show_email_dialog {
 
   my $form = Form->new;
   $form->{$self->nr_key()}  = $self->order->number;
 
   my $form = Form->new;
   $form->{$self->nr_key()}  = $self->order->number;
+  $form->{cusordnumber}     = $self->order->cusordnumber;
   $form->{formname}         = $self->type;
   $form->{type}             = $self->type;
   $form->{formname}         = $self->type;
   $form->{type}             = $self->type;
-  $form->{language}         = 'de';
+  $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();
   $form->{format}           = 'pdf';
 
   $email_form->{subject}             = $form->generate_email_subject();
@@ -370,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');
 
@@ -383,15 +430,13 @@ sub action_send_email {
   $::form->{media}  = 'email';
 
   if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
   $::form->{media}  = 'email';
 
   if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
-    my $language;
-    $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
-
     my $pdf;
     my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
     my $pdf;
     my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
-                                                     format     => $::form->{print_options}->{format},
-                                                     formname   => $::form->{print_options}->{formname},
-                                                     language   => $language,
-                                                     groupitems => $::form->{print_options}->{groupitems}});
+                                                    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);
     }
@@ -404,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
@@ -417,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
@@ -435,7 +487,7 @@ sub action_show_periodic_invoices_config_dialog {
   $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(
@@ -456,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 },
@@ -494,7 +547,7 @@ 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);
 
@@ -528,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
@@ -556,27 +592,20 @@ 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
 }
 
 # workflow from sales quotation to sales order
@@ -589,6 +618,16 @@ 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
 #
 # This action is called on an change of the customer/vendor picker.
 # set form elements in respect to a changed customer or vendor
 #
 # This action is called on an change of the customer/vendor picker.
@@ -607,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;
@@ -617,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();
 }
 
@@ -696,16 +740,22 @@ sub action_add_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};
@@ -722,61 +772,40 @@ sub action_add_item {
 
       $self->order->add_items( $item );
       $self->recalc();
 
       $self->order->add_items( $item );
       $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->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
+
   $self->js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
 
   $self->js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
 
-# open the dialog for entering multiple items at once
-sub action_show_multi_items_dialog {
-  $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
-                all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
-}
-
-# update the filter results in the multi item dialog
-sub action_multi_items_update_result {
-  my $max_count = 100;
-
-  $::form->{multi_items}->{filter}->{obsolete} = 0;
-
-  my $count = $_[0]->multi_items_models->count;
-
-  if ($count == 0) {
-    my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
-    $_[0]->render($text, { layout => 0 });
-  } elsif ($count > $max_count) {
-    my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
-    $_[0]->render($text, { layout => 0 });
-  } else {
-    my $multi_items = $_[0]->multi_items_models->get;
-    $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
-                  multi_items => $multi_items);
-  }
-}
-
 # add item rows for multiple items at once
 sub action_add_multi_items {
   my ($self) = @_;
 
 # add item rows for multiple items at once
 sub action_add_multi_items {
   my ($self) = @_;
 
-  my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
+  my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
   return $self->js->render() unless scalar @form_attr;
 
   my @items;
   return $self->js->render() unless scalar @form_attr;
 
   my @items;
@@ -803,24 +832,31 @@ sub action_add_multi_items {
   $self->recalc();
 
   foreach my $item (@items) {
   $self->recalc();
 
   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->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
+
   $self->js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
   $self->js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
@@ -836,24 +872,47 @@ sub action_recalc_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)
@@ -870,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
@@ -907,6 +950,56 @@ sub action_load_second_rows {
   $self->js->render();
 }
 
   $self->js->render();
 }
 
+# update description, notes and sellprice from master data
+sub action_update_row_from_master_data {
+  my ($self) = @_;
+
+  foreach my $item_id (@{ $::form->{item_ids} }) {
+    my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
+    my $item  = $self->order->items_sorted->[$idx];
+    my $texts = get_part_texts($item->part, $self->order->language_id);
+
+    $item->description($texts->{description});
+    $item->longdescription($texts->{longdescription});
+
+    my $price_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) = @_;
 
 sub js_load_second_row {
   my ($self, $item, $item_id, $do_parse) = @_;
 
@@ -923,8 +1016,8 @@ 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 {
@@ -967,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))
@@ -974,6 +1080,39 @@ 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
 #
 #
 # helpers
 #
@@ -1002,6 +1141,23 @@ sub init_cv {
   return $cv;
 }
 
   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_p {
   SL::Presenter->get;
 }
@@ -1010,24 +1166,6 @@ sub init_order {
   $_[0]->make_order;
 }
 
   $_[0]->make_order;
 }
 
-# model used to filter/display the parts in the multi-items dialog
-sub init_multi_items_models {
-  SL::Controller::Helper::GetModels->new(
-    controller     => $_[0],
-    model          => 'Part',
-    with_objects   => [ qw(unit_obj) ],
-    disable_plugin => 'paginated',
-    source         => $::form->{multi_items},
-    sorted         => {
-      _default    => {
-        by  => 'partnumber',
-        dir => 1,
-      },
-      partnumber  => t8('Partnumber'),
-      description => t8('Description')}
-  );
-}
-
 sub init_all_price_factors {
   SL::DB::Manager::PriceFactor->get_all;
 }
 sub init_all_price_factors {
   SL::DB::Manager::PriceFactor->get_all;
 }
@@ -1064,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.
@@ -1122,6 +1276,12 @@ sub load_order {
   return if !$::form->{id};
 
   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
   return if !$::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
@@ -1137,9 +1297,10 @@ sub make_order {
   # be retrieved via items until the order is saved. Adding empty items to new
   # order here solves this problem.
   my $order;
   # be retrieved via items until the order is saved. Adding empty items to new
   # order here solves this problem.
   my $order;
-  $order   = SL::DB::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}) {
 
   my $cv_id_method = $self->cv . '_id';
   if (!$::form->{id} && $::form->{$cv_id_method}) {
@@ -1147,13 +1308,17 @@ sub make_order {
     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([]);
@@ -1196,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;
 }
@@ -1235,8 +1404,9 @@ sub new_item {
     $price_src->price($item->sellprice);
   } else {
     $price_src = $price_source->best_price
     $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;
   }
 
     $price_src->price(0) if !$price_source->best_price;
   }
 
@@ -1269,7 +1439,9 @@ 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;
 }
@@ -1277,12 +1449,12 @@ sub new_item {
 sub setup_order_from_cv {
   my ($order) = @_;
 
 sub setup_order_from_cv {
   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});
@@ -1290,27 +1462,47 @@ 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.
 sub recalc {
   my ($self) = @_;
 
 # recalculate prices and taxes
 #
 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 sub recalc {
   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
 }
 
 # get data for saving, printing, ..., that is not changed in the form
@@ -1361,13 +1553,23 @@ sub save {
   my $db     = $self->order->db;
 
   $db->with_transaction(sub {
   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 }) {
@@ -1388,15 +1590,74 @@ sub save {
   return $errors;
 }
 
   return $errors;
 }
 
+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) = @_;
 
 sub workflow_sales_or_purchase_order {
   my ($self) = @_;
 
+  # always save
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
   my $destination_type = $::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()
                        : '';
 
   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};
 
@@ -1405,6 +1666,15 @@ 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);
   # change form type
   $::form->{type} = $destination_type;
   $self->type($self->init_type);
@@ -1415,11 +1685,10 @@ sub workflow_sales_or_purchase_order {
   $self->get_unalterable_data();
   $self->pre_render();
 
   $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',
@@ -1432,26 +1701,28 @@ sub workflow_sales_or_purchase_order {
 sub pre_render {
   my ($self) = @_;
 
 sub pre_render {
   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_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,
     form => $print_form,
     options => {dialog_name_prefix => 'print_options.',
                 show_headers       => 1,
@@ -1484,7 +1755,10 @@ 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->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;
 }
 
   $self->setup_edit_action_bar;
 }
 
@@ -1503,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'),
@@ -1511,6 +1785,32 @@ 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,
         action => [
           t8('Save and Delivery Order'),
           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
@@ -1524,24 +1824,12 @@ sub setup_edit_action_bar {
           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
         ],
           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
         ],
-      ], # end of combobox "Save"
-
-      combobox => [
         action => [
         action => [
-          t8('Workflow'),
-        ],
-        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 => [
@@ -1549,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'),
@@ -1571,6 +1860,18 @@ 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"
     );
   }
 }
     );
   }
 }
@@ -1586,6 +1887,7 @@ sub generate_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});
   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
   $order->language($params->{language});
@@ -1604,7 +1906,7 @@ sub generate_pdf {
     extension   => $template_ext,
     email       => $print_form->{media} eq 'email',
     language    => $params->{language},
     extension   => $template_ext,
     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) {
@@ -1629,7 +1931,7 @@ sub generate_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;
@@ -1671,7 +1973,7 @@ 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);
 }
@@ -1713,6 +2015,45 @@ sub get_title_for {
        : '';
 }
 
        : '';
 }
 
+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';
 }
 sub sales_order_type {
   'sales_order';
 }
@@ -1737,6 +2078,26 @@ sub nr_key {
        : '';
 }
 
        : '';
 }
 
+sub save_and_redirect_to {
+  my ($self, %params) = @_;
+
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  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__
@@ -1752,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
 
@@ -1774,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.
 
@@ -1790,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 *
 
@@ -1838,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
@@ -1864,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
@@ -1925,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
@@ -1947,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 *
@@ -1973,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