+
+sub transfer_out_default {
+ $main::lxdebug->enter_sub();
+
+ my $form = $main::form;
+
+ transfer_in_out_default('direction' => 'out');
+
+ $main::lxdebug->leave_sub();
+}
+
+sub transfer_in_default {
+ $main::lxdebug->enter_sub();
+
+ my $form = $main::form;
+
+ transfer_in_out_default('direction' => 'in');
+
+ $main::lxdebug->leave_sub();
+}
+
+# Falls das Standardlagerverfahren aktiv ist, wird
+# geprüft, ob alle Standardlagerplätze für die Auslager-
+# artikel vorhanden sind UND ob die Warenmenge ausreicht zum
+# Auslagern. Falls nicht wird entsprechend eine Fehlermeldung
+# generiert. Offen Chargennummer / bestbefore wird nicht berücksichtigt
+sub transfer_in_out_default {
+ $main::lxdebug->enter_sub();
+
+ my $form = $main::form;
+ my %myconfig = %main::myconfig;
+ my $locale = $main::locale;
+ my %params = @_;
+
+ my (%missing_default_bins, %qty_parts, @all_requests, %part_info_map, $default_warehouse_id, $default_bin_id);
+
+ Common::check_params(\%params, qw(direction));
+
+ # entsprechende defaults holen, falls standardlagerplatz verwendet werden soll
+ if ($::instance_conf->get_transfer_default_use_master_default_bin) {
+ $default_warehouse_id = $::instance_conf->get_warehouse_id;
+ $default_bin_id = $::instance_conf->get_bin_id;
+ }
+
+
+ my @part_ids = map { $form->{"id_${_}"} } (1 .. $form->{rowcount});
+ if (@part_ids) {
+ my $units = AM->retrieve_units(\%myconfig, $form);
+ %part_info_map = IC->get_basic_part_info('id' => \@part_ids);
+ foreach my $i (1 .. $form->{rowcount}) {
+ next unless ($form->{"id_$i"});
+ my $base_unit_factor = $units->{ $part_info_map{$form->{"id_$i"}}->{unit} }->{factor} || 1;
+ my $qty = $form->parse_amount(\%myconfig, $form->{"qty_$i"}) * $units->{$form->{"unit_$i"}}->{factor} / $base_unit_factor;
+
+ $form->show_generic_error($locale->text("Cannot transfer negative entries." )) if ($qty < 0);
+ # if we do not want to transfer services and this part is a service, set qty to zero
+ # ... and do not create a hash entry in %qty_parts below (will skip check for bins for the transfer == out case)
+ # ... and push only a empty (undef) element to @all_requests (will skip check for bin_id and warehouse_id and will not alter the row)
+
+ $qty = 0 if (!$::instance_conf->get_transfer_default_services && $part_info_map{$form->{"id_$i"}}->{part_type} eq 'service');
+ $qty_parts{$form->{"id_$i"}} += $qty;
+ if ($qty == 0) {
+ delete $qty_parts{$form->{"id_$i"}} unless $qty_parts{$form->{"id_$i"}};
+ undef $form->{"stock_in_$i"};
+ }
+
+ $part_info_map{$form->{"id_$i"}}{bin_id} ||= $default_bin_id;
+ $part_info_map{$form->{"id_$i"}}{warehouse_id} ||= $default_warehouse_id;
+
+ push @all_requests, ($qty == 0) ? { } : {
+ 'chargenumber' => '', #?? die müsste entsprechend geholt werden
+ #'bestbefore' => undef, # TODO wird nicht berücksichtigt
+ 'bin_id' => $part_info_map{$form->{"id_$i"}}{bin_id},
+ 'qty' => $qty,
+ 'parts_id' => $form->{"id_$i"},
+ 'comment' => $locale->text("Default transfer delivery order"),
+ 'unit' => $part_info_map{$form->{"id_$i"}}{unit},
+ 'warehouse_id' => $part_info_map{$form->{"id_$i"}}{warehouse_id},
+ 'oe_id' => $form->{id},
+ 'project_id' => $form->{"project_id_$i"} ? $form->{"project_id_$i"} : $form->{globalproject_id}
+ };
+ }
+
+ # jetzt wird erst überprüft, ob die Stückzahl entsprechend stimmt.
+ # check if bin (transfer in and transfer out and qty (transfer out) is correct
+ foreach my $key (keys %qty_parts) {
+
+ $missing_default_bins{$key}{missing_bin} = 1 unless ($part_info_map{$key}{bin_id});
+ next unless ($part_info_map{$key}{bin_id}); # abbruch
+
+ if ($params{direction} eq 'out') { # wird nur für ausgehende Mengen benötigt
+ my ($max_qty, $error) = WH->get_max_qty_parts_bin(parts_id => $key, bin_id => $part_info_map{$key}{bin_id});
+ if ($error == 1) {
+ # wir können nicht entscheiden, welche charge oder mhd (bestbefore) ausgewählt sein soll
+ # deshalb rückmeldung nach oben geben, manuell auszulagern
+ # TODO Bei nur einem Treffer mit Charge oder bestbefore wäre das noch möglich
+ $missing_default_bins{$key}{chargenumber} = 1;
+ }
+ if ($max_qty < $qty_parts{$key}){
+ $missing_default_bins{$key}{missing_qty} = $max_qty - $qty_parts{$key};
+ }
+ }
+ }
+ } # if @parts_id
+
+ # Abfrage für Fehlerbehandlung (nur bei direction == out)
+ if (scalar (keys %missing_default_bins)) {
+ my $fehlertext;
+ foreach my $fehler (keys %missing_default_bins) {
+
+ my $ware = WH->get_part_description(parts_id => $fehler);
+ if ($missing_default_bins{$fehler}{missing_bin}){
+ $fehlertext .= "Kein Standardlagerplatz definiert bei $ware <br>";
+ }
+ if ($missing_default_bins{$fehler}{missing_qty}) { # missing_qty
+ $fehlertext .= "Es fehlen " . $missing_default_bins{$fehler}{missing_qty}*-1 .
+ " von $ware auf dem Standard-Lagerplatz " . $part_info_map{$fehler}{bin} . " zum Auslagern<br>";
+ }
+ if ($missing_default_bins{$fehler}{chargenumber}){
+ $fehlertext .= "Die Ware hat eine Chargennummer oder eine Mindesthaltbarkeit definiert.
+ Hier kann man nicht automatisch entscheiden.
+ Bitte diesen Lieferschein manuell auslagern.
+ Bei: $ware";
+ }
+ # auslagern soll immer gehen, auch wenn nicht genügend auf lager ist.
+ # der lagerplatz ist hier extra konfigurierbar, bspw. Lager-Korrektur mit
+ # Lagerplatz Lagerplatz-Korrektur
+ my $default_warehouse_id_ignore_onhand = $::instance_conf->get_warehouse_id_ignore_onhand;
+ my $default_bin_id_ignore_onhand = $::instance_conf->get_bin_id_ignore_onhand;
+ if ($::instance_conf->get_transfer_default_ignore_onhand && $default_bin_id_ignore_onhand) {
+ # entsprechende defaults holen
+ # falls chargenumber, bestbefore oder anzahl nicht stimmt, auf automatischen
+ # lagerplatz wegbuchen!
+ foreach (@all_requests) {
+ if ($_->{parts_id} eq $fehler){
+ $_->{bin_id} = $default_bin_id_ignore_onhand;
+ $_->{warehouse_id} = $default_warehouse_id_ignore_onhand;
+ }
+ }
+ } else {
+ #$main::lxdebug->message(0, 'Fehlertext: ' . $fehlertext);
+ $form->show_generic_error($locale->text("Cannot transfer. <br> Reason:<br>#1", $fehlertext ));
+ }
+ }
+ }
+
+
+ # hier der eigentliche fallunterschied für in oder out
+ my $prefix = $params{direction} eq 'in' ? 'in' : 'out';
+
+ # dieser array_ref ist für DO->save da:
+ # einmal die all_requests in YAML verwandeln, damit delivery_order_items_stock
+ # gefüllt werden kann.
+ # could be dumped to the form in the first loop,
+ # but maybe bin_id and warehouse_id has changed to the "korrekturlager" with
+ # allowed negative qty ($::instance_conf->get_warehouse_id_ignore_onhand) ...
+ my $i = 0;
+ foreach (@all_requests){
+ $i++;
+ next unless scalar(%{ $_ });
+ $form->{"stock_${prefix}_$i"} = SL::YAML::Dump([$_]);
+ }
+
+ save(no_redirect => 1); # Wir können auslagern, deshalb beleg speichern
+ # und in delivery_order_items_stock speichern
+
+ # ... and fill back the persistent dois_id for inventory fk
+ undef (@all_requests);
+ foreach my $i (1 .. $form->{rowcount}) {
+ next unless ($form->{"id_$i"} && $form->{"stock_${prefix}_$i"});
+ push @all_requests, @{ DO->unpack_stock_information('packed' => $form->{"stock_${prefix}_$i"}) };
+ }
+ DO->transfer_in_out('direction' => $prefix,
+ 'requests' => \@all_requests);
+
+ SL::DB::DeliveryOrder->new(id => $form->{id})->load->update_attributes(delivered => 1);
+
+ $form->{callback} = 'do.pl?action=edit&type=sales_delivery_order&id=' . $form->escape($form->{id}) if $params{direction} eq 'out';
+ $form->{callback} = 'do.pl?action=edit&type=purchase_delivery_order&id=' . $form->escape($form->{id}) if $params{direction} eq 'in';
+ $form->redirect;
+
+}
+
+sub sort {
+ $main::lxdebug->enter_sub();
+
+ check_do_access();
+
+ my $form = $main::form;
+ my %temp_hash;
+
+ save(no_redirect => 1); # has to be done, at least for newly added positions
+
+ # hashify partnumbers, positions. key is delivery_order_items_id
+ for my $i (1 .. ($form->{rowcount}) ) {
+ $temp_hash{$form->{"delivery_order_items_id_$i"}} = { runningnumber => $form->{"runningnumber_$i"}, partnumber => $form->{"partnumber_$i"} };
+ if ($form->{id} && $form->{"discount_$i"}) {
+ # prepare_order assumes a db value if there is a form->id and multiplies *100
+ # We hope for new controller code (no more format_amount/parse_amount distinction)
+ $form->{"discount_$i"} /=100;
+ }
+ }
+ # naturally sort partnumbers and get a sorted array of doi_ids
+ my @sorted_doi_ids = sort { Sort::Naturally::ncmp($temp_hash{$a}->{"partnumber"}, $temp_hash{$b}->{"partnumber"}) } keys %temp_hash;
+
+
+ my $new_number = 1;
+
+ for (@sorted_doi_ids) {
+ $form->{"runningnumber_$temp_hash{$_}->{runningnumber}"} = $new_number;
+ $new_number++;
+ }
+ # all parse_amounts changes are in form (i.e. , to .) therefore we need
+ # another format_amount to change it back, for the next save ;-(
+ # works great except for row discounts (see above comment)
+ prepare_order();
+
+
+ $main::lxdebug->leave_sub();
+ save();
+}
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+do.pl - Script for all calls to delivery order
+
+=head1 FUNCTIONS
+
+=over 2
+
+=item C<sort>
+
+Sorts all position with Natural Sort. Can be activated in form_footer.html like this
+C<E<lt>input class="submit" type="submit" name="action_sort" id="sort_button" value="[% 'Sort and Save' | $T8 %]"E<gt>>
+
+=back
+
+=head1 TODO
+
+Sort and Save can be implemented as an optional button if configuration ca be set by client config.
+Example coding for database scripts and templates in (git show af2f24b8), check also
+autogeneration for rose (scripts/rose_auto_create_model.pl --h)