epic-ts
[kivitendo-erp.git] / SL / Controller / Order.pm
index 983d168..0953811 100644 (file)
@@ -13,10 +13,12 @@ use SL::File;
 use SL::MIME;
 use SL::Util qw(trim);
 use SL::YAML;
+use SL::DB::History;
 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;
@@ -43,7 +45,7 @@ use Sort::Naturally;
 use Rose::Object::MakeMethods::Generic
 (
  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
- 'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors search_cvpartnumber show_update_button) ],
+ 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
 );
 
 
@@ -67,9 +69,14 @@ sub action_add {
   my ($self) = @_;
 
   $self->order->transdate(DateTime->now_local());
-  my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
-                   $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
-  $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
+  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();
@@ -218,11 +225,17 @@ sub action_save_as_new {
                         ? 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) {
-    my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
-                     $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
-    $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
+    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;
   }
@@ -314,38 +327,59 @@ sub action_print {
     $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', $@));
-    }
+  my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $pdf_filename);
+  if (scalar @warnings) {
+    $self->js->flash('warning', $_) for @warnings;
   }
-  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', $@));
-    }
+
+  $self->save_history('PRINTED');
+
+  $self->js
+    ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
+    ->render;
+}
+sub action_preview_pdf {
+  my ($self) = @_;
+
+  my $errors = $self->save();
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
   }
-  $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
@@ -381,6 +415,7 @@ sub action_save_and_show_email_dialog {
   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
   $form->{format}           = 'pdf';
+  $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
   $email_form->{subject}             = $form->generate_email_subject();
   $email_form->{attachment_filename} = $form->generate_attachment_filename();
@@ -388,11 +423,13 @@ sub action_save_and_show_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',
+                                  ALL_EMPLOYEES => $self->{all_employees},
   );
 
   $self->js
@@ -441,6 +478,11 @@ sub action_send_email {
       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;
@@ -465,6 +507,8 @@ sub action_send_email {
 
   $self->order->update_attributes(intnotes => $intnotes);
 
+  $self->save_history('MAILED');
+
   flash_later('info', t8('The email has been sent.'));
 
   my @redirect_params = (
@@ -508,7 +552,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} ]);
-    $::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 },
@@ -581,27 +628,10 @@ sub action_get_has_active_periodic_invoices {
 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',
-    id         => $self->order->id,
   );
-
-  $self->redirect_to(@redirect_params);
 }
 
 # save the order and redirect to the frontend subroutine for a new
@@ -609,27 +639,20 @@ sub action_save_and_delivery_order {
 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',
-    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
@@ -646,27 +669,10 @@ sub action_purchase_order {
 sub action_save_and_ap_transaction {
   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 => 'ap.pl',
     action     => 'add_from_purchase_order',
-    id         => $self->order->id,
   );
-
-  $self->redirect_to(@redirect_params);
 }
 
 # set form elements in respect to a changed customer or vendor
@@ -771,6 +777,8 @@ sub action_unit_changed {
 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};
@@ -842,38 +850,11 @@ sub action_add_item {
   $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) = @_;
 
-  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;
@@ -918,7 +899,7 @@ sub action_add_multi_items {
   }
 
   $self->js
-    ->run('kivi.Order.close_multi_items_dialog')
+    ->run('kivi.Part.close_picker_dialogs')
     ->run('kivi.Order.init_row_handlers')
     ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
@@ -997,6 +978,61 @@ sub action_price_popup {
   $self->render_price_dialog($item);
 }
 
+# save the order in a session variable and redirect to the part controller
+sub action_create_part {
+  my ($self) = @_;
+
+  my $previousform = $::auth->save_form_in_session(non_scalars => 1);
+
+  my $callback     = $self->url_for(
+    action       => 'return_from_create_part',
+    type         => $self->type, # type is needed for check_auth on return
+    previousform => $previousform,
+  );
+
+  flash_later('info', t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.'));
+
+  my @redirect_params = (
+    controller => 'Part',
+    action     => 'add',
+    part_type  => $::form->{add_item}->{create_part_type},
+    callback   => $callback,
+    show_abort => 1,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
+sub action_return_from_create_part {
+  my ($self) = @_;
+
+  $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
+
+  $::auth->restore_form_from_session(delete $::form->{previousform});
+
+  # set item ids to new fake id, to identify them as new items
+  foreach my $item (@{$self->order->items_sorted}) {
+    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  }
+
+  $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
 #
 # This action gets the html code for all items second rows by rendering a template for
@@ -1234,28 +1270,17 @@ sub init_order {
   $_[0]->make_order;
 }
 
-# model used to filter/display the parts in the multi-items dialog
-sub init_multi_items_models {
-  SL::Controller::Helper::GetModels->new(
-    controller     => $_[0],
-    model          => 'Part',
-    with_objects   => [ qw(unit_obj) ],
-    disable_plugin => 'paginated',
-    source         => $::form->{multi_items},
-    sorted         => {
-      _default    => {
-        by  => 'partnumber',
-        dir => 1,
-      },
-      partnumber  => t8('Partnumber'),
-      description => t8('Description')}
-  );
-}
-
 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) = @_;
 
@@ -1623,6 +1648,8 @@ sub delete {
       my $spool = $::lx_office_conf{paths}->{spool};
       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
+      $self->save_history('DELETED');
+
       1;
   }) || push(@{$errors}, $db->error);
 
@@ -1670,12 +1697,58 @@ sub save {
         }
       }
     }
+
+    $self->save_history('SAVED');
+
     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) = @_;
 
@@ -1782,8 +1855,10 @@ sub pre_render {
   }
 
   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) {
@@ -1801,7 +1876,7 @@ sub pre_render {
   $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);
+                                                         edit_periodic_invoices_config calculate_qty kivi.Validator follow_up show_history);
   $self->setup_edit_action_bar;
 }
 
@@ -1834,6 +1909,16 @@ sub setup_edit_action_bar {
         action => [
           t8('Workflow'),
         ],
+        action => [
+          t8('Save and Quotation'),
+          submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
+          only_if  => (any { $self->type eq $_ } (sales_order_type())),
+        ],
+        action => [
+          t8('Save and RFQ'),
+          submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
+          only_if  => (any { $self->type eq $_ } (purchase_order_type())),
+        ],
         action => [
           t8('Save and Sales Order'),
           submit   => [ '#order_form', { action => "Order/sales_order" } ],
@@ -1869,13 +1954,24 @@ sub setup_edit_action_bar {
         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,
+                                                                                                         ],
+        ],
         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,
+                                                                                                      ],
         ],
         action => [
           t8('Save and E-mail'),
-          call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::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 => [
@@ -1904,6 +2000,11 @@ sub setup_edit_action_bar {
           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
           only_if  => $::auth->assert('productivity', 1),
         ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $self->order->id, 'id' ],
+          disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
+        ],
       ], # end of combobox "more"
     );
   }
@@ -1981,6 +2082,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{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
   }
 
   my @parts =
@@ -2111,6 +2213,81 @@ 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;
+}
+
 1;
 
 __END__
@@ -2206,14 +2383,6 @@ One row for already entered items
 
 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
@@ -2232,24 +2401,16 @@ java script functions
 
 =item * testing
 
-=item * credit limit
-
-=item * more workflows (quotation, rfq)
-
 =item * price sources: little symbols showing better price / better discount
 
 =item * select units in input row?
 
 =item * check for direct delivery (workflow sales order -> purchase order)
 
-=item * language / part translations
-
 =item * access rights
 
 =item * display weights
 
-=item * history
-
 =item * mtime check
 
 =item * optional client/user behaviour
@@ -2289,11 +2450,6 @@ Sorting does not include C<position>, neither does reordering.
 This behavior was implemented intentionally. But we can discuss, which behavior
 should be implemented.
 
-=item *
-
-C<show_multi_items_dialog> does not use the currently inserted string for
-filtering.
-
 =back
 
 =head1 To discuss / Nice to have