commit a7ca8ba215c2a039e97c27bc3c637712fca837e9
Author: Jan Büren <jan@kivitendo-premium.de>
Date:   Fri Sep 4 11:31:22 2015 +0200

    DeliveryOrder um convert_invoice erweitert
    
    Diesselbe Idee wie bei SalesOrder->convert_invoice. Der ursprüngliche
    Lieferschein wird geschlossen und das neue Objekt mittels record_links
    verknüpft.
    Entsprechend Testfall mitgeliefert.

diff --git a/SL/DB/DeliveryOrder.pm b/SL/DB/DeliveryOrder.pm
index 7cfc2eac1..b6f1f9596 100644
--- a/SL/DB/DeliveryOrder.pm
+++ b/SL/DB/DeliveryOrder.pm
@@ -187,6 +187,39 @@ sub customervendor {
   $_[0]->is_sales ? $_[0]->customer : $_[0]->vendor;
 }
 
+sub convert_to_invoice {
+  my ($self, %params) = @_;
+
+  croak("Conversion to invoices is only supported for sales records") unless $self->customer_id;
+
+  my $invoice;
+  if (!$self->db->with_transaction(sub {
+    require SL::DB::Invoice;
+    $invoice = SL::DB::Invoice->new_from($self)->post(%params) || die;
+    $self->link_to_record($invoice);
+    foreach my $item (@{ $invoice->items }) {
+      foreach (qw(delivery_order_items)) {    # expand if needed (delivery_order_items)
+        if ($item->{"converted_from_${_}_id"}) {
+          die unless $item->{id};
+          RecordLinks->create_links('mode'       => 'ids',
+                                    'from_table' => $_,
+                                    'from_ids'   => $item->{"converted_from_${_}_id"},
+                                    'to_table'   => 'invoice',
+                                    'to_id'      => $item->{id},
+          ) || die;
+          delete $item->{"converted_from_${_}_id"};
+        }
+      }
+    }
+    $self->update_attributes(closed => 1);
+    1;
+  })) {
+    return undef;
+  }
+
+  return $invoice;
+}
+
 1;
 __END__
 
@@ -288,6 +321,21 @@ TODO: Describe sales_order
 Returns a string describing this record's type: either
 C<sales_delivery_order> or C<purchase_delivery_order>.
 
+=item C<convert_to_invoice %params>
+
+Creates a new invoice with C<$self> as the basis by calling
+L<SL::DB::Invoice::new_from>. That invoice is posted, and C<$self> is
+linked to the new invoice via L<SL::DB::RecordLink>. C<$self>'s
+C<closed> attribute is set to C<true>, and C<$self> is saved.
+
+The arguments in C<%params> are passed to L<SL::DB::Invoice::post>.
+
+Returns the new invoice instance on success and C<undef> on
+failure. The whole process is run inside a transaction. On failure
+nothing is created or changed in the database.
+
+At the moment only sales delivery orders can be converted.
+
 =back
 
 =head1 BUGS
diff --git a/t/db_helper/convert_invoice.t b/t/db_helper/convert_invoice.t
new file mode 100644
index 000000000..5279d65eb
--- /dev/null
+++ b/t/db_helper/convert_invoice.t
@@ -0,0 +1,290 @@
+use Test::More tests => 29;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Support::TestSetup;
+
+use Carp;
+use Data::Dumper;
+use Support::TestSetup;
+use Test::Exception;
+use List::Util qw(max);
+
+use SL::DB::Buchungsgruppe;
+use SL::DB::Currency;
+use SL::DB::Customer;
+use SL::DB::Employee;
+use SL::DB::Invoice;
+use SL::DB::Order;
+use SL::DB::DeliveryOrder;
+use SL::DB::Part;
+use SL::DB::Unit;
+use SL::DB::TaxZone;
+
+my ($customer, $currency_id, $buchungsgruppe, $employee, $vendor, $taxzone, $buchungsgruppe7, $tax, $tax7,
+    $unit, @parts);
+
+sub clear_up {
+  foreach (qw(DeliveryOrderItem DeliveryOrder InvoiceItem Invoice Part Customer Vendor Employee Department PaymentTerm)) {
+    "SL::DB::Manager::${_}"->delete_all(all => 1);
+  }
+};
+
+sub reset_state {
+  my %params = @_;
+
+  clear_up();
+
+  $buchungsgruppe   = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%', %{ $params{buchungsgruppe} }) || croak "No accounting group 19\%";
+  $buchungsgruppe7  = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 7%', %{ $params{buchungsgruppe} })  || croak "No accounting group 7\%";
+  $taxzone          = SL::DB::Manager::TaxZone->find_by( description => 'Inland')                                           || croak "No taxzone";
+  $tax              = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, %{ $params{tax} })                           || croak "No tax for 19\%";
+  $tax7             = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07)                                              || croak "No tax for 7\%";
+  $unit             = SL::DB::Manager::Unit->find_by(name => 'kg', %{ $params{unit} })                                      || croak "No unit";
+  $currency_id     = $::instance_conf->get_currency_id;
+
+  $customer     = SL::DB::Customer->new(
+    name        => '520484567dfaedc9e60fc',
+    currency_id => $currency_id,
+    taxzone_id  => $taxzone->id,
+    %{ $params{customer} }
+  )->save;
+
+  # some od.rnr real anonym data
+  my $employee_bk = SL::DB::Employee->new(
+                'id' => 31915,
+                'login' => 'barbuschka.kappes',
+                'name' => 'Barbuschka Kappes',
+  )->save;
+
+  my $department_do = SL::DB::Department->new(
+                 'description' => 'Maisenhaus-Versand',
+                 'id' => 32149,
+                 'itime' => undef,
+                 'mtime' => undef
+  )->save;
+
+  my $payment_do = SL::DB::PaymentTerm->new(
+                 '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.",
+                 'id' => 11276,
+                 'itime' => undef,
+                 'mtime' => undef,
+                 'percent_skonto' => '0.02',
+                 'ranking' => undef,
+                 'sortkey' => 4,
+                 'terms_netto' => 30,
+                 'auto_calculation' => undef,
+                 'terms_skonto' => 14
+  )->save;
+
+  # two real parts
+  @parts = ();
+  push @parts, SL::DB::Part->new(
+                 'id' => 26321,
+                 'image' => '',
+                 'lastcost' => '49.95000',
+                 'listprice' => '0.00000',
+                 'onhand' => '5.00000',
+                 'partnumber' => 'v-519160549',
+                 #'partsgroup_id' => 111645,
+                 'rop' => '0',
+                 'sellprice' => '242.20000',
+                 #'warehouse_id' => 64702,
+                 'weight' => '0.79',
+                 description        => "Nussbaum, Gr.5, Unterfilz weinrot, genietet[[Aufschnittbreite: 11,0, Kernform: US]]\"" ,
+                 buchungsgruppen_id => $buchungsgruppe->id,
+                 unit               => $unit->name,
+                 id                 => 26321,
+  )->save;
+
+  push @parts, SL::DB::Part->new(
+                 'description' => "[[0640]]Flügel Hammerstiele bestehend aus:
+70 Stielen Standard in Weißbuche und
+20 Stielen Diskant abgekehlt in Weißbuche
+mit Röllchen aus Synthetikleder,
+Kapseln mit Yamaha Profil, Kerbenabstand 3,6 mm mit eingedrehten Abnickschrauben",
+                 'id' => 25505,
+                 'lastcost' => '153.00000',
+                 'listprice' => '0.00000',
+                 'onhand' => '9.00000',
+                 'partnumber' => 'v-120160086',
+                 # 'partsgroup_id' => 111639,
+                 'rop' => '0',
+                 'sellprice' => '344.30000',
+                 'weight' => '0.9',
+                  buchungsgruppen_id => $buchungsgruppe->id,
+                  unit               => $unit->name,
+  )->save;
+}
+
+sub new_delivery_order {
+  my %params  = @_;
+
+  return SL::DB::DeliveryOrder->new(
+   currency_id => $currency_id,
+   taxzone_id  => $taxzone->id,
+    %params,
+  )->save;
+}
+
+Support::TestSetup::login();
+
+reset_state();
+
+# we create L20199 with two items
+my $do1 = new_delivery_order('department_id'    => 32149,
+                             'donumber'         => 'L20199',
+                             'employee_id'      => 31915,
+                             'intnotes'         => 'Achtung: Neue Lieferadresse ab 16.02.2015 in der Carl-von-Ossietzky-Str.32!   13.02.2015/MH
+
+                                            Steinway-Produkte (201...) immer plus 25% dazu rechnen / BK 13.02.2014',
+                              'ordnumber'       => 'A16399',
+                              'payment_id'      => 11276,
+                              'salesman_id'     => 31915,
+                              'shippingpoint'   => 'Maisenhaus',
+                              # 'shipto_id'     => 451463,
+                              'is_sales'        => 'true',
+                              'shipvia'         => 'DHL, Versand am 06.03.2015, 1 Paket  17,00 kg',
+                              'taxzone_id'      => 4,
+                              'closed'          => undef,
+                              # 'currency_id'   => 1,
+                              'cusordnumber'    => 'b84da',
+                              'customer_id'     => $customer->id,
+                              'id'              => 464003,
+);
+
+my $do1_item1 = SL::DB::DeliveryOrderItem->new('delivery_order_id' => 464003,
+                                               'description' => "Flügel Hammerkopf bestehend aus:
+                                                                 Bass/Diskant 26/65 Stück, Gesamtlänge 80/72, Bohrlänge 56/48
+                                                                 Nussbaum, Gr.5, Unterfilz weinrot, genietet[[Aufschnittbreite: 11,0, Kernform: US]]",
+                                               'discount' => '0.25',
+                                               'id' => 144736,
+                                               'lastcost' => '49.95000',
+                                               'longdescription' => '',
+                                               'marge_price_factor' => 1,
+                                               'mtime' => undef,
+                                               'ordnumber' => 'A16399',
+                                               'parts_id' => 26321,
+                                               'position' => 1,
+                                               'price_factor' => 1,
+                                               'qty' => '2.00000',
+                                               'sellprice' => '242.20000',
+                                               'transdate' => '06.03.2015',
+                                               'unit' => 'kg')->save;
+
+my $do1_item2 = SL::DB::DeliveryOrderItem->new('delivery_order_id' => 464003,
+                 'description' => "[[0640]]Flügel Hammerstiele bestehend aus:
+70 Stielen Standard in Weißbuche und
+20 Stielen Diskant abgekehlt in Weißbuche
+mit Röllchen aus Synthetikleder,
+Kapseln mit Yamaha Profil, Kerbenabstand 3,6 mm mit eingedrehten Abnickschrauben",
+                 'discount' => '0.25',
+                 'id' => 144737,
+                 'itime' => undef,
+                 'lastcost' => '153.00000',
+                 'longdescription' => '',
+                 'marge_price_factor' => 1,
+                 'mtime' => undef,
+                 'ordnumber' => 'A16399',
+                 'parts_id' => 25505,
+                 'position' => 2,
+                 'price_factor' => 1,
+                 'price_factor_id' => undef,
+                 'pricegroup_id' => undef,
+                 'project_id' => undef,
+                 'qty' => '3.00000',
+                 'reqdate' => undef,
+                 'sellprice' => '344.30000',
+                 'serialnumber' => '',
+                 'transdate' => '06.03.2015',
+                 'unit' => 'kg')->save;
+
+# TESTS
+
+
+# test delivery order before any conversion
+ok($do1->donumber eq "L20199", 'Delivery Order Number created');
+ok((not $do1->closed) , 'Delivery Order is not closed');
+ok($do1_item1->parts_id eq '26321', 'doi linked with part');
+ok($do1_item1->qty == 2, 'qty check doi');
+ok($do1_item2->position == 2, 'doi2 position check');
+ok(2 ==  scalar@{ SL::DB::Manager::DeliveryOrderItem->get_all(where => [ delivery_order_id => $do1->id ]) }, 'two doi linked');
+
+
+# convert this do to invoice
+my $invoice = $do1->convert_to_invoice();
+
+# test invoice afterwards
+
+ok ($invoice->shipvia eq "DHL, Versand am 06.03.2015, 1 Paket  17,00 kg", "ship via check");
+ok ($invoice->shippingpoint eq "Maisenhaus", "shipping point check");
+ok ($invoice->ordnumber eq "A16399", "ordnumber check");
+ok ($invoice->donumber eq "L20199", "donumber check");
+ok(($do1->closed) , 'Delivery Order is closed after conversion');
+ok (SL::DB::PaymentTerm->new(id => $invoice->{payment_id})->load->description eq "14Tage 2%Skonto, 30Tage netto", 'payment term description check');
+
+# some test data from original client invoice console (!)
+# my $invoice3 = SL::DB::Manager::Invoice->find_by( ordnumber => 'A16399' );
+# which will fail due to PTC Calculation differs from GUI-Calculation, see issue: http://redmine.kivitendo-premium.de/issues/82
+# pp $invoice3
+# values from gui should be:
+#ok($invoice->amount == 1354.20000, 'amount check');
+#ok($invoice->marge_percent == 50.88666, 'marge percent check');
+#ok($invoice->marge_total == 579.08000, 'marge total check');
+#ok($invoice->netamount == 1137.98000, 'netamount check');
+
+
+# the values change if one reloads the object
+# without reloading we get this failures
+#not ok 17 - amount check
+#   Failed test 'amount check'
+#   at t/db_helper/convert_invoice.t line 272.
+#          got: '1354.17'
+#     expected: '1354.17000'
+#not ok 18 - marge percent check
+#   Failed test 'marge percent check'
+#   at t/db_helper/convert_invoice.t line 273.
+#          got: '50.8857956342929'
+#     expected: '50.88580'
+#not ok 19 - marge total check
+#   Failed test 'marge total check'
+#   at t/db_helper/convert_invoice.t line 274.
+#          got: '579.06'
+#     expected: '579.06000'
+#not ok 20 - netamount check
+#   Failed test 'netamount check'
+#   at t/db_helper/convert_invoice.t line 275.
+#          got: '1137.96'
+#     expected: '1137.96000'
+
+$invoice->load;
+
+ok($invoice->currency_id eq '1', 'currency_id');
+ok($invoice->cusordnumber eq 'b84da', 'cusordnumber check');
+ok(SL::DB::Department->new(id => $invoice->{department_id})->load->description eq "Maisenhaus-Versand", 'department description');
+is($invoice->amount, '1354.17000', 'amount check');
+is($invoice->marge_percent, '50.88580', 'marge percent check');
+is($invoice->marge_total, '579.06000', 'marge total check');
+is($invoice->netamount, '1137.96000', 'netamount check');
+
+# some item checks
+ok(@ {$invoice->items_sorted}[0]->parts_id eq '26321', 'invoiceitem 1 linked with part');
+ok(2 ==  scalar@{ $invoice->invoiceitems }, 'two invoice items linked with invoice');
+is(@ {$invoice->items_sorted}[0]->position, 1, "position 1 order correct");
+is(@ {$invoice->items_sorted}[1]->position, 2, "position 2 order correct");
+is(@ {$invoice->items_sorted}[0]->part->partnumber, 'v-519160549', "partnumber 1 correct");
+is(@ {$invoice->items_sorted}[1]->part->partnumber, 'v-120160086', "partnumber 2 correct");
+is(@ {$invoice->items_sorted}[0]->qty, '2.00000', "pos 1 qty");
+is(@ {$invoice->items_sorted}[1]->qty, '3.00000', "pos 2 qty");
+is(@ {$invoice->items_sorted}[0]->discount, 0.25, "pos 1 discount");
+is(@ {$invoice->items_sorted}[1]->discount, 0.25, "pos 2 discount");
+
+# more ideas: check onhand, lastcost (parsed lastcost)
+
+clear_up();
+
+1;
