]> wagnertech.de Git - mfinanz.git/blobdiff - SL/Controller/Order.pm
SuSa - vergessene Hidden für department_id
[mfinanz.git] / SL / Controller / Order.pm
index c129999b088ebcda4e12f5eb6409d096eb971939..92ef60c8cbdd76a2477684e8d2ff9bb333585ce5 100644 (file)
@@ -3,39 +3,33 @@ package SL::Controller::Order;
 use strict;
 use parent qw(SL::Controller::Base);
 
 use strict;
 use parent qw(SL::Controller::Base);
 
-use SL::Helper::Flash;
-use SL::Presenter;
-use SL::Locale::String;
+use SL::Helper::Flash qw(flash_later);
+use SL::Presenter::Tag qw(select_tag hidden_tag);
+use SL::Locale::String qw(t8);
 use SL::SessionFile::Random;
 use SL::PriceSource;
 use SL::SessionFile::Random;
 use SL::PriceSource;
-use SL::Form;
 use SL::Webdav;
 use SL::Webdav;
-use SL::Template;
-
+use SL::File;
+use SL::Util qw(trim);
 use SL::DB::Order;
 use SL::DB::Order;
-use SL::DB::Customer;
-use SL::DB::Vendor;
-use SL::DB::TaxZone;
-use SL::DB::Employee;
-use SL::DB::Project;
 use SL::DB::Default;
 use SL::DB::Unit;
 use SL::DB::Default;
 use SL::DB::Unit;
-use SL::DB::Price;
-use SL::DB::PriceFactor;
 use SL::DB::Part;
 use SL::DB::Printer;
 use SL::DB::Language;
 use SL::DB::Part;
 use SL::DB::Printer;
 use SL::DB::Language;
+use SL::DB::RecordLink;
 
 
-use SL::Helper::DateTime;
 use SL::Helper::CreatePDF qw(:all);
 use SL::Helper::PrintOptions;
 
 use SL::Controller::Helper::GetModels;
 
 use SL::Helper::CreatePDF qw(:all);
 use SL::Helper::PrintOptions;
 
 use SL::Controller::Helper::GetModels;
 
-use List::Util qw(max first);
-use List::MoreUtils qw(none pairwise first_index);
+use List::Util qw(first);
+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 English qw(-no_match_vars);
 use File::Spec;
+use Cwd;
 
 use Rose::Object::MakeMethods::Generic
 (
 
 use Rose::Object::MakeMethods::Generic
 (
@@ -48,10 +42,10 @@ 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_and_delivery_order print create_pdf send_email) ]);
+                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
 
 __PACKAGE__->run_before('_get_unalterable_data',
 
 __PACKAGE__->run_before('_get_unalterable_data',
-                        only => [ qw(save save_and_delivery_order print create_pdf send_email) ]);
+                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
 
 #
 # actions
 
 #
 # actions
@@ -62,14 +56,13 @@ sub action_add {
   my ($self) = @_;
 
   $self->order->transdate(DateTime->now_local());
   my ($self) = @_;
 
   $self->order->transdate(DateTime->now_local());
-  $self->order->reqdate(DateTime->today_local->next_workday) if !$self->order->reqdate;
+  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;
 
   $self->_pre_render();
   $self->render(
     'order/form',
 
   $self->_pre_render();
   $self->render(
     'order/form',
-    title => $self->type eq _sales_order_type()    ? $::locale->text('Add Sales Order')
-           : $self->type eq _purchase_order_type() ? $::locale->text('Add Purchase Order')
-           : '',
+    title => $self->_get_title_for('add'),
     %{$self->{template_args}}
   );
 }
     %{$self->{template_args}}
   );
 }
@@ -83,9 +76,7 @@ sub action_edit {
   $self->_pre_render();
   $self->render(
     'order/form',
   $self->_pre_render();
   $self->render(
     'order/form',
-    title => $self->type eq _sales_order_type()    ? $::locale->text('Edit Sales Order')
-           : $self->type eq _purchase_order_type() ? $::locale->text('Edit Purchase Order')
-           : '',
+    title => $self->_get_title_for('edit'),
     %{$self->{template_args}}
   );
 }
     %{$self->{template_args}}
   );
 }
@@ -101,9 +92,15 @@ sub action_delete {
     return $self->js->render();
   }
 
     return $self->js->render();
   }
 
-  flash_later('info', $::locale->text('The order has been deleted'));
+  my $text = $self->type eq _sales_order_type()       ? $::locale->text('The order has been deleted')
+           : $self->type eq _purchase_order_type()    ? $::locale->text('The order has been deleted')
+           : $self->type eq _sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
+           : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been deleted')
+           : '';
+  flash_later('info', $text);
+
   my @redirect_params = (
   my @redirect_params = (
-    action => 'edit',
+    action => 'add',
     type   => $self->type,
   );
 
     type   => $self->type,
   );
 
@@ -121,7 +118,13 @@ sub action_save {
     return $self->js->render();
   }
 
     return $self->js->render();
   }
 
-  flash_later('info', $::locale->text('The order has been saved'));
+  my $text = $self->type eq _sales_order_type()       ? $::locale->text('The order has been saved')
+           : $self->type eq _purchase_order_type()    ? $::locale->text('The order has been saved')
+           : $self->type eq _sales_quotation_type()   ? $::locale->text('The quotation has been saved')
+           : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
+           : '';
+  flash_later('info', $text);
+
   my @redirect_params = (
     action => 'edit',
     type   => $self->type,
   my @redirect_params = (
     action => 'edit',
     type   => $self->type,
@@ -131,6 +134,53 @@ sub action_save {
   $self->redirect_to(@redirect_params);
 }
 
   $self->redirect_to(@redirect_params);
 }
 
+# save the order as new document an open it for edit
+sub action_save_as_new {
+  my ($self) = @_;
+
+  my $order = $self->order;
+
+  if (!$order->id) {
+    $self->js->flash('error', t8('This object has not been saved yet.'));
+    return $self->js->render();
+  }
+
+  # load order from db to check if values changed
+  my $saved_order = SL::DB::Order->new(id => $order->id)->load;
+
+  my %new_attrs;
+  # Lets assign a new number if the user hasn't changed the previous one.
+  # If it has been changed manually then use it as-is.
+  $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
+                        ? ''
+                        : trim($order->number);
+
+  # Clear transdate unless changed
+  $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
+                        ? DateTime->today_local
+                        : $order->transdate;
+
+  # Set new reqdate unless changed
+  if ($order->reqdate == $saved_order->reqdate) {
+    my $extra_days = $self->type eq _sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1;
+    $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
+  } else {
+    $new_attrs{reqdate} = $order->reqdate;
+  }
+
+  # Update employee
+  $new_attrs{employee}  = SL::DB::Manager::Employee->current;
+
+  # Create new record from current one
+  $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
+
+  # no linked records on save as new
+  delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
+
+  # save
+  $self->action_save();
+}
+
 # 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.
@@ -161,6 +211,7 @@ sub action_print {
   my $language;
   $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
   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->{ordnumber} = $self->order->ordnumber;
   $form->{type}      = $self->type;
   my $form = Form->new;
   $form->{ordnumber} = $self->order->ordnumber;
   $form->{type}      = $self->type;
@@ -219,7 +270,20 @@ sub action_print {
       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
     }
   }
       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
     }
   }
-
+  if ($self->order->ordnumber && $::instance_conf->get_doc_storage) {
+    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->js->render;
 }
 
   $self->js->render;
 }
 
@@ -249,10 +313,11 @@ sub action_show_email_dialog {
                     ->render($self);
   }
 
                     ->render($self);
   }
 
-  $self->{email}->{to}   = $self->order->contact->cp_email if $self->order->contact;
-  $self->{email}->{to} ||= $self->order->$cv_method->email;
-  $self->{email}->{cc}   = $self->order->$cv_method->cc;
-  $self->{email}->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
+  my $email_form;
+  $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
+  $email_form->{to} ||= $self->order->$cv_method->email;
+  $email_form->{cc}   = $self->order->$cv_method->cc;
+  $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
   # Todo: get addresses from shipto, if any
 
   my $form = Form->new;
   # Todo: get addresses from shipto, if any
 
   my $form = Form->new;
@@ -262,11 +327,19 @@ sub action_show_email_dialog {
   $form->{language} = 'de';
   $form->{format}   = 'pdf';
 
   $form->{language} = 'de';
   $form->{format}   = 'pdf';
 
-  $self->{email}->{subject}             = $form->generate_email_subject();
-  $self->{email}->{attachment_filename} = $form->generate_attachment_filename();
-  $self->{email}->{message}             = $form->create_email_signature();
+  $email_form->{subject}             = $form->generate_email_subject();
+  $email_form->{attachment_filename} = $form->generate_attachment_filename();
+  $email_form->{message}             = $form->generate_email_body();
+  $email_form->{js_send_function}    = 'kivi.Order.send_email()';
+
+  my %files = $self->_get_files_for_email_dialog();
+  my $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('order/tabs/_email_dialog', { output => 0 });
   $self->js
       ->run('kivi.Order.show_email_dialog', $dialog_html)
       ->reinit_widgets
   $self->js
       ->run('kivi.Order.show_email_dialog', $dialog_html)
       ->reinit_widgets
@@ -279,41 +352,158 @@ sub action_show_email_dialog {
 sub action_send_email {
   my ($self) = @_;
 
 sub action_send_email {
   my ($self) = @_;
 
-  my $mail      = Mailer->new;
-  $mail->{from} = qq|"$::myconfig{name}" <$::myconfig{email}>|;
-  $mail->{$_}   = $::form->{email}->{$_} for qw(to cc bcc subject message);
+  my $email_form  = delete $::form->{email_form};
+  my %field_names = (to => 'email');
 
 
-  my $pdf;
-  my @errors = _create_pdf($self->order, \$pdf, {media => 'email'});
-  if (scalar @errors) {
-    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
-  }
+  $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 
-  $mail->{attachments} = [{ "content" => $pdf,
-                            "name"    => $::form->{email}->{attachment_filename} }];
+  # for Form::cleanup which may be called in Form::send_email
+  $::form->{cwd}    = getcwd();
+  $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 
-  if (my $err = $mail->send) {
-    return $self->js->flash('error', t8('Sending E-mail: ') . $err)
-                    ->render($self);
+  $::form->{media}  = 'email';
+
+  if (($::form->{attachment_policy} // '') eq 'normal') {
+    my $language;
+    $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
+
+    my $pdf;
+    my @errors = _create_pdf($self->order, \$pdf, {media      => $::form->{media},
+                                                   format     => $::form->{print_options}->{format},
+                                                   formname   => $::form->{print_options}->{formname},
+                                                   language   => $language,
+                                                   groupitems => $::form->{print_options}->{groupitems}});
+    if (scalar @errors) {
+      return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
+    }
+
+    my $sfile = SL::SessionFile::Random->new(mode => "w");
+    $sfile->fh->print($pdf);
+    $sfile->fh->close;
+
+    $::form->{tmpfile} = $sfile->file_name;
+    $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
   }
 
   }
 
+  $::form->send_email(\%::myconfig, 'pdf');
+
   # internal notes
   my $intnotes = $self->order->intnotes;
   $intnotes   .= "\n\n" if $self->order->intnotes;
   $intnotes   .= t8('[email]')                                                                                        . "\n";
   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
   # internal notes
   my $intnotes = $self->order->intnotes;
   $intnotes   .= "\n\n" if $self->order->intnotes;
   $intnotes   .= t8('[email]')                                                                                        . "\n";
   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
-  $intnotes   .= t8('To (email)') . ": " . $mail->{to}                                                                . "\n";
-  $intnotes   .= t8('Cc')         . ": " . $mail->{cc}                                                                . "\n"    if $mail->{cc};
-  $intnotes   .= t8('Bcc')        . ": " . $mail->{bcc}                                                               . "\n"    if $mail->{bcc};
-  $intnotes   .= t8('Subject')    . ": " . $mail->{subject}                                                           . "\n\n";
-  $intnotes   .= t8('Message')    . ": " . $mail->{message};
+  $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
+  $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
+  $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
+  $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
+  $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
   $self->js
       ->val('#order_intnotes', $intnotes)
       ->run('kivi.Order.close_email_dialog')
 
   $self->js
       ->val('#order_intnotes', $intnotes)
       ->run('kivi.Order.close_email_dialog')
+      ->flash('info', t8('The email has been sent.'))
       ->render($self);
 }
 
       ->render($self);
 }
 
+# open the periodic invoices config dialog
+#
+# If there are values in the form (i.e. dialog was opened before),
+# then use this values. Create new ones, else.
+sub action_show_periodic_invoices_config_dialog {
+  my ($self) = @_;
+
+  my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
+  $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
+  $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
+                                                   order_value_periodicity => 'p', # = same as periodicity
+                                                   start_date_as_date      => $::form->{transdate} || $::form->current_date,
+                                                   extend_automatically_by => 12,
+                                                   active                  => 1,
+                                                   email_subject           => GenericTranslations->get(
+                                                                                language_id      => $::form->{language_id},
+                                                                                translation_type =>"preset_text_periodic_invoices_email_subject"),
+                                                   email_body              => GenericTranslations->get(
+                                                                                language_id      => $::form->{language_id},
+                                                                                translation_type =>"preset_text_periodic_invoices_email_body"),
+  );
+  $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
+  $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
+
+  $::form->get_lists(printers => "ALL_PRINTERS",
+                     charts   => { key       => 'ALL_CHARTS',
+                                   transdate => 'current_date' });
+
+  $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
+
+  if ($::form->{customer_id}) {
+    $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
+  }
+
+  $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
+                popup_dialog             => 1,
+                popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
+                popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
+                config                   => $config,
+                %$::form);
+}
+
+# assign the values of the periodic invoices config dialog
+# as yaml in the hidden tag and set the status.
+sub action_assign_periodic_invoices_config {
+  my ($self) = @_;
+
+  $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
+
+  my $config = { active                  => $::form->{active}     ? 1 : 0,
+                 terminated              => $::form->{terminated} ? 1 : 0,
+                 direct_debit            => $::form->{direct_debit} ? 1 : 0,
+                 periodicity             => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
+                 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
+                 start_date_as_date      => $::form->{start_date_as_date},
+                 end_date_as_date        => $::form->{end_date_as_date},
+                 first_billing_date_as_date => $::form->{first_billing_date_as_date},
+                 print                   => $::form->{print} ? 1 : 0,
+                 printer_id              => $::form->{print} ? $::form->{printer_id} * 1 : undef,
+                 copies                  => $::form->{copies} * 1 ? $::form->{copies} : 1,
+                 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
+                 ar_chart_id             => $::form->{ar_chart_id} * 1,
+                 send_email                 => $::form->{send_email} ? 1 : 0,
+                 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
+                 email_recipient_address    => $::form->{email_recipient_address},
+                 email_sender               => $::form->{email_sender},
+                 email_subject              => $::form->{email_subject},
+                 email_body                 => $::form->{email_body},
+               };
+
+  my $periodic_invoices_config = YAML::Dump($config);
+
+  my $status = $self->_get_periodic_invoices_status($config);
+
+  $self->js
+    ->remove('#order_periodic_invoices_config')
+    ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
+    ->run('kivi.Order.close_periodic_invoices_config_dialog')
+    ->html('#periodic_invoices_status', $status)
+    ->flash('info', t8('The periodic invoices config has been assigned.'))
+    ->render($self);
+}
+
+sub action_get_has_active_periodic_invoices {
+  my ($self) = @_;
+
+  my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
+  $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
+
+  my $has_active_periodic_invoices =
+       $self->type eq _sales_order_type()
+    && $config
+    && $config->active
+    && (!$config->end_date || ($config->end_date > DateTime->today_local))
+    && $config->get_previous_billed_period_start_date;
+
+  $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
+}
+
 # save the order and redirect to the frontend subroutine for a new
 # delivery order
 sub action_save_and_delivery_order {
 # save the order and redirect to the frontend subroutine for a new
 # delivery order
 sub action_save_and_delivery_order {
@@ -325,7 +515,13 @@ sub action_save_and_delivery_order {
     $self->js->flash('error', $_) foreach @{ $errors };
     return $self->js->render();
   }
     $self->js->flash('error', $_) foreach @{ $errors };
     return $self->js->render();
   }
-  flash_later('info', $::locale->text('The order has been saved'));
+
+  my $text = $self->type eq _sales_order_type()       ? $::locale->text('The order has been saved')
+           : $self->type eq _purchase_order_type()    ? $::locale->text('The order has been saved')
+           : $self->type eq _sales_quotation_type()   ? $::locale->text('The quotation has been saved')
+           : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
+           : '';
+  flash_later('info', $text);
 
   my @redirect_params = (
     controller => 'oe.pl',
 
   my @redirect_params = (
     controller => 'oe.pl',
@@ -336,7 +532,45 @@ sub action_save_and_delivery_order {
   $self->redirect_to(@redirect_params);
 }
 
   $self->redirect_to(@redirect_params);
 }
 
-# set form elements in respect of a changed customer or vendor
+# save the order and redirect to the frontend subroutine for a new
+# invoice
+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 = (
+    controller => 'oe.pl',
+    action     => 'oe_invoice_from_order',
+    id         => $self->order->id,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
+# workflow from sales quotation to sales order
+sub action_sales_order {
+  $_[0]->_workflow_sales_or_purchase_order();
+}
+
+# workflow from rfq to purchase order
+sub action_purchase_order {
+  $_[0]->_workflow_sales_or_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.
 sub action_customer_vendor_changed {
 #
 # This action is called on an change of the customer/vendor picker.
 sub action_customer_vendor_changed {
@@ -362,6 +596,7 @@ sub action_customer_vendor_changed {
     $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
                               ? $self->order->$cv_method->taxincluded_checked
                               : $::myconfig{taxincluded_checked});
     $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
                               ? $self->order->$cv_method->taxincluded_checked
                               : $::myconfig{taxincluded_checked});
+    $self->js->val('#order_salesman_id', $self->order->$cv_method->salesman_id);
   }
 
   $self->order->payment_id($self->order->$cv_method->payment_id);
   }
 
   $self->order->payment_id($self->order->$cv_method->payment_id);
@@ -372,6 +607,7 @@ sub action_customer_vendor_changed {
   $self->js
     ->replaceWith('#order_cp_id',            $self->build_contact_select)
     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
   $self->js
     ->replaceWith('#order_cp_id',            $self->build_contact_select)
     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
+    ->replaceWith('#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_taxzone_id',       $self->order->taxzone_id)
     ->val(        '#order_taxincluded',      $self->order->taxincluded)
     ->val(        '#order_payment_id',       $self->order->payment_id)
@@ -397,7 +633,7 @@ sub action_unit_changed {
 
   $self->js
     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
   $self->js
     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
-  $self->_js_redisplay_linetotals;
+  $self->_js_redisplay_line_values;
   $self->_js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
   $self->_js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
@@ -411,6 +647,7 @@ sub action_add_item {
   return unless $form_attr->{parts_id};
 
   my $item = _new_item($self->order, $form_attr);
   return unless $form_attr->{parts_id};
 
   my $item = _new_item($self->order, $form_attr);
+
   $self->order->add_items($item);
 
   $self->_recalc();
   $self->order->add_items($item);
 
   $self->_recalc();
@@ -419,11 +656,41 @@ sub action_add_item {
   my $row_as_html = $self->p->render('order/tabs/_row',
                                      ITEM              => $item,
                                      ID                => $item_id,
   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
   );
 
   $self->js
                                      ALL_PRICE_FACTORS => $self->all_price_factors
   );
 
   $self->js
-    ->append('#row_table_id', $row_as_html)
+    ->append('#row_table_id', $row_as_html);
+
+  if ( $item->part->is_assortment ) {
+    $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
+    foreach my $assortment_item ( @{$item->part->assortment_items} ) {
+      my $attr = { parts_id => $assortment_item->parts_id,
+                   qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
+                   unit     => $assortment_item->unit,
+                   description => $assortment_item->part->description,
+                 };
+      my $item = _new_item($self->order, $attr);
+
+      # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
+      $item->discount(1) unless $assortment_item->charge;
+
+      $self->order->add_items( $item );
+      $self->_recalc();
+      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
+      );
+      $self->js
+        ->append('#row_table_id', $row_as_html);
+    };
+  };
+
+  $self->js
     ->val('.add_item_input', '')
     ->run('kivi.Order.init_row_handlers')
     ->run('kivi.Order.row_table_scroll_down')
     ->val('.add_item_input', '')
     ->run('kivi.Order.init_row_handlers')
     ->run('kivi.Order.row_table_scroll_down')
@@ -462,7 +729,7 @@ sub action_multi_items_update_result {
   }
 }
 
   }
 }
 
-# add item rows for multiple items add once
+# add item rows for multiple items at once
 sub action_add_multi_items {
   my ($self) = @_;
 
 sub action_add_multi_items {
   my ($self) = @_;
 
@@ -471,7 +738,22 @@ sub action_add_multi_items {
 
   my @items;
   foreach my $attr (@form_attr) {
 
   my @items;
   foreach my $attr (@form_attr) {
-    push @items, _new_item($self->order, $attr);
+    my $item = _new_item($self->order, $attr);
+    push @items, $item;
+    if ( $item->part->is_assortment ) {
+      foreach my $assortment_item ( @{$item->part->assortment_items} ) {
+        my $attr = { parts_id => $assortment_item->parts_id,
+                     qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
+                     unit     => $assortment_item->unit,
+                     description => $assortment_item->part->description,
+                   };
+        my $item = _new_item($self->order, $attr);
+
+        # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
+        $item->discount(1) unless $assortment_item->charge;
+        push @items, $item;
+      }
+    }
   }
   $self->order->add_items(@items);
 
   }
   $self->order->add_items(@items);
 
@@ -482,6 +764,7 @@ sub action_add_multi_items {
     my $row_as_html = $self->p->render('order/tabs/_row',
                                        ITEM              => $item,
                                        ID                => $item_id,
     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
     );
 
                                        ALL_PRICE_FACTORS => $self->all_price_factors
     );
 
@@ -505,12 +788,12 @@ sub action_recalc_amounts_and_taxes {
 
   $self->_recalc();
 
 
   $self->_recalc();
 
-  $self->_js_redisplay_linetotals;
+  $self->_js_redisplay_line_values;
   $self->_js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
 
   $self->_js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
 
-# redisplay item rows if the are sorted by an attribute
+# redisplay item rows if they are sorted by an attribute
 sub action_reorder_items {
   my ($self) = @_;
 
 sub action_reorder_items {
   my ($self) = @_;
 
@@ -548,7 +831,7 @@ sub action_price_popup {
 # longdescription was opened and the longdescription is empty
 #
 # If this item is new, get the longdescription from Part.
 # longdescription was opened and the longdescription is empty
 #
 # If this item is new, get the longdescription from Part.
-# Get it from OrderItem else.
+# Otherwise get it from OrderItem.
 sub action_get_item_longdescription {
   my $longdescription;
 
 sub action_get_item_longdescription {
   my $longdescription;
 
@@ -560,12 +843,70 @@ sub action_get_item_longdescription {
   $_[0]->render(\ $longdescription, { type => 'text' });
 }
 
   $_[0]->render(\ $longdescription, { type => 'text' });
 }
 
-sub _js_redisplay_linetotals {
+# 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
+# the second row and sets the html code via client js.
+sub action_load_second_rows {
   my ($self) = @_;
 
   my ($self) = @_;
 
-  my @data = map {$::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0)} @{ $self->order->items_sorted };
+  $self->_recalc() if $self->order->is_sales; # for margin calculation
+
+  foreach my $item_id (@{ $::form->{item_ids} }) {
+    my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
+    my $item = $self->order->items_sorted->[$idx];
+
+    $self->_js_load_second_row($item, $item_id, 0);
+  }
+
+  $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
+
+  $self->js->render();
+}
+
+sub _js_load_second_row {
+  my ($self, $item, $item_id, $do_parse) = @_;
+
+  if ($do_parse) {
+    # Parse values from form (they are formated while rendering (template)).
+    # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
+    # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
+    foreach my $var (@{ $item->cvars_by_config }) {
+      $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
+    }
+    $item->parse_custom_variable_values;
+  }
+
+  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);
+}
+
+sub _js_redisplay_line_values {
+  my ($self) = @_;
+
+  my $is_sales = $self->order->is_sales;
+
+  # sales orders with margins
+  my @data;
+  if ($is_sales) {
+    @data = map {
+      [
+       $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
+       $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
+       $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
+      ]} @{ $self->order->items_sorted };
+  } else {
+    @data = map {
+      [
+       $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
+      ]} @{ $self->order->items_sorted };
+  }
+
   $self->js
   $self->js
-    ->run('kivi.Order.redisplay_linetotals', \@data);
+    ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 }
 
 sub _js_redisplay_amounts_and_taxes {
 }
 
 sub _js_redisplay_amounts_and_taxes {
@@ -595,7 +936,7 @@ sub _js_redisplay_amounts_and_taxes {
 #
 
 sub init_valid_types {
 #
 
 sub init_valid_types {
-  [ _sales_order_type(), _purchase_order_type() ];
+  [ _sales_order_type(), _purchase_order_type(), _sales_quotation_type(), _request_quotation_type() ];
 }
 
 sub init_type {
 }
 
 sub init_type {
@@ -611,8 +952,8 @@ sub init_type {
 sub init_cv {
   my ($self) = @_;
 
 sub init_cv {
   my ($self) = @_;
 
-  my $cv = $self->type eq _sales_order_type()    ? 'customer'
-         : $self->type eq _purchase_order_type() ? 'vendor'
+  my $cv = (any { $self->type eq $_ } (_sales_order_type(),    _sales_quotation_type()))   ? 'customer'
+         : (any { $self->type eq $_ } (_purchase_order_type(), _request_quotation_type())) ? 'vendor'
          : die "Not a valid type for order";
 
   return $cv;
          : die "Not a valid type for order";
 
   return $cv;
@@ -665,12 +1006,12 @@ sub _check_auth {
 sub build_contact_select {
   my ($self) = @_;
 
 sub build_contact_select {
   my ($self) = @_;
 
-  $self->p->select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
-                       value_key  => 'cp_id',
-                       title_key  => 'full_name_dep',
-                       default    => $self->order->cp_id,
-                       with_empty => 1,
-                       style      => 'width: 300px',
+  select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
+    value_key  => 'cp_id',
+    title_key  => 'full_name_dep',
+    default    => $self->order->cp_id,
+    with_empty => 1,
+    style      => 'width: 300px',
   );
 }
 
   );
 }
 
@@ -680,15 +1021,23 @@ sub build_contact_select {
 sub build_shipto_select {
   my ($self) = @_;
 
 sub build_shipto_select {
   my ($self) = @_;
 
-  $self->p->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', [ $self->order->{$self->cv}->shipto ],
+    value_key  => 'shipto_id',
+    title_key  => 'displayable_id',
+    default    => $self->order->shipto_id,
+    with_empty => 1,
+    style      => 'width: 300px',
   );
 }
 
   );
 }
 
+# render the info line for business
+#
+# Needed, if customer/vendor changed.
+sub build_business_info_row
+{
+  $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
+}
+
 # build the rows for displaying taxes
 #
 # Called if amounts where recalculated and redisplayed.
 # build the rows for displaying taxes
 #
 # Called if amounts where recalculated and redisplayed.
@@ -734,7 +1083,7 @@ sub _load_order {
 
 # load or create a new order object
 #
 
 # load or create a new order object
 #
-# And assign changes from the for to this object.
+# And assign changes from the form to this object.
 # If the order is loaded from db, check if items are deleted in the form,
 # remove them form the object and collect them for removing from db on saving.
 # Then create/update items from form (via _make_item) and add them.
 # If the order is loaded from db, check if items are deleted in the form,
 # remove them form the object and collect them for removing from db on saving.
 # Then create/update items from form (via _make_item) and add them.
@@ -746,11 +1095,17 @@ sub _make_order {
   # order here solves this problem.
   my $order;
   $order   = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
   # 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 => []);
+  $order ||= SL::DB::Order->new(orderitems => [],
+                                quotation  => (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type())));
+
+  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};
   $order->assign_attributes(%{$::form->{order}});
 
   $order->assign_attributes(%{$::form->{order}});
 
+  my $periodic_invoices_config = _make_periodic_invoices_config_from_yaml($form_periodic_invoices_config);
+  $order->periodic_invoices_config($periodic_invoices_config) if $periodic_invoices_config;
+
   # remove deleted items
   $self->item_ids_to_delete([]);
   foreach my $idx (reverse 0..$#{$order->orderitems}) {
   # remove deleted items
   $self->item_ids_to_delete([]);
   foreach my $idx (reverse 0..$#{$order->orderitems}) {
@@ -792,14 +1147,16 @@ 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->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};
 
   return $item;
 }
 
 # create a new item
 #
 
   return $item;
 }
 
 # create a new item
 #
-# This is used to add one (or more) items
+# This is used to add one item
 sub _new_item {
   my ($record, $attr) = @_;
 
 sub _new_item {
   my ($record, $attr) = @_;
 
@@ -812,7 +1169,11 @@ sub _new_item {
   $item->unit($part->unit) if !$item->unit;
 
   my $price_src;
   $item->unit($part->unit) if !$item->unit;
 
   my $price_src;
-  if ($item->sellprice) {
+  if ( $part->is_assortment ) {
+    # add assortment items with price 0, as the components carry the price
+    $price_src = $price_source->price_from_source("");
+    $price_src->price(0);
+  } elsif ($item->sellprice) {
     $price_src = $price_source->price_from_source("");
     $price_src->price($item->sellprice);
   } else {
     $price_src = $price_source->price_from_source("");
     $price_src->price($item->sellprice);
   } else {
@@ -842,8 +1203,9 @@ sub _new_item {
   $new_attr{discount}               = $discount_src->discount;
   $new_attr{active_price_source}    = $price_src;
   $new_attr{active_discount_source} = $discount_src;
   $new_attr{discount}               = $discount_src->discount;
   $new_attr{active_price_source}    = $price_src;
   $new_attr{active_discount_source} = $discount_src;
-
-  $new_attr{longdescription}        = $part->notes if ! defined $attr->{longdescription};
+  $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
+  $new_attr{project_id}             = $record->globalproject_id;
+  $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
   # they cannot be retrieved via custom_variables until the order/orderitem is
 
   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
   # they cannot be retrieved via custom_variables until the order/orderitem is
@@ -857,7 +1219,7 @@ sub _new_item {
 
 # recalculate prices and taxes
 #
 
 # recalculate prices and taxes
 #
-# Using the PriceTaxCalclulator. Store linetotals in the item objects.
+# Using the PriceTaxCalculator. Store linetotals in the item objects.
 sub _recalc {
   my ($self) = @_;
 
 sub _recalc {
   my ($self) = @_;
 
@@ -901,9 +1263,9 @@ sub _delete {
   my ($self) = @_;
 
   my $errors = [];
   my ($self) = @_;
 
   my $errors = [];
-  my $db = $self->order->db;
+  my $db     = $self->order->db;
 
 
-  $db->do_transaction(
+  $db->with_transaction(
     sub {
       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
       $self->order->delete;
     sub {
       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
       $self->order->delete;
@@ -923,36 +1285,89 @@ sub _save {
   my ($self) = @_;
 
   my $errors = [];
   my ($self) = @_;
 
   my $errors = [];
-  my $db = $self->order->db;
-
-  $db->do_transaction(
-    sub {
-      SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
-      $self->order->save(cascade => 1);
+  my $db     = $self->order->db;
+
+  $db->with_transaction(sub {
+    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}) {
+      SL::DB::Order->new(id => $::form->{converted_from_oe_id})->load->link_to_record($self->order);
+
+      if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
+        my $idx = 0;
+        foreach (@{ $self->order->items_sorted }) {
+          my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
+          next if !$from_id;
+          SL::DB::RecordLink->new(from_table => 'orderitems',
+                                  from_id    => $from_id,
+                                  to_table   => 'orderitems',
+                                  to_id      => $_->id
+          )->save;
+          $idx++;
+        }
+      }
+    }
+    1;
   }) || push(@{$errors}, $db->error);
 
   return $errors;
 }
 
   }) || push(@{$errors}, $db->error);
 
   return $errors;
 }
 
+sub _workflow_sales_or_purchase_order {
+  my ($self) = @_;
+
+  my $destination_type = $::form->{type} eq _sales_quotation_type()   ? _sales_order_type()
+                       : $::form->{type} eq _request_quotation_type() ? _purchase_order_type()
+                       : '';
+
+  $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
+  $self->{converted_from_oe_id} = delete $::form->{id};
+
+  # change form type
+  $::form->{type} = $destination_type;
+  $self->init_type;
+  $self->_check_auth;
+
+  $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}}
+  );
+}
+
 
 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_projects}        = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
-                                                                                      active => 1 ] ],
-                                                                   sort_by => 'projectnumber');
-  $self->{all_payment_terms}   = SL::DB::Manager::PaymentTerm->get_all_sorted();
-  $self->{all_delivery_terms}  = SL::DB::Manager::DeliveryTerm->get_all_sorted();
-
-  $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
+  $self->{all_taxzones}             = SL::DB::Manager::TaxZone->get_all_sorted();
+  $self->{all_departments}          = SL::DB::Manager::Department->get_all_sorted();
+  $self->{all_employees}            = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
+                                                                                            deleted => 0 ] ],
+                                                                         sort_by => 'name');
+  $self->{all_salesmen}             = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
+                                                                                            deleted => 0 ] ],
+                                                                         sort_by => 'name');
+  $self->{all_projects}             = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
+                                                                                           active => 1 ] ],
+                                                                        sort_by => 'projectnumber');
+  $self->{all_payment_terms}        = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
+                                                                                                      obsolete => 0 ] ]);
+  $self->{all_delivery_terms}       = SL::DB::Manager::DeliveryTerm->get_all_sorted();
+  $self->{current_employee_id}      = SL::DB::Manager::Employee->current->id;
+  $self->{periodic_invoices_status} = $self->_get_periodic_invoices_status($self->order->periodic_invoices_config);
+  $self->{order_probabilities}      = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
   my $print_form = Form->new('');
   $print_form->{type}      = $self->type;
 
   my $print_form = Form->new('');
   $print_form->{type}      = $self->type;
@@ -979,15 +1394,98 @@ sub _pre_render {
       type     => $self->type,
       number   => $self->order->ordnumber,
     );
       type     => $self->type,
       number   => $self->order->ordnumber,
     );
-    my $webdav_path = $webdav->webdav_path;
     my @all_objects = $webdav->get_all_objects;
     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
                                                     type => t8('File'),
     my @all_objects = $webdav->get_all_objects;
     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
                                                     type => t8('File'),
-                                                    link => File::Spec->catdir($webdav_path, $_->filename),
+                                                    link => File::Spec->catfile($_->full_filedescriptor),
                                                 } } @all_objects;
   }
 
                                                 } } @all_objects;
   }
 
-  $::request->{layout}->use_javascript("${_}.js")  for qw(kivi.SalesPurchase kivi.Order ckeditor/ckeditor ckeditor/adapters/jquery);
+  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config);
+  $self->_setup_edit_action_bar;
+}
+
+sub _setup_edit_action_bar {
+  my ($self, %params) = @_;
+
+  my $deletion_allowed = (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type()))
+                      || (($self->type eq _sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
+                      || (($self->type eq _purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Save'),
+          call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
+        ],
+        action => [
+          t8('Save as new'),
+          call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
+          disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+        ],
+        action => [
+          t8('Save and Delivery Order'),
+          call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
+          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' ],
+        ],
+      ], # end of combobox "Save"
+
+      combobox => [
+        action => [
+          t8('Workflow'),
+        ],
+        action => [
+          t8('Sales Order'),
+          submit   => [ '#order_form', { action => "Order/sales_order" } ],
+          only_if  => (any { $self->type eq $_ } (_sales_quotation_type())),
+          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+        ],
+        action => [
+          t8('Purchase Order'),
+          submit   => [ '#order_form', { action => "Order/purchase_order" } ],
+          only_if  => (any { $self->type eq $_ } (_request_quotation_type())),
+          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+        ],
+      ], # end of combobox "Workflow"
+
+      combobox => [
+        action => [
+          t8('Export'),
+        ],
+        action => [
+          t8('Print'),
+          call => [ 'kivi.Order.show_print_options' ],
+        ],
+        action => [
+          t8('E-mail'),
+          call => [ 'kivi.Order.email' ],
+        ],
+        action => [
+          t8('Download attachments of all parts'),
+          call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
+          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          only_if  => $::instance_conf->get_doc_storage,
+        ],
+      ], # end of combobox "Export"
+
+      action => [
+        t8('Delete'),
+        call     => [ 'kivi.Order.delete_order' ],
+        confirm  => $::locale->text('Do you really want to delete this object?'),
+        disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+        only_if  => $deletion_allowed,
+      ],
+    );
+  }
 }
 
 sub _create_pdf {
 }
 
 sub _create_pdf {
@@ -1002,9 +1500,8 @@ sub _create_pdf {
   $print_form->{media}       = $params->{media}    || 'file';
   $print_form->{groupitems}  = $params->{groupitems};
   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
   $print_form->{media}       = $params->{media}    || 'file';
   $print_form->{groupitems}  = $params->{groupitems};
   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
-  $print_form->{language}    = $params->{language}->template_code if $print_form->{language};
-  $print_form->{language_id} = $params->{language}->id            if $print_form->{language};
 
 
+  $order->language($params->{language});
   $order->flatten_to_form($print_form, format_amounts => 1);
 
   # search for the template
   $order->flatten_to_form($print_form, format_amounts => 1);
 
   # search for the template
@@ -1041,6 +1538,84 @@ sub _create_pdf {
   return @errors;
 }
 
   return @errors;
 }
 
+sub _get_files_for_email_dialog {
+  my ($self) = @_;
+
+  my %files = map { ($_ => []) } qw(versions files vc_files part_files);
+
+  return %files if !$::instance_conf->get_doc_storage;
+
+  if ($self->order->id) {
+    $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') ];
+  }
+
+  my @parts =
+    uniq_by { $_->{id} }
+    map {
+      +{ id         => $_->part->id,
+         partnumber => $_->part->partnumber }
+    } @{$self->order->items_sorted};
+
+  foreach my $part (@parts) {
+    my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
+    push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
+  }
+
+  foreach my $key (keys %files) {
+    $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
+  }
+
+  return %files;
+}
+
+sub _make_periodic_invoices_config_from_yaml {
+  my ($yaml_config) = @_;
+
+  return if !$yaml_config;
+  my $attr = YAML::Load($yaml_config);
+  return if 'HASH' ne ref $attr;
+  return SL::DB::PeriodicInvoicesConfig->new(%$attr);
+}
+
+
+sub _get_periodic_invoices_status {
+  my ($self, $config) = @_;
+
+  return                      if $self->type ne _sales_order_type();
+  return t8('not configured') if !$config;
+
+  my $active = ('HASH' eq ref $config)                           ? $config->{active}
+             : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
+             :                                                     die "Cannot get status of periodic invoices config";
+
+  return $active ? t8('active') : t8('inactive');
+}
+
+sub _get_title_for {
+  my ($self, $action) = @_;
+
+  return '' if none { lc($action)} qw(add edit);
+
+  # for locales:
+  # $::locale->text("Add Sales Order");
+  # $::locale->text("Add Purchase Order");
+  # $::locale->text("Add Quotation");
+  # $::locale->text("Add Request for Quotation");
+  # $::locale->text("Edit Sales Order");
+  # $::locale->text("Edit Purchase Order");
+  # $::locale->text("Edit Quotation");
+  # $::locale->text("Edit Request for Quotation");
+
+  $action = ucfirst(lc($action));
+  return $self->type eq _sales_order_type()       ? $::locale->text("$action Sales Order")
+       : $self->type eq _purchase_order_type()    ? $::locale->text("$action Purchase Order")
+       : $self->type eq _sales_quotation_type()   ? $::locale->text("$action Quotation")
+       : $self->type eq _request_quotation_type() ? $::locale->text("$action Request for Quotation")
+       : '';
+}
+
 sub _sales_order_type {
   'sales_order';
 }
 sub _sales_order_type {
   'sales_order';
 }
@@ -1049,6 +1624,14 @@ sub _purchase_order_type {
   'purchase_order';
 }
 
   'purchase_order';
 }
 
+sub _sales_quotation_type {
+  'sales_quotation';
+}
+
+sub _request_quotation_type {
+  'request_quotation';
+}
+
 1;
 
 __END__
 1;
 
 __END__
@@ -1068,36 +1651,44 @@ 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.
 
 of work. Also the code should be more readable, more reliable and
 better to maintain.
 
-=head2 key features
+=head2 Key Features
 
 
-=over 2
+=over 4
 
 =item *
 
 =item *
+
 One input row, so that input happens every time at the same place.
 
 =item *
 One input row, so that input happens every time at the same place.
 
 =item *
+
 Use of pickers where possible.
 
 =item *
 Use of pickers where possible.
 
 =item *
+
 Possibility to enter more than one item at once.
 
 =item *
 Possibility to enter more than one item at once.
 
 =item *
+
 Save order only on "save" (and "save and delivery order"-workflow). No
 Save order only on "save" (and "save and delivery order"-workflow). No
-hidden save on "print" or "email". 
+hidden save on "print" or "email".
 
 =item *
 
 =item *
+
 Item list in a scrollable area, so that the workflow buttons stay at
 the bottom.
 
 =item *
 Item list in a scrollable area, so that the workflow buttons stay at
 the bottom.
 
 =item *
+
 Reordering item rows with drag and drop is possible. Sorting item rows is
 possible (by partnumber, description, qty, sellprice and discount for now).
 
 =item *
 Reordering item rows with drag and drop is possible. Sorting item rows is
 possible (by partnumber, description, qty, sellprice and discount for now).
 
 =item *
-No "update" is necessary. All entries and calculations are managed
-with ajax-calls and the page does only reload on "save".
+
+No C<update> is necessary. All entries and calculations are managed
+with ajax-calls and the page does only reload on C<save>.
 
 =item *
 
 =item *
+
 User can see changes immediately, because of the use of java script
 and ajax.
 
 User can see changes immediately, because of the use of java script
 and ajax.
 
@@ -1105,127 +1696,183 @@ and ajax.
 
 =head1 CODE
 
 
 =head1 CODE
 
-=head2 layout
+=head2 Layout
 
 
-=over 2
+=over 4
 
 
-=item *
-SL/Controller/Order.pm: the controller
+=item * C<SL/Controller/Order.pm>
 
 
-=item *
-template/webpages/order/form.html: main form
+the controller
 
 
-=item *
-template/webpages/order/tabs/basic_data.html: main tab for basic_data
+=item * C<template/webpages/order/form.html>
 
 
-This is the only tab here for now. "linked records" and "webdav" tabs are reused
-from generic code.
+main form
 
 
-=over 3
+=item * C<template/webpages/order/tabs/basic_data.html>
 
 
-=item *
-template/webpages/order/tabs/_item_input.html: the input line for items
+Main tab for basic_data.
 
 
-=item *
-template/webpages/order/tabs/_row.html: one row for already entered items
+This is the only tab here for now. "linked records" and "webdav" tabs are
+reused from generic code.
 
 
-=item *
-template/webpages/order/tabs/_tax_row.html: displaying tax information
+=over 4
 
 
-=item *
-template/webpages/order/tabs/_multi_items_dialog.html: dialog for entering more
-than one item at once
+=item * C<template/webpages/order/tabs/_business_info_row.html>
 
 
-=item *
-template/webpages/order/tabs/_multi_items_result.html: results for the filter in
-the multi items dialog
+For displaying information on business type
 
 
-=item *
-template/webpages/order/tabs/_price_sources_dialog.html: dialog for selecting
-price and discount sources
+=item * C<template/webpages/order/tabs/_item_input.html>
 
 
-=item *
-template/webpages/order/tabs/_email_dialog.html: email dialog
+The input line for items
+
+=item * C<template/webpages/order/tabs/_row.html>
+
+One row for already entered items
+
+=item * C<template/webpages/order/tabs/_tax_row.html>
+
+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
 
 =back
 
 
 =back
 
-=item *
-js/kivi.Order.js: java script functions
+=item * C<js/kivi.Order.js>
+
+java script functions
 
 =back
 
 =head1 TODO
 
 
 =back
 
 =head1 TODO
 
-=over 2
+=over 4
 
 
-=item *
+=item * testing
 
 
-testing
+=item * currency
 
 
+=item * customer/vendor details ('D'-button)
 
 
-=item *
+=item * credit limit
 
 
-currency
+=item * more workflows (save as new, quotation, purchase order)
 
 
+=item * price sources: little symbols showing better price / better discount
 
 
-=item *
+=item * select units in input row?
 
 
-customer/vendor details ('D'-button)
+=item * custom shipto address
 
 
+=item * language / part translations
 
 
-=item *
+=item * access rights
 
 
-credit limit
+=item * display weights
 
 
+=item * history
 
 
-=item *
+=item * mtime check
 
 
-more workflows (save as new / invoice)
+=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)
 
 
-=item *
+=back
 
 
-price sources: little symbols showing better price / better discount
+=head1 KNOWN BUGS AND CAVEATS
 
 
+=over 4
 
 =item *
 
 
 =item *
 
-custom shipto address
+Customer discount is not displayed as a valid discount in price source popup
+(this might be a bug in price sources)
 
 
+(I cannot reproduce this (Bernd))
 
 =item *
 
 
 =item *
 
-periodic invoices
+No indication that <shift>-up/down expands/collapses second row.
+
+=item *
 
 
+Inline creation of parts is not currently supported
 
 =item *
 
 
 =item *
 
-more details on second row (marge, ...)
+Table header is not sticky in the scrolling area.
+
+=item *
+
+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.
 
 =item *
 
 
 =item *
 
-language / part translations
+The language selected in print or email dialog is not saved when the order is saved.
+
+=back
+
+=head1 To discuss / Nice to have
+
+=over 4
+
+=item *
 
 
+How to expand/collapse second row. Now it can be done clicking the icon or
+<shift>-up/down.
 
 =item *
 
 
 =item *
 
-access rights
+Possibility to change longdescription in input row?
+
+=item *
 
 
+Possibility to select PriceSources in input row?
 
 =item *
 
 
 =item *
 
-preset salesman from customer
+This controller uses a (changed) copy of the template for the PriceSource
+dialog. Maybe there could be used one code source.
 
 
+=item *
+
+Rounding-differences between this controller (PriceTaxCalculator) and the old
+form. This is not only a problem here, but also in all parts using the PTC.
+There exists a ticket and a patch. This patch should be testet.
 
 =item *
 
 
 =item *
 
-display weights
+An indicator, if the actual inputs are saved (like in an
+editor or on text processing application).
+
+=item *
 
 
+A warning when leaving the page without saveing unchanged inputs.
 
 =item *
 
 
 =item *
 
-force project if enabled in client config
+Workflows for delivery order and invoice are in the menu "Save", because the
+order is saved before opening the new document form. Nevertheless perhaps these
+workflow buttons should be put under "Workflows".
 
 
 =back
 
 
 =back