+ $form->{name} = $form->{customer};
+ $form->{name} =~ s/--\Q$form->{customer_id}\E//;
+
+ # add shipto
+ if (!$form->{shipto_id}) {
+ $form->add_shipto($dbh, $form->{id}, "AR");
+ }
+
+ # save printed, emailed and queued
+ $form->save_status($dbh);
+
+ Common::webdav_folder($form);
+
+ if ($form->{convert_from_ar_ids}) {
+ RecordLinks->create_links('dbh' => $dbh,
+ 'mode' => 'ids',
+ 'from_table' => 'ar',
+ 'from_ids' => $form->{convert_from_ar_ids},
+ 'to_table' => 'ar',
+ 'to_id' => $form->{id},
+ );
+ delete $form->{convert_from_ar_ids};
+ }
+
+ # Link this record to the records it was created from.
+ if ($form->{convert_from_oe_ids}) {
+ RecordLinks->create_links('dbh' => $dbh,
+ 'mode' => 'ids',
+ 'from_table' => 'oe',
+ 'from_ids' => $form->{convert_from_oe_ids},
+ 'to_table' => 'ar',
+ 'to_id' => $form->{id},
+ );
+ delete $form->{convert_from_oe_ids};
+ }
+
+ my @convert_from_do_ids = map { $_ * 1 } grep { $_ } split m/\s+/, $form->{convert_from_do_ids};
+
+ if (scalar @convert_from_do_ids) {
+ DO->close_orders('dbh' => $dbh,
+ 'ids' => \@convert_from_do_ids);
+
+ RecordLinks->create_links('dbh' => $dbh,
+ 'mode' => 'ids',
+ 'from_table' => 'delivery_orders',
+ 'from_ids' => \@convert_from_do_ids,
+ 'to_table' => 'ar',
+ 'to_id' => $form->{id},
+ );
+ }
+ delete $form->{convert_from_do_ids};
+
+ ARAP->close_orders_if_billed('dbh' => $dbh,
+ 'arap_id' => $form->{id},
+ 'table' => 'ar',);
+
+ # search for orphaned invoice items
+ $query = sprintf 'SELECT id FROM invoice WHERE trans_id = ? AND NOT id IN (%s)', join ', ', ("?") x scalar @processed_invoice_ids;
+ @values = (conv_i($form->{id}), map { conv_i($_) } @processed_invoice_ids);
+ my @orphaned_ids = map { $_->{id} } selectall_hashref_query($form, $dbh, $query, @values);
+ if (scalar @orphaned_ids) {
+ # clean up invoice items
+ $query = sprintf 'DELETE FROM invoice WHERE id IN (%s)', join ', ', ("?") x scalar @orphaned_ids;
+ do_query($form, $dbh, $query, @orphaned_ids);
+ }
+
+ # safety check datev export
+ if ($::instance_conf->get_datev_check_on_sales_invoice) {
+ my $transdate = $::form->{invdate} ? DateTime->from_lxoffice($::form->{invdate}) : undef;
+ $transdate ||= DateTime->today;
+
+ my $datev = SL::DATEV->new(
+ exporttype => DATEV_ET_BUCHUNGEN,
+ format => DATEV_FORMAT_KNE,
+ dbh => $dbh,
+ trans_id => $form->{id},
+ );
+
+ $datev->export;
+
+ if ($datev->errors) {
+ die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
+ }
+ }
+
+ return 1;
+}
+
+sub transfer_out {
+ $::lxdebug->enter_sub;
+
+ my ($self, $form, $dbh) = @_;
+
+ my (@errors, @transfers);
+
+ # do nothing, if transfer default is not requeseted at all
+ if (!$::instance_conf->get_transfer_default) {
+ $::lxdebug->leave_sub;
+ return \@errors;
+ }
+
+ require SL::WH;
+
+ foreach my $i (1 .. $form->{rowcount}) {
+ next if !$form->{"id_$i"};
+ my ($err, $wh_id, $bin_id) = _determine_wh_and_bin($dbh, $::instance_conf,
+ $form->{"id_$i"},
+ $form->{"qty_$i"},
+ $form->{"unit_$i"});
+ if (!@{ $err } && $wh_id && $bin_id) {
+ push @transfers, {
+ 'parts_id' => $form->{"id_$i"},
+ 'qty' => $form->{"qty_$i"},
+ 'unit' => $form->{"unit_$i"},
+ 'transfer_type' => 'shipped',
+ 'src_warehouse_id' => $wh_id,
+ 'src_bin_id' => $bin_id,
+ 'project_id' => $form->{"project_id_$i"},
+ 'invoice_id' => $form->{"invoice_id_$i"},
+ 'comment' => $::locale->text("Default transfer invoice"),
+ };
+ }
+
+ push @errors, @{ $err };
+ }
+
+ if (!@errors) {
+ WH->transfer(@transfers);
+ }
+
+ $::lxdebug->leave_sub;
+ return \@errors;
+}
+
+sub _determine_wh_and_bin {
+ $::lxdebug->enter_sub(2);
+
+ my ($dbh, $conf, $part_id, $qty, $unit) = @_;
+ my @errors;
+
+ my $part = SL::DB::Part->new(id => $part_id)->load;
+
+ # ignore service if they are not configured to be transfered
+ if ($part->is_service && !$conf->get_transfer_default_services) {
+ $::lxdebug->leave_sub(2);
+ return (\@errors);
+ }
+
+ # test negative qty
+ if ($qty < 0) {
+ push @errors, $::locale->text("Cannot transfer negative quantities.");
+ return (\@errors);
+ }
+
+ # get/test default bin
+ my ($default_wh_id, $default_bin_id);
+ if ($conf->get_transfer_default_use_master_default_bin) {
+ $default_wh_id = $conf->get_warehouse_id if $conf->get_warehouse_id;
+ $default_bin_id = $conf->get_bin_id if $conf->get_bin_id;
+ }
+ my $wh_id = $part->warehouse_id || $default_wh_id;
+ my $bin_id = $part->bin_id || $default_bin_id;
+
+ # check qty and bin
+ if ($bin_id) {
+ my ($max_qty, $error) = WH->get_max_qty_parts_bin(dbh => $dbh,
+ parts_id => $part->id,
+ bin_id => $bin_id);
+ if ($error == 1) {
+ push @errors, $::locale->text('Part "#1" has chargenumber or best before date set. So it cannot be transfered automatically.',
+ $part->description);
+ }
+ my $form_unit_obj = SL::DB::Unit->new(name => $unit)->load;
+ my $part_unit_qty = $form_unit_obj->convert_to($qty, $part->unit_obj);
+ my $diff_qty = $max_qty - $part_unit_qty;
+ if (!@errors && $diff_qty < 0) {
+ push @errors, $::locale->text('For part "#1" there are missing #2 #3 in the default warehouse/bin "#4/#5".',
+ $part->description,
+ $::form->format_amount(\%::myconfig, -1*$diff_qty),
+ $part->unit_obj->name,
+ SL::DB::Warehouse->new(id => $wh_id)->load->description,
+ SL::DB::Bin->new( id => $bin_id)->load->description);
+ }
+ } else {
+ push @errors, $::locale->text('For part "#1" there is no default warehouse and bin defined.',
+ $part->description);
+ }
+
+ # transfer to special "ignore onhand" bin if requested and default bin does not work
+ if (@errors && $conf->get_transfer_default_ignore_onhand && $conf->get_bin_id_ignore_onhand) {
+ $wh_id = $conf->get_warehouse_id_ignore_onhand;
+ $bin_id = $conf->get_bin_id_ignore_onhand;
+ if ($wh_id && $bin_id) {
+ @errors = ();
+ } else {
+ push @errors, $::locale->text('For part "#1" there is no default warehouse and bin for ignoring onhand defined.',
+ $part->description);
+ }
+ }
+
+ $::lxdebug->leave_sub(2);
+ return (\@errors, $wh_id, $bin_id);
+}
+
+sub _delete_transfers {
+ $::lxdebug->enter_sub;
+
+ my ($dbh, $form, $id) = @_;
+
+ my $query = qq|DELETE FROM inventory WHERE invoice_id
+ IN (SELECT id FROM invoice WHERE trans_id = ?)|;
+
+ do_query($form, $dbh, $query, $id);
+
+ $::lxdebug->leave_sub;
+}
+
+sub _delete_payments {
+ $main::lxdebug->enter_sub();
+
+ my ($self, $form, $dbh) = @_;
+
+ my @delete_acc_trans_ids;
+
+ # Delete old payment entries from acc_trans.
+ my $query =
+ qq|SELECT acc_trans_id
+ FROM acc_trans
+ WHERE (trans_id = ?) AND fx_transaction
+
+ UNION
+
+ SELECT at.acc_trans_id
+ FROM acc_trans at
+ LEFT JOIN chart c ON (at.chart_id = c.id)
+ WHERE (trans_id = ?) AND (c.link LIKE '%AR_paid%')|;
+ push @delete_acc_trans_ids, selectall_array_query($form, $dbh, $query, conv_i($form->{id}), conv_i($form->{id}));
+
+ $query =
+ qq|SELECT at.acc_trans_id
+ FROM acc_trans at
+ LEFT JOIN chart c ON (at.chart_id = c.id)
+ WHERE (trans_id = ?)
+ AND ((c.link = 'AR') OR (c.link LIKE '%:AR') OR (c.link LIKE 'AR:%'))
+ ORDER BY at.acc_trans_id
+ OFFSET 1|;
+ push @delete_acc_trans_ids, selectall_array_query($form, $dbh, $query, conv_i($form->{id}));
+
+ if (@delete_acc_trans_ids) {
+ $query = qq|DELETE FROM acc_trans WHERE acc_trans_id IN (| . join(", ", @delete_acc_trans_ids) . qq|)|;
+ do_query($form, $dbh, $query);
+ }
+
+ $main::lxdebug->leave_sub();
+}
+
+sub post_payment {
+ my ($self, $myconfig, $form, $locale) = @_;
+ $main::lxdebug->enter_sub();
+
+ my $rc = SL::DB->client->with_transaction(\&_post_payment, $self, $myconfig, $form, $locale);
+
+ $::lxdebug->leave_sub;
+ return $rc;
+}
+
+sub _post_payment {
+ my ($self, $myconfig, $form, $locale) = @_;
+
+ my $dbh = SL::DB->client->dbh;
+
+ my (%payments, $old_form, $row, $item, $query, %keep_vars);
+
+ $old_form = save_form();
+
+ # Delete all entries in acc_trans from prior payments.
+ if (SL::DB::Default->get->payments_changeable != 0) {
+ $self->_delete_payments($form, $dbh);
+ }
+
+ # Save the new payments the user made before cleaning up $form.
+ map { $payments{$_} = $form->{$_} } grep m/^datepaid_\d+$|^gldate_\d+$|^acc_trans_id_\d+$|^memo_\d+$|^source_\d+$|^exchangerate_\d+$|^paid_\d+$|^AR_paid_\d+$|^paidaccounts$/, keys %{ $form };
+
+ # Clean up $form so that old content won't tamper the results.
+ %keep_vars = map { $_, 1 } qw(login password id);
+ map { delete $form->{$_} unless $keep_vars{$_} } keys %{ $form };
+
+ # Retrieve the invoice from the database.
+ $self->retrieve_invoice($myconfig, $form);
+
+ # Set up the content of $form in the way that IS::post_invoice() expects.
+ $form->{exchangerate} = $form->format_amount($myconfig, $form->{exchangerate});
+
+ for $row (1 .. scalar @{ $form->{invoice_details} }) {
+ $item = $form->{invoice_details}->[$row - 1];
+
+ map { $item->{$_} = $form->format_amount($myconfig, $item->{$_}) } qw(qty sellprice discount);
+
+ map { $form->{"${_}_${row}"} = $item->{$_} } keys %{ $item };
+ }
+
+ $form->{rowcount} = scalar @{ $form->{invoice_details} };
+
+ delete @{$form}{qw(invoice_details paidaccounts storno paid)};
+
+ # Restore the payment options from the user input.
+ map { $form->{$_} = $payments{$_} } keys %payments;
+
+ # Get the AR accno (which is normally done by Form::create_links()).
+ $query =
+ qq|SELECT c.accno
+ FROM acc_trans at
+ LEFT JOIN chart c ON (at.chart_id = c.id)
+ WHERE (trans_id = ?)
+ AND ((c.link = 'AR') OR (c.link LIKE '%:AR') OR (c.link LIKE 'AR:%'))
+ ORDER BY at.acc_trans_id
+ LIMIT 1|;
+
+ ($form->{AR}) = selectfirst_array_query($form, $dbh, $query, conv_i($form->{id}));
+
+ # Post the new payments.
+ $self->post_invoice($myconfig, $form, $dbh, 1);
+
+ restore_form($old_form);
+
+ return 1;
+}
+
+sub process_assembly {
+ $main::lxdebug->enter_sub();
+
+ my ($dbh, $myconfig, $form, $position, $id, $totalqty) = @_;
+
+ my $query =
+ qq|SELECT a.parts_id, a.qty, p.part_type, p.partnumber, p.description, p.unit
+ FROM assembly a
+ JOIN parts p ON (a.parts_id = p.id)
+ WHERE (a.id = ?)|;
+ my $sth = prepare_execute_query($form, $dbh, $query, conv_i($id));
+
+ while (my $ref = $sth->fetchrow_hashref('NAME_lc')) {
+
+ my $allocated = 0;
+
+ $ref->{inventory_accno_id} *= 1;
+ $ref->{expense_accno_id} *= 1;