Zusätzliche Rechnungsadressen: in Verkaufsbelegmasken auswählbar
[kivitendo-erp.git] / SL / Controller / Order.pm
index 10c7dd4..37ba87c 100644 (file)
@@ -4,39 +4,50 @@ 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::AdditionalBillingAddress;
+use SL::DB::History;
 use SL::DB::Order;
 use SL::DB::Default;
 use SL::DB::Unit;
 use SL::DB::Part;
 use SL::DB::Order;
 use SL::DB::Default;
 use SL::DB::Unit;
 use SL::DB::Part;
+use SL::DB::PartClassification;
 use SL::DB::PartsGroup;
 use SL::DB::Printer;
 use SL::DB::Language;
 use SL::DB::RecordLink;
 use SL::DB::PartsGroup;
 use SL::DB::Printer;
 use SL::DB::Language;
 use SL::DB::RecordLink;
+use SL::DB::RequirementSpec;
+use SL::DB::Shipto;
+use SL::DB::Translation;
 
 use SL::Helper::CreatePDF qw(:all);
 use SL::Helper::PrintOptions;
 use SL::Helper::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 part_picker_classification_ids) ],
 );
 
 
 );
 
 
@@ -44,10 +55,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,8 +71,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;
-  $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
+  my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
+                   $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
+
+  if (   ($self->type eq sales_order_type()     &&  $::instance_conf->get_deliverydate_on)
+      || ($self->type eq sales_quotation_type() &&  $::instance_conf->get_reqdate_on)
+      && (!$self->order->reqdate)) {
+    $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
+  }
+
 
   $self->pre_render();
   $self->render(
 
   $self->pre_render();
   $self->render(
@@ -83,11 +103,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();
@@ -208,10 +227,17 @@ sub action_save_as_new {
                         ? DateTime->today_local
                         : $order->transdate;
 
                         ? DateTime->today_local
                         : $order->transdate;
 
-  # Set new reqdate unless changed
+  # Set new reqdate unless changed if it is enabled in client config
   if ($order->reqdate == $saved_order->reqdate) {
   if ($order->reqdate == $saved_order->reqdate) {
-    my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1;
-    $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
+    my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
+                     $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
+
+    if (   ($self->type eq sales_order_type()     &&  !$::instance_conf->get_deliverydate_on)
+        || ($self->type eq sales_quotation_type() &&  !$::instance_conf->get_reqdate_on)) {
+      $new_attrs{reqdate} = '';
+    } else {
+      $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
+    }
   } else {
     $new_attrs{reqdate} = $order->reqdate;
   }
   } else {
     $new_attrs{reqdate} = $order->reqdate;
   }
@@ -232,11 +258,8 @@ 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) = @_;
 
@@ -247,14 +270,14 @@ sub action_print {
     return $self->js->render();
   }
 
     return $self->js->render();
   }
 
-  $self->js->val('#id', $self->order->id)
-           ->val('#order_' . $self->nr_key(), $self->order->number);
+  $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)) {
@@ -266,22 +289,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;
@@ -289,16 +310,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
@@ -311,59 +329,72 @@ sub action_print {
     $self->js->flash('info', t8('The PDF has been printed'));
   }
 
     $self->js->flash('info', t8('The PDF has been printed'));
   }
 
-  # copy file to webdav folder
-  if ($self->order->number && $::instance_conf->get_webdav_documents) {
-    my $webdav = SL::Webdav->new(
-      type     => $self->type,
-      number   => $self->order->number,
-    );
-    my $webdav_file = SL::Webdav::File->new(
-      webdav   => $webdav,
-      filename => $pdf_filename,
-    );
-    eval {
-      $webdav_file->store(data => \$pdf);
-      1;
-    } or do {
-      $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
-    }
-  }
-  if ($self->order->number && $::instance_conf->get_doc_storage) {
-    eval {
-      SL::File->save(object_id     => $self->order->id,
-                     object_type   => $self->type,
-                     mime_type     => 'application/pdf',
-                     source        => 'created',
-                     file_type     => 'document',
-                     file_name     => $pdf_filename,
-                     file_contents => $pdf);
-      1;
-    } or do {
-      $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
-    }
+  my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $pdf_filename);
+  if (scalar @warnings) {
+    $self->js->flash('warning', $_) for @warnings;
   }
   }
-  $self->js->render;
-}
 
 
-# offer pdf for download
-#
-# It needs to get the key for the session value to get the pdf file.
-sub action_download_pdf {
+  $self->save_history('PRINTED');
+
+  $self->js
+    ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
+    ->render;
+}
+sub action_preview_pdf {
   my ($self) = @_;
 
   my ($self) = @_;
 
-  my $key = $::form->{key};
-  my $tmp_filename = $::auth->get_session_value("Order::print-${key}");
-  return $self->send_file(
-    $tmp_filename,
-    type => 'application/pdf',
-    name => $::form->{pdf_filename},
+  my $errors = $self->save();
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  $self->js_reset_order_and_item_ids_after_save;
+
+  my $format      = 'pdf';
+  my $media       = 'screen';
+  my $formname    = $self->type;
+
+  # only pdf
+  # create a form for generate_attachment_filename
+  my $form   = Form->new;
+  $form->{$self->nr_key()}  = $self->order->number;
+  $form->{type}             = $self->type;
+  $form->{format}           = $format;
+  $form->{formname}         = $formname;
+  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
+  my $pdf_filename          = $form->generate_attachment_filename();
+
+  my $pdf;
+  my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
+                                                   formname   => $formname,
+                                                   language   => $self->order->language,
+                                                 });
+  if (scalar @errors) {
+    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
+  }
+  $self->save_history('PREVIEWED');
+  $self->js->flash('info', t8('The PDF has been previewed'));
+  # screen/download
+  $self->send_file(
+    \$pdf,
+    type         => SL::MIME->mime_type_from_ext($pdf_filename),
+    name         => $pdf_filename,
+    js_no_render => 0,
   );
 }
 
 # open the email dialog
   );
 }
 
 # open the email dialog
-sub action_show_email_dialog {
+sub action_save_and_show_email_dialog {
   my ($self) = @_;
 
   my ($self) = @_;
 
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
   my $cv_method = $self->cv;
 
   if (!$self->order->$cv_method) {
   my $cv_method = $self->cv;
 
   if (!$self->order->$cv_method) {
@@ -380,10 +411,13 @@ 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';
   $form->{format}           = 'pdf';
+  $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
   $email_form->{subject}             = $form->generate_email_subject();
   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
   $email_form->{subject}             = $form->generate_email_subject();
   $email_form->{attachment_filename} = $form->generate_attachment_filename();
@@ -391,11 +425,13 @@ sub action_show_email_dialog {
   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
   my %files = $self->get_files_for_email_dialog();
   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
   my %files = $self->get_files_for_email_dialog();
+  $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
                                   email_form  => $email_form,
                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
                                   FILES       => \%files,
                                   is_customer => $self->cv eq 'customer',
   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
                                   email_form  => $email_form,
                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
                                   FILES       => \%files,
                                   is_customer => $self->cv eq 'customer',
+                                  ALL_EMPLOYEES => $self->{all_employees},
   );
 
   $self->js
   );
 
   $self->js
@@ -418,8 +454,7 @@ sub action_send_email {
     return $self->js->render();
   }
 
     return $self->js->render();
   }
 
-  $self->js->val('#id', $self->order->id)
-           ->val('#order_' . $self->nr_key(), $self->order->number);
+  $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');
@@ -434,19 +469,22 @@ 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},
                                                     format     => $::form->{print_options}->{format},
                                                     formname   => $::form->{print_options}->{formname},
     my $pdf;
     my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
                                                     format     => $::form->{print_options}->{format},
                                                     formname   => $::form->{print_options}->{formname},
-                                                    language   => $language,
+                                                    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);
     }
 
                                                     groupitems => $::form->{print_options}->{groupitems}});
     if (scalar @errors) {
       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
     }
 
+    my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $::form->{attachment_filename});
+    if (scalar @warnings) {
+      flash_later('warning', $_) for @warnings;
+    }
+
     my $sfile = SL::SessionFile::Random->new(mode => "w");
     $sfile->fh->print($pdf);
     $sfile->fh->close;
     my $sfile = SL::SessionFile::Random->new(mode => "w");
     $sfile->fh->print($pdf);
     $sfile->fh->close;
@@ -455,6 +493,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
@@ -470,11 +509,17 @@ sub action_send_email {
 
   $self->order->update_attributes(intnotes => $intnotes);
 
 
   $self->order->update_attributes(intnotes => $intnotes);
 
-  $self->js
-      ->val('#order_intnotes', $intnotes)
-      ->run('kivi.Order.close_email_dialog')
-      ->flash('info', t8('The email has been sent.'))
-      ->render($self);
+  $self->save_history('MAILED');
+
+  flash_later('info', t8('The email has been sent.'));
+
+  my @redirect_params = (
+    action => 'edit',
+    type   => $self->type,
+    id     => $self->order->id,
+  );
+
+  $self->redirect_to(@redirect_params);
 }
 
 # open the periodic invoices config dialog
 }
 
 # open the periodic invoices config dialog
@@ -509,7 +554,10 @@ sub action_show_periodic_invoices_config_dialog {
 
   if ($::form->{customer_id}) {
     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
   if ($::form->{customer_id}) {
     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
-    $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail;
+    my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
+    $::form->{postal_invoice}                  = $customer_object->postal_invoice;
+    $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
+    $config->send_email(0) if $::form->{postal_invoice};
   }
 
   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
   }
 
   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
@@ -548,7 +596,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);
 
@@ -582,27 +630,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
@@ -610,27 +641,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
@@ -643,6 +667,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.
@@ -661,26 +695,36 @@ sub action_customer_vendor_changed {
   }
 
   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
   }
 
   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
-    $self->js->show('#shipto_row');
+    $self->js->show('#shipto_selection');
   } else {
   } else {
-    $self->js->hide('#shipto_row');
+    $self->js->hide('#shipto_selection');
+  }
+
+  if ($cv_method eq 'customer') {
+    my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
+    $self->js->$show_hide('#billing_address_row');
   }
 
   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
   $self->js
   }
 
   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
   $self->js
-    ->replaceWith('#order_cp_id',            $self->build_contact_select)
-    ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
-    ->replaceWith('#business_info_row',      $self->build_business_info_row)
-    ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
-    ->val(        '#order_taxincluded',      $self->order->taxincluded)
-    ->val(        '#order_payment_id',       $self->order->payment_id)
-    ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
-    ->val(        '#order_intnotes',         $self->order->intnotes)
-    ->val(        '#language_id',            $self->order->$cv_method->language_id)
-    ->focus(      '#order_' . $self->cv . '_id');
+    ->replaceWith('#order_cp_id',              $self->build_contact_select)
+    ->replaceWith('#order_shipto_id',          $self->build_shipto_select)
+    ->replaceWith('#shipto_inputs  ',          $self->build_shipto_inputs)
+    ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
+    ->replaceWith('#business_info_row',        $self->build_business_info_row)
+    ->val(        '#order_taxzone_id',         $self->order->taxzone_id)
+    ->val(        '#order_taxincluded',        $self->order->taxincluded)
+    ->val(        '#order_currency_id',        $self->order->currency_id)
+    ->val(        '#order_payment_id',         $self->order->payment_id)
+    ->val(        '#order_delivery_term_id',   $self->order->delivery_term_id)
+    ->val(        '#order_intnotes',           $self->order->intnotes)
+    ->val(        '#order_language_id',        $self->order->$cv_method->language_id)
+    ->focus(      '#order_' . $self->cv . '_id')
+    ->run('kivi.Order.update_exchangerate');
 
   $self->js_redisplay_amounts_and_taxes;
 
   $self->js_redisplay_amounts_and_taxes;
+  $self->js_redisplay_cvpartnumbers;
   $self->js->render();
 }
 
   $self->js->render();
 }
 
@@ -705,6 +749,9 @@ sub action_show_customer_vendor_details_dialog {
   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
+  foreach my $entry (@{ $cv->additional_billing_addresses }) {
+    push @{ $details{ADDITIONAL_BILLING_ADDRESSES} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
+  }
   foreach my $entry (@{ $cv->shipto }) {
     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
   }
   foreach my $entry (@{ $cv->shipto }) {
     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
   }
@@ -741,6 +788,8 @@ sub action_unit_changed {
 sub action_add_item {
   my ($self) = @_;
 
 sub action_add_item {
   my ($self) = @_;
 
+  delete $::form->{add_item}->{create_part_type};
+
   my $form_attr = $::form->{add_item};
 
   return unless $form_attr->{parts_id};
   my $form_attr = $::form->{add_item};
 
   return unless $form_attr->{parts_id};
@@ -751,16 +800,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};
@@ -777,61 +832,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;
@@ -858,24 +892,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();
 }
@@ -891,24 +932,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)
@@ -925,20 +989,59 @@ sub action_price_popup {
   $self->render_price_dialog($item);
 }
 
   $self->render_price_dialog($item);
 }
 
-# get the longdescription for an item if the dialog to enter/change the
-# longdescription was opened and the longdescription is empty
-#
-# If this item is new, get the longdescription from Part.
-# Otherwise get it from OrderItem.
-sub action_get_item_longdescription {
-  my $longdescription;
+# save the order in a session variable and redirect to the part controller
+sub action_create_part {
+  my ($self) = @_;
+
+  my $previousform = $::auth->save_form_in_session(non_scalars => 1);
+
+  my $callback     = $self->url_for(
+    action       => 'return_from_create_part',
+    type         => $self->type, # type is needed for check_auth on return
+    previousform => $previousform,
+  );
+
+  flash_later('info', t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.'));
+
+  my @redirect_params = (
+    controller => 'Part',
+    action     => 'add',
+    part_type  => $::form->{add_item}->{create_part_type},
+    callback   => $callback,
+    show_abort => 1,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
 
 
-  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;
+sub action_return_from_create_part {
+  my ($self) = @_;
+
+  $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
+
+  $::auth->restore_form_from_session(delete $::form->{previousform});
+
+  # set item ids to new fake id, to identify them as new items
+  foreach my $item (@{$self->order->items_sorted}) {
+    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
   }
   }
-  $_[0]->render(\ $longdescription, { type => 'text' });
+
+  $self->recalc();
+  $self->get_unalterable_data();
+  $self->pre_render();
+
+  # trigger rendering values for second row/longdescription as hidden,
+  # because they are loaded only on demand. So we need to keep the values
+  # from the source.
+  $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
+  $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
+
+  $self->render(
+    'order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+
 }
 
 # load the second row for one or more items
 }
 
 # load the second row for one or more items
@@ -962,6 +1065,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) = @_;
 
@@ -978,8 +1131,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 {
@@ -1042,6 +1195,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
 #
@@ -1070,6 +1256,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;
 }
@@ -1078,28 +1281,17 @@ sub init_order {
   $_[0]->make_order;
 }
 
   $_[0]->make_order;
 }
 
-# model used to filter/display the parts in the multi-items dialog
-sub init_multi_items_models {
-  SL::Controller::Helper::GetModels->new(
-    controller     => $_[0],
-    model          => 'Part',
-    with_objects   => [ qw(unit_obj) ],
-    disable_plugin => 'paginated',
-    source         => $::form->{multi_items},
-    sorted         => {
-      _default    => {
-        by  => 'partnumber',
-        dir => 1,
-      },
-      partnumber  => t8('Partnumber'),
-      description => t8('Description')}
-  );
-}
-
 sub init_all_price_factors {
   SL::DB::Manager::PriceFactor->get_all;
 }
 
 sub init_all_price_factors {
   SL::DB::Manager::PriceFactor->get_all;
 }
 
+sub init_part_picker_classification_ids {
+  my ($self)    = @_;
+  my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
+
+  return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
+}
+
 sub check_auth {
   my ($self) = @_;
 
 sub check_auth {
   my ($self) = @_;
 
@@ -1126,21 +1318,53 @@ sub build_contact_select {
   );
 }
 
   );
 }
 
+# build the selection box for the additional billing address
+#
+# Needed, if customer/vendor changed.
+sub build_billing_address_select {
+  my ($self) = @_;
+
+  select_tag('order.billing_address_id',
+             [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
+             value_key  => 'id',
+             title_key  => 'displayable_id',
+             default    => $self->order->billing_address_id,
+             with_empty => 0,
+             style      => 'width: 300px',
+  );
+}
+
 # build the selection box for shiptos
 #
 # Needed, if customer/vendor changed.
 sub build_shipto_select {
   my ($self) = @_;
 
 # build the selection box for shiptos
 #
 # Needed, if customer/vendor changed.
 sub build_shipto_select {
   my ($self) = @_;
 
-  select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
-    value_key  => 'shipto_id',
-    title_key  => 'displayable_id',
-    default    => $self->order->shipto_id,
-    with_empty => 1,
-    style      => 'width: 300px',
+  select_tag('order.shipto_id',
+             [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
+             value_key  => 'shipto_id',
+             title_key  => 'displayable_id',
+             default    => $self->order->shipto_id,
+             with_empty => 0,
+             style      => 'width: 300px',
   );
 }
 
   );
 }
 
+# build the inputs for the cusom shipto dialog
+#
+# Needed, if customer/vendor changed.
+sub build_shipto_inputs {
+  my ($self) = @_;
+
+  my $content = $self->p->render('common/_ship_to_dialog',
+                                 vc_obj      => $self->order->customervendor,
+                                 cs_obj      => $self->order->custom_shipto,
+                                 cvars       => $self->order->custom_shipto->cvars_by_config,
+                                 id_selector => '#order_shipto_id');
+
+  div_tag($content, id => 'shipto_inputs');
+}
+
 # render the info line for business
 #
 # Needed, if customer/vendor changed.
 # render the info line for business
 #
 # Needed, if customer/vendor changed.
@@ -1190,6 +1414,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
@@ -1205,9 +1435,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}) {
@@ -1215,12 +1446,14 @@ 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}});
 
-  if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? YAML::Load($form_periodic_invoices_config) : undef) {
+  $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);
   }
     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);
   }
@@ -1266,9 +1499,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;
 }
@@ -1305,8 +1542,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;
   }
 
@@ -1339,7 +1577,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;
 }
@@ -1347,17 +1587,43 @@ 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);
 
 
   $order->intnotes($order->customervendor->notes);
 
-  if ($order->is_sales) {
-    $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
-    $order->taxincluded(defined($order->customer->taxincluded_checked)
-                        ? $order->customer->taxincluded_checked
-                        : $::myconfig{taxincluded_checked});
-  }
+  return if !$order->is_sales;
+
+  $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
+  $order->taxincluded(defined($order->customer->taxincluded_checked)
+                      ? $order->customer->taxincluded_checked
+                      : $::myconfig{taxincluded_checked});
 
 
+  my $address = $order->customer->default_billing_address;;
+  $order->billing_address_id($address ? $address->id : undef);
+}
+
+# setup custom shipto from form
+#
+# The dialog returns form variables starting with 'shipto' and cvars starting
+# with 'shiptocvar_'.
+# Mark it to be deleted if a shipto from master data is selected
+# (i.e. order has a shipto).
+# Else, update or create a new custom shipto. If the fields are empty, it
+# will not be saved on save.
+sub setup_custom_shipto_from_form {
+  my ($self, $order, $form) = @_;
+
+  if ($order->shipto) {
+    $self->is_custom_shipto_to_delete(1);
+  } else {
+    my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
+
+    my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
+    my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
+
+    $custom_shipto->assign_attributes(%$shipto_attrs);
+    $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
+  }
 }
 
 # recalculate prices and taxes
 }
 
 # recalculate prices and taxes
@@ -1366,21 +1632,17 @@ sub setup_order_from_cv {
 sub recalc {
   my ($self) = @_;
 
 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
@@ -1415,6 +1677,8 @@ sub delete {
       my $spool = $::lx_office_conf{paths}->{spool};
       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
       my $spool = $::lx_office_conf{paths}->{spool};
       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
+      $self->save_history('DELETED');
+
       1;
   }) || push(@{$errors}, $db->error);
 
       1;
   }) || push(@{$errors}, $db->error);
 
@@ -1431,12 +1695,19 @@ sub save {
   my $db     = $self->order->db;
 
   $db->with_transaction(sub {
   my $db     = $self->order->db;
 
   $db->with_transaction(sub {
+    # delete custom shipto if it is to be deleted or if it is empty
+    if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
+      $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
+      $self->order->custom_shipto(undef);
+    }
+
     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
     $self->order->save(cascade => 1);
 
     # link records
     if ($::form->{converted_from_oe_id}) {
       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
     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}) {
       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$/;
       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$/;
@@ -1455,22 +1726,88 @@ sub save {
           $idx++;
         }
       }
           $idx++;
         }
       }
+
+      $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
     }
     }
+
+    $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
+
+    $self->save_history('SAVED');
+
     1;
   }) || push(@{$errors}, $db->error);
 
   return $errors;
 }
 
     1;
   }) || push(@{$errors}, $db->error);
 
   return $errors;
 }
 
+sub workflow_sales_or_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};
 
@@ -1479,6 +1816,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);
@@ -1489,11 +1835,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',
@@ -1506,26 +1851,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,
@@ -1542,8 +1889,10 @@ sub pre_render {
   }
 
   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
   }
 
   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;
+    # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
+    # Do not use write_to_objects to prevent order->delivered to be set, because this should be
+    # the value from db, which can be set manually or is set when linked delivery orders are saved.
+    SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
   }
 
   if ($self->order->number && $::instance_conf->get_webdav) {
   }
 
   if ($self->order->number && $::instance_conf->get_webdav) {
@@ -1558,7 +1907,15 @@ 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);
+  if (   (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
+      && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
+    $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
+  }
+
+  $self->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 show_history);
   $self->setup_edit_action_bar;
 }
 
   $self->setup_edit_action_bar;
 }
 
@@ -1569,6 +1926,9 @@ sub setup_edit_action_bar {
                       || (($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);
 
                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
+  my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
+  my @req_cusordnumber   = qw(kivi.Order.check_cusordnumber_presence)           x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
+
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
       combobox => [
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
       combobox => [
@@ -1576,13 +1936,17 @@ sub setup_edit_action_bar {
           t8('Save'),
           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
                                                     $::instance_conf->get_order_warn_no_deliverydate,
           t8('Save'),
           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
                                                     $::instance_conf->get_order_warn_no_deliverydate,
-                                                                                                      ],
-          checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
+          ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
         ],
         action => [
           t8('Save as new'),
           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
         ],
         action => [
           t8('Save as new'),
           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
-          checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
         ],
       ], # end of combobox "Save"
           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
         ],
       ], # end of combobox "Save"
@@ -1592,43 +1956,78 @@ sub setup_edit_action_bar {
           t8('Workflow'),
         ],
         action => [
           t8('Workflow'),
         ],
         action => [
-          t8('Sales Order'),
+          t8('Save and Quotation'),
+          submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
+          checks   => [ @req_trans_cost_art, @req_cusordnumber ],
+          only_if  => (any { $self->type eq $_ } (sales_order_type())),
+        ],
+        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" } ],
           submit   => [ '#order_form', { action => "Order/sales_order" } ],
+          checks   => [ @req_trans_cost_art ],
           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
           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 => [
         ],
         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 Purchase Order'),
+          call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
+          checks    => [ @req_trans_cost_art, @req_cusordnumber ],
+          only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
         ],
         action => [
           t8('Save and Delivery Order'),
           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
                                                                        $::instance_conf->get_order_warn_no_deliverydate,
                                                                                                                         ],
         ],
         action => [
           t8('Save and Delivery Order'),
           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
                                                                        $::instance_conf->get_order_warn_no_deliverydate,
                                                                                                                         ],
-          checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
         ],
         action => [
           t8('Save and Invoice'),
           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
           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' ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+        ],
+        action => [
+          t8('Save and AP Transaction'),
+          call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
+          only_if   => (any { $self->type eq $_ } (purchase_order_type()))
         ],
         ],
+
       ], # end of combobox "Workflow"
 
       combobox => [
         action => [
           t8('Export'),
         ],
       ], # end of combobox "Workflow"
 
       combobox => [
         action => [
           t8('Export'),
         ],
+        action => [
+          t8('Save and preview PDF'),
+          call   => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
+                                                        $::instance_conf->get_order_warn_no_deliverydate,
+                    ],
+          checks => [ @req_trans_cost_art, @req_cusordnumber ],
+        ],
         action => [
           t8('Save and print'),
         action => [
           t8('Save and print'),
-          call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ],
+          call   => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
+                                                       $::instance_conf->get_order_warn_no_deliverydate,
+                    ],
+          checks => [ @req_trans_cost_art, @req_cusordnumber ],
         ],
         action => [
           t8('Save and E-mail'),
         ],
         action => [
           t8('Save and E-mail'),
-          call => [ 'kivi.Order.email', $::instance_conf->get_order_warn_duplicate_parts ],
+          id   => 'save_and_email_action',
+          call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
+                                                                     $::instance_conf->get_order_warn_no_deliverydate,
+                  ],
+          disabled => !$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'),
@@ -1645,6 +2044,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('History'),
+          call     => [ 'set_history_window', $self->order->id, 'id' ],
+          disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
+        ],
+        action => [
+          t8('Follow-Up'),
+          call     => [ 'kivi.Order.follow_up_window' ],
+          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          only_if  => $::auth->assert('productivity', 1),
+        ],
+      ], # end of combobox "more"
     );
   }
 }
     );
   }
 }
@@ -1660,6 +2076,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});
@@ -1678,7 +2095,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) {
@@ -1703,7 +2120,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;
@@ -1720,6 +2137,7 @@ sub get_files_for_email_dialog {
     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
+    $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
   }
 
   my @parts =
   }
 
   my @parts =
@@ -1745,7 +2163,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);
 }
@@ -1787,6 +2205,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';
 }
@@ -1811,6 +2268,107 @@ 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);
+}
+
+sub save_history {
+  my ($self, $addition) = @_;
+
+  my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
+  my $snumbers    = $number_type . '_' . $self->order->$number_type;
+
+  SL::DB::History->new(
+    trans_id    => $self->order->id,
+    employee_id => SL::DB::Manager::Employee->current->id,
+    what_done   => $self->order->type,
+    snumbers    => $snumbers,
+    addition    => $addition,
+  )->save;
+}
+
+sub store_pdf_to_webdav_and_filemanagement {
+  my($order, $content, $filename) = @_;
+
+  my @errors;
+
+  # copy file to webdav folder
+  if ($order->number && $::instance_conf->get_webdav_documents) {
+    my $webdav = SL::Webdav->new(
+      type     => $order->type,
+      number   => $order->number,
+    );
+    my $webdav_file = SL::Webdav::File->new(
+      webdav   => $webdav,
+      filename => $filename,
+    );
+    eval {
+      $webdav_file->store(data => \$content);
+      1;
+    } or do {
+      push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
+    };
+  }
+  if ($order->id && $::instance_conf->get_doc_storage) {
+    eval {
+      SL::File->save(object_id     => $order->id,
+                     object_type   => $order->type,
+                     mime_type     => 'application/pdf',
+                     source        => 'created',
+                     file_type     => 'document',
+                     file_name     => $filename,
+                     file_contents => $content);
+      1;
+    } or do {
+      push @errors, t8('Storing PDF in storage backend failed: #1', $@);
+    };
+  }
+
+  return @errors;
+}
+
+sub link_requirement_specs_linking_to_created_from_objects {
+  my ($self, @converted_from_oe_ids) = @_;
+
+  return unless @converted_from_oe_ids;
+
+  my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
+  foreach my $rs_order (@{ $rs_orders }) {
+    SL::DB::RequirementSpecOrder->new(
+      order_id            => $self->order->id,
+      requirement_spec_id => $rs_order->requirement_spec_id,
+      version_id          => $rs_order->version_id,
+    )->save;
+  }
+}
+
+sub set_project_in_linked_requirement_specs {
+  my ($self) = @_;
+
+  my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
+  foreach my $rs_order (@{ $rs_orders }) {
+    next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
+
+    $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
+  }
+}
+
 1;
 
 __END__
 1;
 
 __END__
@@ -1826,9 +2384,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
 
@@ -1859,7 +2416,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 *
 
@@ -1907,14 +2464,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
@@ -1933,34 +2482,22 @@ java script functions
 
 =item * testing
 
 
 =item * testing
 
-=item * currency
-
-=item * credit limit
-
-=item * more workflows (quotation, rfq)
-
 =item * price sources: little symbols showing better price / better discount
 
 =item * select units in input row?
 
 =item * price sources: little symbols showing better price / better discount
 
 =item * select units in input row?
 
-=item * custom shipto address
-
 =item * check for direct delivery (workflow sales order -> purchase order)
 
 =item * check for direct delivery (workflow sales order -> purchase order)
 
-=item * language / part translations
-
 =item * access rights
 
 =item * display weights
 
 =item * access rights
 
 =item * display weights
 
-=item * history
-
 =item * mtime check
 
 =item * optional client/user behaviour
 
 (transactions has to be set - department has to be set -
 =item * mtime check
 
 =item * optional client/user behaviour
 
 (transactions has to be set - department has to be set -
- force project if enabled in client config - transport cost reminder)
+ force project if enabled in client config)
 
 =back
 
 
 =back
 
@@ -1994,15 +2531,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
@@ -2016,10 +2544,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 *