+sub action_transfer_stock {
+ my ($self, $default_transfer) = @_;
+
+ if ($self->order->delivered) {
+ return $self->js->flash("error",
+ t8('The parts for this order have already been transferred')
+ )->render;
+ }
+
+ my $inout = $self->type_data->properties('transfer');
+
+ $self->save;
+
+ my $order = $self->order;
+
+ # TODO move to type data
+ my $trans_type = $inout eq 'in'
+ ? SL::DB::Manager::TransferType->find_by(
+ direction => "in", description => "stock")
+ : SL::DB::Manager::TransferType->find_by(
+ direction => "out", description => "shipped");
+
+
+ my @transfer_requests;
+
+ for my $item (@{ $order->items_sorted }) {
+ for my $stock (@{ $item->delivery_order_stock_entries }) {
+ my $transfer = SL::DB::Inventory->new_from($stock);
+ $transfer->trans_type($trans_type);
+ $transfer->oe_id($order->id);
+ $transfer->qty($transfer->qty * -1) if $inout eq 'out';
+ $transfer->qty($transfer->qty * 1) if $inout eq 'in';
+ $transfer->comment(t8("Default transfer delivery order")) if $default_transfer;
+
+ push @transfer_requests, $transfer if defined $transfer->qty && $transfer->qty != 0;
+ };
+ }
+
+ if (!@transfer_requests) {
+ return $self->js->flash("error", t8("No stock to transfer"))->render;
+ }
+
+ if ($inout eq 'out') { # check stock for enough qty
+ my @missing_qtys = SL::Helper::Inventory::check_stock_out_transfer_requests(
+ transfer_requests => \@transfer_requests,
+ default_transfer => $default_transfer,
+ );
+
+ if (scalar @missing_qtys) {
+ my $error = t8('The stock is to low: #1.',
+ join(". ", map {
+ $_->{chargenumber} && $_->{bestbefore}
+ ? t8(
+ "For #1, #2 #3 are missing of batch with chargenumber #4 and bestbefore date of #5 in bin #6",
+ $_->{part}->displayable_name,
+ $::form->format_amount(\%::myconfig, $_->{missing_qty}),
+ $_->{part}->unit,
+ $_->{chargenumber},
+ DateTime->from_ymdhms($_->{bestbefore})->to_kivitendo,
+ $_->{bin}->full_description,
+ )
+ : $_->{chargenumber}
+ ? t8(
+ "For #1, #2 #3 are missing of batch with chargenumber #4 in bin #5",
+ $_->{part}->displayable_name,
+ $::form->format_amount(\%::myconfig, $_->{missing_qty}),
+ $_->{part}->unit,
+ $_->{chargenumber},
+ $_->{bin}->full_description,
+ )
+ : $_->{bestbefore}
+ ? t8(
+ "For #1, #2 #3 are missing with a bestbefore date of #4 in bin #5",
+ $_->{part}->displayable_name,
+ $::form->format_amount(\%::myconfig, $_->{missing_qty}),
+ $_->{part}->unit,
+ DateTime->from_ymdhms($_->{bestbefore})->to_kivitendo,
+ $_->{bin}->full_description,
+ )
+ : t8(
+ "For #1, #2 #3 are missing in bin #4",
+ $_->{part}->displayable_name,
+ $::form->format_amount(\%::myconfig, $_->{missing_qty}),
+ $_->{part}->unit,
+ $_->{bin}->full_description,
+ )
+ ;
+ } @missing_qtys
+ )
+ );
+ return $self->js->flash("error", $error)->render;
+ }
+ }
+
+ SL::DB->client->with_transaction(sub {
+
+ $_->save for @transfer_requests;
+ $self->order->update_attributes(delivered => 1);
+ });
+ # update qty and stock info
+ foreach my $item (@{$self->order->items}) {
+ $self->order->prepare_stock_info($item);
+ my $stock_info_yaml = $item->{stock_info};
+ my $item_position = $item->position;
+ my $stock_qty = $self->calculate_stock_in_out($item);
+ my $unit = $item->unit;
+ $self->js->text("[data-position=$item_position] .data-stock-qty", "$stock_qty $unit");
+ my $selector = "[data-position=$item_position] .data-stock-info";
+ $self->js->val($selector, $stock_info_yaml);
+ }
+
+ $self->js
+ ->flash("info", t8("Stock transfered"))
+ ->run('kivi.ActionBar.setDisabled', '#save_action',
+ t8('This record has already been delivered.'))
+ ->run('kivi.ActionBar.setDisabled', '#save_and_close',
+ t8('This record has already been delivered.'))
+ ->run('kivi.ActionBar.setDisabled', '#transfer_out_action',
+ t8('The parts for this order have already been transferred'))
+ ->run('kivi.ActionBar.setDisabled', '#transfer_out_default_action',
+ t8('The parts for this order have already been transferred'))
+ ->run('kivi.ActionBar.setDisabled', '#transfer_in_action',
+ t8('The parts for this order have already been transferred'))
+ ->run('kivi.ActionBar.setDisabled', '#transfer_in_default_action',
+ t8('The parts for this order have already been transferred'))
+ ->run('kivi.ActionBar.setDisabled', '#delete_action',
+ t8('The parts for this order have already been transferred'))
+ ->run('kivi.ActionBar.setEnabled', '#undo_transfer_action',
+ t8('The parts for this order have already been transferred'))
+ ->html('#data-status-line', delivery_order_status_line($self->order))
+ ->render;
+}
+
+sub action_transfer_stock_default {
+ my ($self) = @_;
+ my $delivery_order = $self->order;
+ my @items = @{$delivery_order->items_sorted};
+
+ # get default bin if set in config
+ my ($default_warehouse_id, $default_bin_id);
+ 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 @transfer_requests = ();
+ my %parts_qty = ();
+ my %units_by_name = map { $_->name => $_ } @{ SL::DB::Manager::Unit->get_all };
+ foreach my $item (@items) {
+ my $part = $item->part;
+ my $base_unit_factor = $units_by_name{$part->unit}->factor || 1;
+ my $item_unit_factor = $units_by_name{$item->unit}->factor || 1;
+ my $qty = $item->qty * $item_unit_factor / $base_unit_factor;
+ return $self->js->flash('error', t8('Cannot transfer negative entries.'))->render() if $qty < 0;
+ $qty = 0 if (!$::instance_conf->get_transfer_default_services && $part->is_service);
+
+ $parts_qty{$part->id} += $qty if $qty;
+ push @transfer_requests, {
+ 'warehouse_id' => $part->warehouse_id || $default_warehouse_id,
+ 'bin_id' => $part->bin_id || $default_bin_id,
+ 'unit' => $item->unit,
+ 'qty' => $qty,
+ # added in check transfer_request out direction if possible
+ 'chargenumber' => undef, # $item->serialnumber, # Is not used in delivery order
+ 'bestbefore' => undef, # $item->bestbefore, # Is not used in delivery order
+ }
+ }
+
+ # check transfer_requests are correctly
+ my %parts_errors = (); # missing_bin, missing_qty, multiple_options
+ my $grouped_qty_query = qq|
+ SELECT SUM(qty) as qty, chargenumber, bestbefore
+ FROM inventory
+ WHERE parts_id = ? AND bin_id = ?
+ GROUP BY chargenumber, bestbefore
+ |;
+ my $dbh = $self->order->dbh;
+ my $in_out_direction = $delivery_order->type_data->properties('transfer');
+ for my $idx (0 .. scalar @transfer_requests - 1) {
+ my $transfer_request = $transfer_requests[$idx];
+ next unless $transfer_request->{qty}; # empty request
+ my $item = $items[$idx];
+ my $part_id = $item->parts_id;
+ my $bin_id = $transfer_request->{bin_id};
+ $parts_errors{$part_id}{missing_bin} = 1 unless $bin_id;
+ next unless $bin_id;
+ if ($in_out_direction eq 'out') {
+ my @grouped_qty = selectall_hashref_query(
+ $::form, $dbh, $grouped_qty_query, $part_id, $bin_id);
+
+ if (1 < scalar grep {$_->{qty} != 0} @grouped_qty) {
+ $parts_errors{$part_id}{multiple_options} = 1;
+ }
+ my $max_qty = sum0(map {$_->{qty}} @grouped_qty);
+ if ($max_qty < $parts_qty{$part_id}) {
+ $parts_errors{$part_id}{missing_qty} = $parts_qty{$part_id} - $max_qty;
+ $parts_errors{$part_id}{bin_id} = $bin_id;
+ }
+
+ next if $parts_errors{$part_id};
+ # find correct chargenumber and bestbefore
+ my $stock_info = first {$_->{qty} >= $transfer_request->{qty}} @grouped_qty;
+ $transfer_request->{chargenumber} = $stock_info->{chargenumber};
+ $transfer_request->{bestbefore} = $stock_info->{bestbefore};
+ }
+ }
+
+ # 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) {
+ foreach my $part_id (keys %parts_errors) {
+ # entsprechende defaults holen
+ # falls chargenumber, bestbefore oder anzahl nicht stimmt, auf automatischen
+ # lagerplatz wegbuchen!
+ for my $idx (0 .. scalar @transfer_requests - 1) {
+ my $transfer_request = $transfer_requests[$idx];
+ next unless $transfer_request->{qty}; # empty request
+
+ if ($items[$idx]->parts_id eq $part_id){
+ $transfer_request->{bin_id} = $default_bin_id_ignore_onhand;
+ $transfer_request->{warehouse_id} = $default_warehouse_id_ignore_onhand;
+ }
+ }
+ delete %parts_errors{$part_id};
+ }
+ }
+
+ # render errors
+ if (scalar keys %parts_errors) {
+ my @multiple_options = ();
+ foreach my $part_id (keys %parts_errors) {
+ my $part = SL::DB::Part->new(id => $part_id)->load();
+ if ($parts_errors{$part_id}{missing_bin}){
+ $self->js->error(t8('No standard bin set for #1.', $part->displayable_name));
+ }
+ if ($parts_errors{$part_id}{missing_qty}) {
+ my $bin = SL::DB::Manager::Bin->find_by(
+ id => $parts_errors{$part_id}{bin_id}
+ );
+ $self->js->error(
+ t8('There are #1 of "#2" missing from the bin #3 for transfer.',
+ $parts_errors{$part_id}{missing_qty}, $part->displayable_name, $bin->full_description));
+ }
+ if ($parts_errors{$part_id}{multiple_options}){
+ push @multiple_options, $part;
+ }
+ }
+ if (scalar @multiple_options) {
+ $self->js->error(t8(
+ "There are parts with multiple chargenumbers or bestbefore dates set. This can't be decided automatically. Pleas transfer this delivery order manually. Can't decided for #1.",
+ join ", ", map {$_->displayable_name} @multiple_options)
+ );
+ }
+ return $self->js->render();
+ }
+
+ # assign each delivery_order_item it's stock
+ for my $idx (0 .. scalar @transfer_requests - 1) {
+ my %transfer_request = %{$transfer_requests[$idx]};
+ next unless $transfer_request{qty}; # empty request
+
+ my $item = $items[$idx];
+ my @stocks = (SL::DB::DeliveryOrderItemsStock->new(%transfer_request));
+ $item->delivery_order_stock_entries(@stocks);
+ }
+
+ my $default_transfer = 1;
+ $self->action_transfer_stock($default_transfer);
+}
+
+sub action_undo_transfers {
+ my ( $self ) = @_;
+
+ SL::DB->client->with_transaction(sub {
+ foreach my $item (@{$self->order->orderitems}) {
+ foreach my $inv_item (@{ $item->delivery_order_stock_entries}) {
+ $inv_item->inventory->delete;
+ $inv_item->delete;
+ }
+ }
+ $self->order->update_attributes(delivered => 0);
+ $self->order->update_attributes(closed => 0);
+ });
+
+ flash_later('info', t8("Transfer undone"));
+ my @redirect_params = (
+ action => 'edit',
+ type => $self->type,
+ id => $self->order->id,
+ );
+
+ $self->redirect_to(@redirect_params);
+}
+