commit c22f1710614aa27af78e163e2897890f7bcc013e
Author: Tamino Steinert <tamino.steinert@tamino.st>
Date:   Wed Apr 27 10:58:24 2022 +0200

    Reclamation: Test for workflow invoice_to_reclamation

diff --git a/SL/DB/Invoice.pm b/SL/DB/Invoice.pm
index 7c2677838..3024d8274 100644
--- a/SL/DB/Invoice.pm
+++ b/SL/DB/Invoice.pm
@@ -5,7 +5,7 @@ use strict;
 use Carp;
 use List::Util qw(first sum);
 
-use Rose::DB::Object::Helpers qw(has_loaded_related forget_related);
+use Rose::DB::Object::Helpers qw(has_loaded_related forget_related as_tree strip);
 use SL::DB::MetaSetup::Invoice;
 use SL::DB::Manager::Invoice;
 use SL::DB::Helper::Payment qw(:ALL);
diff --git a/SL/DB/InvoiceItem.pm b/SL/DB/InvoiceItem.pm
index 80c406b77..d43509802 100644
--- a/SL/DB/InvoiceItem.pm
+++ b/SL/DB/InvoiceItem.pm
@@ -16,6 +16,7 @@ use SL::DB::Helper::CustomVariables (
     },
   },
 );
+use Rose::DB::Object::Helpers qw(as_tree strip);
 
 __PACKAGE__->meta->make_manager_class;
 
diff --git a/SL/DB/PurchaseInvoice.pm b/SL/DB/PurchaseInvoice.pm
index 3fdbae6ea..f30fefb94 100644
--- a/SL/DB/PurchaseInvoice.pm
+++ b/SL/DB/PurchaseInvoice.pm
@@ -13,7 +13,7 @@ use SL::DB::Helper::LinkedRecords;
 use SL::DB::Helper::Payment qw(:ALL);
 use SL::DB::Helper::SalesPurchaseInvoice;
 use SL::Locale::String qw(t8);
-use Rose::DB::Object::Helpers qw(has_loaded_related forget_related);
+use Rose::DB::Object::Helpers qw(has_loaded_related forget_related as_tree strip);
 
 # The calculator hasn't been adjusted for purchase invoices yet.
 # use SL::DB::Helper::PriceTaxCalculator;
diff --git a/SL/Dev/Record.pm b/SL/Dev/Record.pm
index 30623a696..ec7602290 100644
--- a/SL/Dev/Record.pm
+++ b/SL/Dev/Record.pm
@@ -37,6 +37,7 @@ use SL::Locale::String qw(t8);
 use SL::DATEV;
 
 my %record_type_to_item_type = ( sales_invoice        => 'SL::DB::InvoiceItem',
+                                 purchase_invoice     => 'SL::DB::InvoiceItem',
                                  credit_note          => 'SL::DB::InvoiceItem',
                                  sales_order          => 'SL::DB::OrderItem',
                                  purchase_order       => 'SL::DB::OrderItem',
@@ -77,6 +78,36 @@ sub create_sales_invoice {
   return $invoice;
 }
 
+sub create_purchase_invoice {
+  my (%params) = @_;
+
+  my $record_type = 'purchase_invoice';
+  my $invoiceitems = delete $params{invoiceitems} // _create_two_items($record_type);
+  _check_items($invoiceitems, $record_type);
+
+  my $vendor = delete $params{vendor} // new_vendor(name => 'Testvendor')->save;
+  die "illegal vendor" unless defined $vendor && ref($vendor) eq 'SL::DB::Vendor';
+
+  my $invoice = SL::DB::PurchaseInvoice->new(
+    invoice      => 1,
+    type         => 'purchase_invoice',
+    vendor       => $vendor->id,
+    taxzone_id   => $vendor->taxzone->id,
+    invnumber    => delete $params{invnumber}   // undef,
+    currency_id  => $params{currency_id} // $::instance_conf->get_currency_id,
+    taxincluded  => $params{taxincluded} // 0,
+    employee_id  => $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    transdate    => $params{transdate}   // DateTime->today_local->to_kivitendo,
+    payment_id   => $params{payment_id}  // undef,
+    gldate       => DateTime->today,
+    invoiceitems => $invoiceitems,
+  );
+  $invoice->assign_attributes(%params) if %params;
+
+  $invoice->save;
+  return $invoice;
+}
+
 sub create_credit_note {
   my (%params) = @_;
 
@@ -147,7 +178,7 @@ sub create_purchase_delivery_order {
   _check_items($orderitems, $record_type);
 
   my $vendor = $params{vendor} // new_vendor(name => 'Testvendor')->save;
-  die "illegal customer" unless ref($vendor) eq 'SL::DB::Vendor';
+  die "illegal vendor" unless ref($vendor) eq 'SL::DB::Vendor';
 
   my $delivery_order = SL::DB::DeliveryOrder->new(
     order_type   => PURCHASE_DELIVERY_ORDER_TYPE,
diff --git a/t/workflow/invoice_to_reclamation.t b/t/workflow/invoice_to_reclamation.t
new file mode 100644
index 000000000..dda13df74
--- /dev/null
+++ b/t/workflow/invoice_to_reclamation.t
@@ -0,0 +1,279 @@
+use Test::More;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Carp;
+use Data::Dumper;
+use Data::Compare;
+use Support::TestSetup;
+use Test::Exception;
+use List::Util qw(zip);
+
+use SL::DB::Order;
+use SL::DB::Reclamation;
+use SL::DB::ReclamationReason;
+use SL::DB::Customer;
+use SL::DB::Vendor;
+use SL::DB::Department;
+use SL::DB::Currency;
+use SL::DB::PaymentTerm;
+use SL::DB::DeliveryTerm;
+use SL::DB::Employee;
+use SL::DB::Part;
+use SL::DB::Unit;
+
+use Rose::DB::Object::Helpers qw(clone);
+
+use SL::Dev::ALL qw(:ALL);
+
+my (
+  $customer, $vendor,
+  $employee,
+  $payment_term,
+  $delivery_term,
+  $unit,
+  @parts,
+  $department,
+  $relamation_reason,
+);
+
+
+sub clear_up {
+  foreach (qw(
+    Invoice PurchaseInvoice InvoiceItem
+    Reclamation ReclamationItem
+    Part
+    Customer Vendor
+    Department PaymentTerm DeliveryTerm
+    )) {
+    "SL::DB::Manager::${_}"->delete_all(all => 1);
+  }
+  SL::DB::Manager::Employee->delete_all(where => [ login => 'testuser' ]);
+};
+
+sub reset_state {
+  my %params = @_;
+
+  clear_up();
+
+  $unit     = SL::DB::Manager::Unit->find_by(name => 'kg') || die "Can't find unit 'kg'";
+
+  $employee = SL::DB::Employee->new(
+    'login' => 'testuser',
+    'name'  => 'Test User',
+  )->save;
+
+  $department = SL::DB::Department->new(
+    'description' => 'Test Department',
+  )->save;
+
+  $payment_term = create_payment_terms(
+     'description'      => '14Tage 2%Skonto, 30Tage netto',
+     'description_long' => "Innerhalb von 14 Tagen abzüglich 2 % Skonto, innerhalb von 30 Tagen rein netto.|Bei einer Zahlung bis zum <%skonto_date%> gewähren wir 2 % Skonto (EUR <%skonto_amount%>) entspricht EUR <%total_wo_skonto%>.Bei einer Zahlung bis zum <%netto_date%> ist der fällige Betrag in Höhe von <%total%> <%currency%> zu überweisen.",
+     'percent_skonto'   => '0.02',
+     'terms_netto'      => 30,
+     'terms_skonto'     => 14
+  );
+
+  $delivery_term = SL::DB::DeliveryTerm->new(
+    'description'      => 'Test Delivey Term',
+    'description_long' => 'Test Delivey Term Test Delivey Term',
+  )->save;
+
+  # some parts/services
+  @parts = ();
+  push @parts, new_part(
+    partnumber => 'Part_1_KG',
+    unit        => $unit->name,
+  )->save;
+  push @parts, new_service(
+    partnumber => 'Serv_1',
+  )->save;
+  push @parts, new_part(
+    partnumber => 'Part_2',
+  )->save;
+  push @parts, new_service(
+    partnumber => 'Serv_2'
+  )->save;
+
+  $relamation_reason = SL::DB::ReclamationReason->new(
+    name => "test_reason",
+    description => "",
+    position => 1,
+  );
+}
+
+Support::TestSetup::login();
+
+reset_state();
+
+#####
+
+my $sales_reclamation = SL::Dev::Record::create_sales_reclamation(
+  save                    => 1,
+  employee                => $employee,
+  shippingpoint           => "sp",
+  transaction_description => "td1",
+  payment                 => $payment_term,
+  delivery_term           => $delivery_term,
+  taxincluded             => 0,
+  reclamation_items       => [
+    SL::Dev::Record::create_reclamation_item(
+      part => $parts[0], qty =>  3, sellprice => 70,
+      reason => $relamation_reason,
+    ),
+    SL::Dev::Record::create_reclamation_item(
+      part => $parts[1], qty => 10, sellprice => 50,
+      reason => $relamation_reason,
+    ),
+  ],
+)->load;
+
+my $purchase_reclamation = SL::Dev::Record::create_purchase_reclamation(
+  save                    => 1,
+  employee                => $employee,
+  shippingpoint           => "sp",
+  transaction_description => "td2",
+  payment                 => $payment_term,
+  delivery_term           => $delivery_term,
+  taxincluded             => 0,
+  reclamation_items       => [
+    SL::Dev::Record::create_reclamation_item(
+      part => $parts[0], qty =>  3, sellprice => 70,
+      reason => $relamation_reason,
+    ),
+    SL::Dev::Record::create_reclamation_item(
+      part => $parts[1], qty => 10, sellprice => 50,
+      reason => $relamation_reason,
+    ),
+  ],
+)->load;
+
+
+my $sales_invoice = SL::Dev::Record::create_sales_invoice(
+  save                    => 1,
+  employee                => $employee,
+  shippingpoint           => "sp",
+  transaction_description => "td3",
+  payment_terms           => $payment_term,
+  delivery_term           => $delivery_term,
+  taxincluded             => 0,
+  invoiceitems => [ SL::Dev::Record::create_invoice_item(part => $parts[0], qty =>  3, sellprice => 70),
+                  SL::Dev::Record::create_invoice_item(part => $parts[1], qty => 10, sellprice => 50),
+  ]
+)->load;
+
+my $purchase_invoice = SL::Dev::Record::create_purchase_invoice(
+  save                    => 1,
+  employee                => $employee,
+  invnumber               => "t1",
+  transaction_description => "td4",
+  payment_terms           => $payment_term,
+  delivery_term           => $delivery_term,
+  taxincluded             => 0,
+  invoiceitems => [ SL::Dev::Record::create_invoice_item(part => $parts[0], qty =>  3, sellprice => 70),
+                  SL::Dev::Record::create_invoice_item(part => $parts[1], qty => 10, sellprice => 50),
+  ]
+)->load;
+
+# convert invoice → reclamation
+my $converted_sales_reclamation = $sales_invoice->convert_to_reclamation;
+$converted_sales_reclamation->items_sorted->[0]->reason($relamation_reason);
+$converted_sales_reclamation->items_sorted->[1]->reason($relamation_reason);
+$converted_sales_reclamation->save->load;
+my $converted_purchase_reclamation = $purchase_invoice->convert_to_reclamation;
+$converted_purchase_reclamation->items_sorted->[0]->reason($relamation_reason);
+$converted_purchase_reclamation->items_sorted->[1]->reason($relamation_reason);
+$converted_purchase_reclamation->save->load;
+
+#get items before strip
+my @purchase_reclamation_items = $purchase_reclamation->items_sorted;
+my @sales_reclamation_items    = $sales_reclamation->items_sorted;
+my @converted_purchase_reclamation_items = $converted_purchase_reclamation->items_sorted;
+my @converted_sales_reclamation_items    = $converted_sales_reclamation->items_sorted;
+my @purchase_invoice_items = $purchase_invoice->items_sorted;
+my @sales_invoice_items    = $sales_invoice->items_sorted;
+
+
+### TESTS #####################################################################
+
+## created sales und purchase reclamation should be nearly the same
+my $sales_reclamation_tmp = clone($sales_reclamation);
+my $purchase_reclamation_tmp = clone($purchase_reclamation);
+# clean different values
+foreach (qw(
+  customer_id vendor_id
+  id record_number
+  salesman_id
+  transaction_description
+  itime mtime
+  )) {
+  $sales_reclamation_tmp->$_(undef);
+  $purchase_reclamation_tmp->$_(undef);
+}
+foreach my $pair (zip(@purchase_reclamation_items, @sales_reclamation_items)) {
+  my ($first, $second) = @{$pair};
+  my $first_tmp = clone($first);
+  my $second_tmp = clone($second);
+  foreach (qw(
+    id reclamation_id
+    itime mtime
+    )) {
+    $first_tmp->$_(undef);
+    $second_tmp->$_(undef);
+  }
+  is_deeply($first_tmp->strip->as_tree, $second_tmp->strip->as_tree);
+}
+is_deeply($purchase_reclamation_tmp->strip->as_tree, $sales_reclamation_tmp->strip->as_tree);
+
+## converted have to be linked to parent
+my $linked_sales_invoice = $converted_sales_reclamation->linked_records->[0];
+is_deeply($linked_sales_invoice->strip->as_tree, $sales_invoice->strip->as_tree);
+my $linked_purchase_invoice = $converted_purchase_reclamation->linked_records->[0];
+is_deeply($linked_purchase_invoice->strip->as_tree, $purchase_invoice->strip->as_tree);
+
+
+## converted should be nealy the same
+foreach my $pair (zip(@sales_invoice_items, @converted_sales_reclamation_items)) {
+  my ($first, $second) = @{$pair};
+  ok Compare($first->strip->as_tree, $second->strip->as_tree, {ignore_hash_keys => [qw(
+        id trans_id reclamation_id itime mtime
+        allocated assemblyitem cusordnumber deliverydate donumber fxsellprice marge_percent marge_price_factor marge_total optional ordnumber subtotal transdate
+        reason_description_ext reason_description_int reason_id reqdate
+      )]});
+}
+ok Compare($sales_invoice->strip->as_tree, $converted_sales_reclamation->strip->as_tree, {ignore_hash_keys => [qw(
+      id employee_id itime mtime transdate
+      datepaid delivery_customer_id delivery_vendor_id deliverydate direct_debit donumber duedate dunning_config_id gldate invnumber_for_credit_note invoice marge_percent marge_total orddate ordnumber paid qrbill_without_amount quodate quonumber storno storno_id type
+      delivered closed exchangerate reqdate vendor_id
+      cp_id contact_id
+      cusordnumber cv_record_number
+      invnumber record_number
+      )]});
+
+foreach my $pair (zip(@purchase_invoice_items, @converted_purchase_reclamation_items)) {
+  my ($first, $second) = @{$pair};
+  ok Compare($first->strip->as_tree, $second->strip->as_tree, {ignore_hash_keys => [qw(
+        id trans_id reclamation_id itime mtime
+        allocated assemblyitem cusordnumber deliverydate donumber fxsellprice marge_percent marge_price_factor marge_total optional ordnumber subtotal transdate
+        reason_description_ext reason_description_int reason_id reqdate
+      )]});
+}
+ok Compare($purchase_invoice->strip->as_tree, $converted_purchase_reclamation->strip->as_tree, {ignore_hash_keys => [qw(
+      id employee_id itime mtime transdate
+      datepaid deliverydate direct_debit duedate gldate invoice orddate ordnumber paid quodate quonumber storno storno_id type
+      customer_id cv_record_number delivered closed exchangerate reqdate salesman_id shippingpoint shipto_id
+      cp_id contact_id
+      invnumber record_number
+      )]});
+
+
+####
+clear_up();
+
+done_testing;
+
+1;
