commit 95a9f8b99d7ef7c3150f1b7ebb3c66704ff3565e
Author: Tamino Steinert <tamino.steinert@tamino.st>
Date:   Wed Nov 3 09:15:35 2021 +0100

    Workflow: order ↔ reclamation

diff --git a/SL/Controller/Order.pm b/SL/Controller/Order.pm
index faf2ffaac..fdd7a5e28 100644
--- a/SL/Controller/Order.pm
+++ b/SL/Controller/Order.pm
@@ -62,11 +62,17 @@ __PACKAGE__->run_before('check_auth_for_edit',
                         except => [ qw(edit show_customer_vendor_details_dialog price_popup load_second_rows) ]);
 
 __PACKAGE__->run_before('recalc',
-                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
+                        only => [ qw(save save_as_new
+                                     save_and_delivery_order
+                                     save_and_reclamation
+                                     save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
                                      print send_email) ]);
 
 __PACKAGE__->run_before('get_unalterable_data',
-                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
+                        only => [ qw(save save_as_new
+                                     save_and_delivery_order
+                                     save_and_reclamation
+                                     save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
                                      print send_email) ]);
 
 #
@@ -96,6 +102,24 @@ sub action_add {
   );
 }
 
+sub action_add_from_reclamation {
+  my ($self) = @_;
+
+  require SL::DB::Reclamation;
+  my $reclamation = SL::DB::Reclamation->new(id => $::form->{from_id})->load;
+  my $order = $reclamation->convert_to_order();
+
+  $self->order($order);
+
+  $self->recalc();
+  $self->pre_render();
+  $self->render(
+    'order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+}
+
 # edit an existing order
 sub action_edit {
   my ($self) = @_;
@@ -740,6 +764,27 @@ sub action_save_and_supplier_delivery_order {
   );
 }
 
+# save the order and redirect to the frontend subroutine for a new reclamation
+sub action_save_and_reclamation {
+  my ($self) = @_;
+
+  # cann't use save_and_redirect_to, because id is set!
+  my $errors = $self->save();
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  my $to_type = $self->order->is_sales ? 'sales_reclamation'
+                                       : 'purchase_reclamation';
+  $self->redirect_to(
+    controller => 'Reclamation',
+    action     => 'add_from_order',
+    type       => $to_type,
+    from_id    => $self->order->id,
+  );
+}
+
 # save the order and redirect to the frontend subroutine for a new
 # invoice
 sub action_save_and_invoice {
@@ -2247,6 +2292,11 @@ sub setup_edit_action_bar {
           only_if   => (any { $self->type eq $_ } (purchase_order_type())),
           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
         ],
+        action => [
+          t8('Save and Reclamation'),
+          call      => [ 'kivi.Order.save', 'save_and_reclamation', $::instance_conf->get_order_warn_duplicate_parts ],
+          only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
+        ],
         action => [
           t8('Save and Invoice'),
           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
diff --git a/SL/Controller/Reclamation.pm b/SL/Controller/Reclamation.pm
index bd551b405..26b9b995e 100644
--- a/SL/Controller/Reclamation.pm
+++ b/SL/Controller/Reclamation.pm
@@ -68,6 +68,7 @@ __PACKAGE__->run_before('recalc',
                           save save_as_new print preview_pdf send_email
                           save_and_show_email_dialog
                           workflow_save_and_sales_or_purchase_reclamation
+                          save_and_order
                        )]);
 
 __PACKAGE__->run_before('get_unalterable_data',
@@ -75,6 +76,7 @@ __PACKAGE__->run_before('get_unalterable_data',
                           save save_as_new print preview_pdf send_email
                           save_and_show_email_dialog
                           workflow_save_and_sales_or_purchase_reclamation
+                          save_and_order
                         )]);
 
 #
@@ -96,6 +98,29 @@ sub action_add {
   );
 }
 
+sub action_add_from_order {
+  my ($self) = @_;
+
+  unless ($::form->{from_id}) {
+    $self->js->flash('error', t8("Can't create new reclamation. No 'from_id' was given."));
+    return $self->js->render();
+  }
+
+  require SL::DB::Order;
+  my $order = SL::DB::Order->new(id => $::form->{from_id})->load;
+  my $reclamation = $order->convert_to_reclamation();
+
+  $self->reclamation($reclamation);
+
+  $self->reinit_after_new_reclamation();
+
+  $self->render(
+    'reclamation/form',
+    title => $self->get_title_for('add'),
+    %{$self->{template_args}},
+  );
+}
+
 # edit an existing reclamation
 sub action_edit {
   my ($self) = @_;
@@ -467,6 +492,19 @@ sub action_send_email {
   $self->redirect_to(@redirect_params);
 }
 
+sub action_save_and_order {
+  my ($self) = @_;
+
+  my $to_type = $self->reclamation->is_sales ? 'sales_order'
+                                             : 'purchase_order';
+  $self->save_and_redirect_to(
+    controller => 'Order',
+    action     => 'add_from_reclamation',
+    type       => $to_type,
+    from_id    => $self->reclamation->id,
+  );
+}
+
 # workflow from purchase to sales reclamation
 sub action_save_and_sales_reclamation {
   $_[0]->workflow_save_and_sales_or_purchase_reclamation();
@@ -1578,9 +1616,11 @@ sub _link_to_records {
   my ($self) = @_;
   my %allowed_linked_records = map {$_ => 1} qw(
     SL::DB::Reclamation
+    SL::DB::Order
   );
   my %allowed_linked_record_items = map {$_ => 1} qw(
     SL::DB::ReclamationItem
+    SL::DB::OrderItem
   );
 
   my $from_record_id = delete $::form->{converted_from_record_id};
@@ -2085,6 +2125,14 @@ sub _setup_edit_action_bar {
           call      => [ 'kivi.Reclamation.purchase_reclamation_check_for_direct_delivery' ],
           only_if   => (any { $self->type eq $_ } (sales_reclamation_type())),
         ],
+        action => [
+          t8('Save and Order'),
+          call      => [
+            'kivi.Reclamation.save', 'save_and_order',
+            $::instance_conf->get_reclamation_warn_duplicate_parts,
+            $::instance_conf->get_reclamation_warn_no_reqdate,
+          ],
+        ],
       ], # end of combobox "Workflow"
 
       combobox => [
diff --git a/SL/DB/Order.pm b/SL/DB/Order.pm
index fdaa1e8bf..96771152d 100644
--- a/SL/DB/Order.pm
+++ b/SL/DB/Order.pm
@@ -309,6 +309,17 @@ sub convert_to_delivery_order {
   return $delivery_order;
 }
 
+sub convert_to_reclamation {
+  my ($self, %params) = @_;
+  $params{destination_type} = $self->is_sales ? 'sales_reclamation'
+                                              : 'purchase_reclamation';
+
+  require SL::DB::Reclamation;
+  my $reclamation = SL::DB::Reclamation->new_from($self, %params);
+
+  return $reclamation;
+}
+
 sub _clone_orderitem_cvar {
   my ($cvar) = @_;
 
@@ -321,7 +332,12 @@ sub _clone_orderitem_cvar {
 sub new_from {
   my ($class, $source, %params) = @_;
 
-  croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) eq 'SL::DB::Order';
+  unless (any {ref($source) eq $_} qw(
+    SL::DB::Order
+    SL::DB::Reclamation
+  )) {
+    croak("Unsupported source object type '" . ref($source) . "'");
+  }
   croak("A destination type must be given as parameter")         unless $params{destination_type};
 
   my $destination_type  = delete $params{destination_type};
@@ -337,6 +353,8 @@ sub new_from {
     { from => 'purchase_order',    to => 'sales_order',       abbr => 'poso' },
     { from => 'sales_order',       to => 'sales_quotation',   abbr => 'sosq' },
     { from => 'purchase_order',    to => 'request_quotation', abbr => 'porq' },
+    { from => 'sales_reclamation', to => 'sales_order',       abbr => 'srso' },
+    { from => 'purchase_reclamation', to => 'purchase_order', abbr => 'prpo' },
   );
   my $from_to = (grep { $_->{from} eq $source->type && $_->{to} eq $destination_type} @from_tos)[0];
   croak("Cannot convert from '" . $source->type . "' to '" . $destination_type . "'") if !$from_to;
@@ -350,19 +368,39 @@ sub new_from {
   if (ref($source) eq 'SL::DB::Order') {
     $item_parent_id_column = 'trans_id';
     $item_parent_column    = 'order';
+  } elsif ( ref($source) eq 'SL::DB::Reclamation') {
+    $item_parent_id_column = 'reclamation_id';
+    $item_parent_column    = 'reclamation';
   }
 
-  my %args = ( map({ ( $_ => $source->$_ ) } qw(amount cp_id currency_id cusordnumber customer_id delivery_customer_id delivery_term_id delivery_vendor_id
-                                                department_id exchangerate globalproject_id intnotes marge_percent marge_total language_id netamount notes
-                                                ordnumber payment_id quonumber reqdate salesman_id shippingpoint shipvia taxincluded tax_point taxzone_id
-                                                transaction_description vendor_id billing_address_id
-                                             )),
-               quotation => !!($destination_type =~ m{quotation$}),
-               closed    => 0,
-               delivered => 0,
-               transdate => DateTime->today_local,
-               employee  => SL::DB::Manager::Employee->current,
-            );
+  my %args;
+  if (ref($source) eq 'SL::DB::Order') {
+    %args = ( map({ ( $_ => $source->$_ ) } qw(amount cp_id currency_id cusordnumber customer_id delivery_customer_id delivery_term_id delivery_vendor_id
+                                                  department_id exchangerate globalproject_id intnotes marge_percent marge_total language_id netamount notes
+                                                  ordnumber payment_id quonumber reqdate salesman_id shippingpoint shipvia taxincluded tax_point taxzone_id
+                                                  transaction_description vendor_id billing_address_id
+                                               )),
+                 quotation => !!($destination_type =~ m{quotation$}),
+                 closed    => 0,
+                 delivered => 0,
+                 transdate => DateTime->today_local,
+                 employee  => SL::DB::Manager::Employee->current,
+              );
+  } elsif ( ref($source) eq 'SL::DB::Reclamation') {
+    #TODO(Tamino): add billing_address_id to reclamation
+    %args = ( map({ ( $_ => $source->$_ ) } qw(
+        amount currency_id customer_id delivery_term_id department_id
+        exchangerate globalproject_id intnotes language_id netamount
+        notes payment_id  reqdate salesman_id shippingpoint shipvia taxincluded
+        tax_point taxzone_id transaction_description vendor_id
+      )),
+      cp_id     => $source->{contact_id},
+      closed    => 0,
+      delivered => 0,
+      transdate => DateTime->today_local,
+      employee  => SL::DB::Manager::Employee->current,
+   );
+  }
 
   if ( $is_abbr_any->(qw(sopo poso)) ) {
     $args{ordnumber} = undef;
@@ -412,16 +450,29 @@ sub new_from {
     $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
     my $item_parent                  = $item_parents{$source_item_id};
 
-    my $current_oe_item = SL::DB::OrderItem->new(map({ ( $_ => $source_item->$_ ) }
-                                                     qw(active_discount_source active_price_source base_qty cusordnumber
-                                                        description discount lastcost longdescription
-                                                        marge_percent marge_price_factor marge_total
-                                                        ordnumber parts_id price_factor price_factor_id pricegroup_id
-                                                        project_id qty reqdate sellprice serialnumber ship subtotal transdate unit
-                                                        optional
-                                                     )),
-                                                 custom_variables => \@custom_variables,
-    );
+    my $current_oe_item;
+    if (ref($source) eq 'SL::DB::Order') {
+      $current_oe_item = SL::DB::OrderItem->new(map({ ( $_ => $source_item->$_ ) }
+                                                       qw(active_discount_source active_price_source base_qty cusordnumber
+                                                          description discount lastcost longdescription
+                                                          marge_percent marge_price_factor marge_total
+                                                          ordnumber parts_id price_factor price_factor_id pricegroup_id
+                                                          project_id qty reqdate sellprice serialnumber ship subtotal transdate unit
+                                                          optional
+                                                       )),
+                                                   custom_variables => \@custom_variables,
+      );
+    } elsif (ref($source) eq 'SL::DB::Reclamation') {
+      $current_oe_item = SL::DB::OrderItem->new(
+        map({ ( $_ => $source_item->$_ ) } qw(
+          active_discount_source active_price_source base_qty description
+          discount lastcost longdescription parts_id price_factor
+          price_factor_id pricegroup_id project_id qty reqdate sellprice
+          serialnumber unit
+        )),
+        custom_variables => \@custom_variables,
+      );
+    }
     if ( $is_abbr_any->(qw(sopo)) ) {
       $current_oe_item->sellprice($source_item->lastcost);
       $current_oe_item->discount(0);
@@ -430,6 +481,7 @@ sub new_from {
       $current_oe_item->lastcost($source_item->sellprice);
     }
     $current_oe_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
+    $current_oe_item->{"converted_from_reclamation_item_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Reclamation';
     $current_oe_item;
   } @{ $items };
 
diff --git a/SL/DB/Reclamation.pm b/SL/DB/Reclamation.pm
index 246e541d5..c2721ff91 100644
--- a/SL/DB/Reclamation.pm
+++ b/SL/DB/Reclamation.pm
@@ -181,11 +181,47 @@ sub valid_reclamation_reasons {
       where => [  $valid_for_type => 1 ]);
 }
 
+sub convert_to_order {
+  my ($self, %params) = @_;
+
+  my $order;
+  $params{destination_type} = $self->is_sales ? 'sales_order'
+                                              : 'purchase_order';
+  if (!$self->db->with_transaction(sub {
+    require SL::DB::Order;
+    $order = SL::DB::Order->new_from($self, %params);
+    $order->save;
+    $self->link_to_record($order);
+    foreach my $item (@{ $order->items }) {
+      foreach (qw(reclamation_item)) {
+        if ($item->{"converted_from_${_}_id"}) {
+          die unless $item->{id};
+          RecordLinks->create_links('dbh'        => $self->db->dbh,
+                                    'mode'       => 'ids',
+                                    'from_table' => 'reclamation_items',
+                                    'from_ids'   => $item->{"converted_from_${_}_id"},
+                                    'to_table'   => 'orderitems',
+                                    'to_id'      => $item->{id},
+          ) || die;
+          delete $item->{"converted_from_${_}_id"};
+        }
+      }
+    }
+
+    1;
+  })) {
+    return undef;
+  }
+
+  return $order;
+}
+
 #TODO(Werner): überprüfen ob alle Felder richtig gestetzt werden
 sub new_from {
   my ($class, $source, %params) = @_;
   my %allowed_sources = map { $_ => 1 } qw(
     SL::DB::Reclamation
+    SL::DB::Order
   );
   unless( $allowed_sources{ref $source} ) {
     croak("Unsupported source object type '" . ref($source) . "'");
@@ -200,6 +236,9 @@ sub new_from {
     { from => 'purchase_reclamation',    to => 'purchase_reclamation', abbr => 'prpr', },
     { from => 'sales_reclamation',       to => 'purchase_reclamation', abbr => 'srpr', },
     { from => 'purchase_reclamation',    to => 'sales_reclamation',    abbr => 'prsr', },
+    #Order
+    { from => 'sales_order',             to => 'sales_reclamation',    abbr => 'sosr', },
+    { from => 'purchase_order',          to => 'purchase_reclamation', abbr => 'popr', },
   );
   my $from_to = (grep { $_->{from} eq $source->type && $_->{to} eq $destination_type} @from_tos)[0];
   if (!$from_to) {
@@ -244,6 +283,33 @@ sub new_from {
       transaction_description
       vendor_id
     ); # }}} for vim folds
+  } elsif ( $is_abbr_any->(qw(sosr popr)) ) { #Order
+    map { $record_args{$_} = $source->$_ } # {{{ for vim folds
+    qw(
+      amount
+      currency_id
+      customer_id
+      delivery_term_id
+      department_id
+      exchangerate
+      globalproject_id
+      intnotes
+      language_id
+      netamount
+      notes
+      payment_id
+      salesman_id
+      shippingpoint
+      shipvia
+      tax_point
+      taxincluded
+      taxzone_id
+      transaction_description
+      vendor_id
+    );
+    $record_args{contact_id} = $source->cp_id;
+    $record_args{cv_record_number} = $source->cusordnumber;
+    # }}} for vim folds
   }
 
   if ( ($from_to->{from} =~ m{sales}) && ($from_to->{to} =~ m{purchase}) ) {
diff --git a/SL/DB/ReclamationItem.pm b/SL/DB/ReclamationItem.pm
index 7797beb01..04bb81402 100644
--- a/SL/DB/ReclamationItem.pm
+++ b/SL/DB/ReclamationItem.pm
@@ -53,6 +53,7 @@ sub new_from {
   unless (any {ref($source) eq $_}
     qw(
       SL::DB::ReclamationItem
+      SL::DB::OrderItem
     )
   ) {
     croak("Unsupported source object type '" . ref($source) . "'");
@@ -70,6 +71,13 @@ sub new_from {
       unit
     );
     $item_args{custom_variables} = \@custom_variables;
+  } elsif (ref($source) eq 'SL::DB::OrderItem') {
+    map { $item_args{$_} = $source->$_ } qw(
+      active_discount_source active_price_source base_qty description discount
+      lastcost longdescription parts_id position price_factor price_factor_id
+      pricegroup_id project_id qty reqdate sellprice serialnumber unit
+    );
+    $item_args{custom_variables} = \@custom_variables;
   }
 
   my $item = $class->new(%item_args);
