use Rose::Object::MakeMethods::Generic
(
scalar => [ qw(action_name) ],
- 'scalar --get_set_init' => [ qw(js) ],
+ 'scalar --get_set_init' => [ qw(js p) ],
);
#
return SL::Presenter->get;
}
+sub init_p {
+ return SL::Presenter->get;
+}
+
sub controller_name {
my $class = ref($_[0]) || $_[0];
$class =~ s/^SL::Controller:://;
use SL::Controller::CsvImport::Helper::Consistency;
use SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData qw(:types);
use SL::DB::DeliveryOrderItem;
use SL::DB::DeliveryOrderItemsStock;
use SL::DB::Part;
{ name => 'globalprojectnumber', description => $::locale->text('Document Project (number)') },
{ name => 'globalproject_id', description => $::locale->text('Document Project (database ID)') },
{ name => 'intnotes', description => $::locale->text('Internal Notes') },
- { name => 'is_sales', description => $::locale->text('Is sales') },
{ name => 'language', description => $::locale->text('Language (name)') },
{ name => 'language_id', description => $::locale->text('Language (database ID)') },
{ name => 'notes', description => $::locale->text('Notes') },
+ { name => 'order_type', description => $::locale->text('Delivery Order Type') },
{ name => 'ordnumber', description => $::locale->text('Order Number') },
{ name => 'payment', description => $::locale->text('Payment terms (name)') },
{ name => 'payment_id', description => $::locale->text('Payment terms (database ID)') },
push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor missing');
}
- $self->handle_is_sales($entry);
+ $self->handle_type($entry);
$self->check_contact($entry);
$self->check_language($entry);
$self->check_payment($entry);
}
}
-sub handle_is_sales {
+sub handle_type {
my ($self, $entry) = @_;
- if (!exists $entry->{raw_data}->{is_sales}) {
- $entry->{object}->is_sales(!!$entry->{object}->customer_id);
+ if (!exists $entry->{raw_data}->{order_type}) {
+ # if no type is present - set to sales delivery order or purchase delivery
+ # order depending on is_sales or customer/vendor
+
+ $entry->{object}->order_type(
+ $entry->{object}->customer_id ? SALES_DELIVERY_ORDER_TYPE :
+ $entry->{object}->vendor_id ? PURCHASE_DELIVERY_ORDER_TYPE :
+ $entry->{raw_data}->{is_sales} ? SALES_DELIVERY_ORDER_TYPE :
+ PURCHASE_DELIVERY_ORDER_TYPE
+ );
}
}
--- /dev/null
+package SL::Controller::DeliveryOrder;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use SL::Helper::Flash qw(flash_later);
+use SL::Helper::Number qw(_format_number _parse_number);
+use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
+use SL::Presenter::DeliveryOrder qw(delivery_order_status_line);
+use SL::Locale::String qw(t8);
+use SL::SessionFile::Random;
+use SL::PriceSource;
+use SL::Webdav;
+use SL::File;
+use SL::MIME;
+use SL::Util qw(trim);
+use SL::YAML;
+use SL::DB::History;
+use SL::DB::Order;
+use SL::DB::Default;
+use SL::DB::Unit;
+use SL::DB::Order;
+use SL::DB::Part;
+use SL::DB::PartClassification;
+use SL::DB::PartsGroup;
+use SL::DB::Printer;
+use SL::DB::Language;
+use SL::DB::RecordLink;
+use SL::DB::Shipto;
+use SL::DB::Translation;
+use SL::DB::TransferType;
+
+use SL::Helper::CreatePDF qw(:all);
+use SL::Helper::PrintOptions;
+use SL::Helper::ShippedQty;
+use SL::Helper::UserPreferences::PositionsScrollbar;
+use SL::Helper::UserPreferences::UpdatePositions;
+
+use SL::Controller::Helper::GetModels;
+use SL::Controller::DeliveryOrder::TypeData qw(:types);
+
+use List::Util qw(first sum0);
+use List::UtilsBy qw(sort_by uniq_by);
+use List::MoreUtils qw(any none pairwise first_index);
+use English qw(-no_match_vars);
+use File::Spec;
+use Cwd;
+use Sort::Naturally;
+
+use Rose::Object::MakeMethods::Generic
+(
+ scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
+ 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids type_data) ],
+);
+
+
+# safety
+__PACKAGE__->run_before('check_auth',
+ except => [ qw(update_stock_information) ]);
+
+__PACKAGE__->run_before('get_unalterable_data',
+ only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
+ print send_email) ]);
+
+#
+# actions
+#
+
+# add a new order
+sub action_add {
+ my ($self) = @_;
+
+ $self->order->transdate(DateTime->now_local());
+ $self->type_data->set_reqdate_by_type;
+
+
+ $self->pre_render();
+ $self->render(
+ 'delivery_order/form',
+ title => $self->get_title_for('add'),
+ %{$self->{template_args}}
+ );
+}
+
+sub action_add_from_order {
+ my ($self) = @_;
+ # this interfers with init_order
+ $self->{converted_from_oe_id} = delete $::form->{id};
+
+ $self->type_data->validate($::form->{type});
+
+ my $order = SL::DB::Order->new(id => $self->{converted_from_oe_id})->load;
+
+ $self->order(SL::DB::DeliveryOrder->new_from($order, type => $::form->{type}));
+
+ $self->action_add;
+}
+
+# edit an existing order
+sub action_edit {
+ my ($self) = @_;
+
+ if ($::form->{id}) {
+ $self->load_order;
+
+ } else {
+ # this is to edit an order from an unsaved order object
+
+ # set item ids to new fake id, to identify them as new items
+ foreach my $item (@{$self->order->items_sorted}) {
+ $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+ }
+ # trigger rendering values for second row as hidden, because they
+ # are loaded only on demand. So we need to keep the values from
+ # the source.
+ $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+ }
+
+ $self->pre_render();
+ $self->render(
+ 'delivery_order/form',
+ title => $self->get_title_for('edit'),
+ %{$self->{template_args}}
+ );
+}
+
+# edit a collective order (consisting of one or more existing orders)
+sub action_edit_collective {
+ my ($self) = @_;
+
+ # collect order ids
+ my @multi_ids = map {
+ $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
+ } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
+
+ # fall back to add if no ids are given
+ if (scalar @multi_ids == 0) {
+ $self->action_add();
+ return;
+ }
+
+ # fall back to save as new if only one id is given
+ if (scalar @multi_ids == 1) {
+ $self->order(SL::DB::DeliveryOrder->new(id => $multi_ids[0])->load);
+ $self->action_save_as_new();
+ return;
+ }
+
+ # make new order from given orders
+ my @multi_orders = map { SL::DB::DeliveryOrder->new(id => $_)->load } @multi_ids;
+ $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
+ $self->order(SL::DB::DeliveryOrder->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
+
+ $self->action_edit();
+}
+
+# delete the order
+sub action_delete {
+ my ($self) = @_;
+
+ my $errors = $self->delete();
+
+ if (scalar @{ $errors }) {
+ $self->js->flash('error', $_) foreach @{ $errors };
+ return $self->js->render();
+ }
+
+ flash_later('info', $self->type_data->text("delete"));
+
+ my @redirect_params = (
+ action => 'add',
+ type => $self->type,
+ );
+
+ $self->redirect_to(@redirect_params);
+}
+
+# save the order
+sub action_save {
+ my ($self) = @_;
+
+ my $errors = $self->save();
+
+ if (scalar @{ $errors }) {
+ $self->js->flash('error', $_) foreach @{ $errors };
+ return $self->js->render();
+ }
+
+ flash_later('info', $self->type_data->text("saved"));
+
+ my @redirect_params = (
+ action => 'edit',
+ type => $self->type,
+ id => $self->order->id,
+ );
+
+ $self->redirect_to(@redirect_params);
+}
+
+# save the order as new document an open it for edit
+sub action_save_as_new {
+ my ($self) = @_;
+
+ my $order = $self->order;
+
+ if (!$order->id) {
+ $self->js->flash('error', t8('This object has not been saved yet.'));
+ return $self->js->render();
+ }
+
+ # load order from db to check if values changed
+ my $saved_order = SL::DB::DeliveryOrder->new(id => $order->id)->load;
+
+ my %new_attrs;
+ # Lets assign a new number if the user hasn't changed the previous one.
+ # If it has been changed manually then use it as-is.
+ $new_attrs{number} = (trim($order->number) eq $saved_order->number)
+ ? ''
+ : trim($order->number);
+
+ # Clear transdate unless changed
+ $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
+ ? DateTime->today_local
+ : $order->transdate;
+
+ # Set new reqdate unless changed if it is enabled in client config
+ $new_attrs{reqdate} = $self->type_data->get_reqdate_by_type($order->reqdate, $saved_order->reqdate);
+
+ # Update employee
+ $new_attrs{employee} = SL::DB::Manager::Employee->current;
+
+ # Create new record from current one
+ $self->order(SL::DB::DeliveryOrder->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
+
+ # no linked records on save as new
+ delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
+
+ # save
+ $self->action_save();
+}
+
+# print the order
+#
+# This is called if "print" is pressed in the print dialog.
+# If PDF creation was requested and succeeded, the pdf is offered for download
+# via send_file (which uses ajax in this case).
+sub action_print {
+ my ($self) = @_;
+
+ my $errors = $self->save();
+
+ if (scalar @{ $errors }) {
+ $self->js->flash('error', $_) foreach @{ $errors };
+ return $self->js->render();
+ }
+
+ $self->js_reset_order_and_item_ids_after_save;
+
+ my $format = $::form->{print_options}->{format};
+ my $media = $::form->{print_options}->{media};
+ my $formname = $::form->{print_options}->{formname};
+ my $copies = $::form->{print_options}->{copies};
+ my $groupitems = $::form->{print_options}->{groupitems};
+ my $printer_id = $::form->{print_options}->{printer_id};
+
+ # only pdf and opendocument by now
+ if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
+ return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
+ }
+
+ # only screen or printer by now
+ if (none { $media eq $_ } qw(screen printer)) {
+ return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
+ }
+
+ # create a form for generate_attachment_filename
+ my $form = Form->new;
+ $form->{$self->nr_key()} = $self->order->number;
+ $form->{type} = $self->type;
+ $form->{format} = $format;
+ $form->{formname} = $formname;
+ $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
+ my $pdf_filename = $form->generate_attachment_filename();
+
+ my $pdf;
+ my @errors = generate_pdf($self->order, \$pdf, { format => $format,
+ formname => $formname,
+ language => $self->order->language,
+ printer_id => $printer_id,
+ groupitems => $groupitems });
+ if (scalar @errors) {
+ return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
+ }
+
+ if ($media eq 'screen') {
+ # screen/download
+ $self->js->flash('info', t8('The PDF has been created'));
+ $self->send_file(
+ \$pdf,
+ type => SL::MIME->mime_type_from_ext($pdf_filename),
+ name => $pdf_filename,
+ js_no_render => 1,
+ );
+
+ } elsif ($media eq 'printer') {
+ # printer
+ my $printer_id = $::form->{print_options}->{printer_id};
+ SL::DB::Printer->new(id => $printer_id)->load->print_document(
+ copies => $copies,
+ content => $pdf,
+ );
+
+ $self->js->flash('info', t8('The PDF has been printed'));
+ }
+
+ my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $pdf_filename);
+ if (scalar @warnings) {
+ $self->js->flash('warning', $_) for @warnings;
+ }
+
+ $self->save_history('PRINTED');
+
+ $self->js
+ ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
+ ->render;
+}
+sub action_preview_pdf {
+ my ($self) = @_;
+
+ my $errors = $self->save();
+ if (scalar @{ $errors }) {
+ $self->js->flash('error', $_) foreach @{ $errors };
+ return $self->js->render();
+ }
+
+ $self->js_reset_order_and_item_ids_after_save;
+
+ my $format = 'pdf';
+ my $media = 'screen';
+ my $formname = $self->type;
+
+ # only pdf
+ # create a form for generate_attachment_filename
+ my $form = Form->new;
+ $form->{$self->nr_key()} = $self->order->number;
+ $form->{type} = $self->type;
+ $form->{format} = $format;
+ $form->{formname} = $formname;
+ $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
+ my $pdf_filename = $form->generate_attachment_filename();
+
+ my $pdf;
+ my @errors = generate_pdf($self->order, \$pdf, { format => $format,
+ formname => $formname,
+ language => $self->order->language,
+ });
+ if (scalar @errors) {
+ return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
+ }
+ $self->save_history('PREVIEWED');
+ $self->js->flash('info', t8('The PDF has been previewed'));
+ # screen/download
+ $self->send_file(
+ \$pdf,
+ type => SL::MIME->mime_type_from_ext($pdf_filename),
+ name => $pdf_filename,
+ js_no_render => 0,
+ );
+}
+
+# open the email dialog
+sub action_save_and_show_email_dialog {
+ my ($self) = @_;
+
+ my $errors = $self->save();
+
+ if (scalar @{ $errors }) {
+ $self->js->flash('error', $_) foreach @{ $errors };
+ return $self->js->render();
+ }
+
+ my $cv_method = $self->cv;
+
+ if (!$self->order->$cv_method) {
+ return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
+ ->render($self);
+ }
+
+ my $email_form;
+ $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
+ $email_form->{to} ||= $self->order->$cv_method->email;
+ $email_form->{cc} = $self->order->$cv_method->cc;
+ $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
+ # Todo: get addresses from shipto, if any
+
+ my $form = Form->new;
+ $form->{$self->nr_key()} = $self->order->number;
+ $form->{cusordnumber} = $self->order->cusordnumber;
+ $form->{formname} = $self->type;
+ $form->{type} = $self->type;
+ $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
+ $form->{language_id} = $self->order->language->id if $self->order->language;
+ $form->{format} = 'pdf';
+ $form->{cp_id} = $self->order->contact->cp_id if $self->order->contact;
+
+ $email_form->{subject} = $form->generate_email_subject();
+ $email_form->{attachment_filename} = $form->generate_attachment_filename();
+ $email_form->{message} = $form->generate_email_body();
+ $email_form->{js_send_function} = 'kivi.DeliveryOrder.send_email()';
+
+ my %files = $self->get_files_for_email_dialog();
+ $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
+ my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
+ email_form => $email_form,
+ show_bcc => $::auth->assert('email_bcc', 'may fail'),
+ FILES => \%files,
+ is_customer => $self->type_data->is_customer,
+ ALL_EMPLOYEES => $self->{all_employees},
+ );
+
+ $self->js
+ ->run('kivi.DeliveryOrder.show_email_dialog', $dialog_html)
+ ->reinit_widgets
+ ->render($self);
+}
+
+# send email
+#
+# Todo: handling error messages: flash is not displayed in dialog, but in the main form
+sub action_send_email {
+ my ($self) = @_;
+
+ my $errors = $self->save();
+
+ if (scalar @{ $errors }) {
+ $self->js->run('kivi.DeliveryOrder.close_email_dialog');
+ $self->js->flash('error', $_) foreach @{ $errors };
+ return $self->js->render();
+ }
+
+ $self->js_reset_order_and_item_ids_after_save;
+
+ my $email_form = delete $::form->{email_form};
+ my %field_names = (to => 'email');
+
+ $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
+
+ # for Form::cleanup which may be called in Form::send_email
+ $::form->{cwd} = getcwd();
+ $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
+
+ $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
+ $::form->{media} = 'email';
+
+ if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
+ my $pdf;
+ my @errors = generate_pdf($self->order, \$pdf, {media => $::form->{media},
+ format => $::form->{print_options}->{format},
+ formname => $::form->{print_options}->{formname},
+ language => $self->order->language,
+ printer_id => $::form->{print_options}->{printer_id},
+ groupitems => $::form->{print_options}->{groupitems}});
+ if (scalar @errors) {
+ return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
+ }
+
+ my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $::form->{attachment_filename});
+ if (scalar @warnings) {
+ flash_later('warning', $_) for @warnings;
+ }
+
+ my $sfile = SL::SessionFile::Random->new(mode => "w");
+ $sfile->fh->print($pdf);
+ $sfile->fh->close;
+
+ $::form->{tmpfile} = $sfile->file_name;
+ $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
+ }
+
+ $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
+ $::form->send_email(\%::myconfig, 'pdf');
+
+ # internal notes
+ my $intnotes = $self->order->intnotes;
+ $intnotes .= "\n\n" if $self->order->intnotes;
+ $intnotes .= t8('[email]') . "\n";
+ $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
+ $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
+ $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
+ $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
+ $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
+ $intnotes .= t8('Message') . ": " . $::form->{message};
+
+ $self->order->update_attributes(intnotes => $intnotes);
+
+ $self->save_history('MAILED');
+
+ flash_later('info', t8('The email has been sent.'));
+
+ my @redirect_params = (
+ action => 'edit',
+ type => $self->type,
+ id => $self->order->id,
+ );
+
+ $self->redirect_to(@redirect_params);
+}
+
+# save the order and redirect to the frontend subroutine for a new
+# delivery order
+sub action_save_and_delivery_order {
+ my ($self) = @_;
+
+ $self->save_and_redirect_to(
+ controller => 'oe.pl',
+ action => 'oe_delivery_order_from_order',
+ );
+}
+
+# save the order and redirect to the frontend subroutine for a new
+# invoice
+sub action_save_and_invoice {
+ my ($self) = @_;
+
+ $self->save_and_redirect_to(
+ controller => 'oe.pl',
+ action => 'oe_invoice_from_order',
+ );
+}
+
+# workflow from sales order to sales quotation
+sub action_sales_quotation {
+ $_[0]->workflow_sales_or_request_for_quotation();
+}
+
+# workflow from sales order to sales quotation
+sub action_request_for_quotation {
+ $_[0]->workflow_sales_or_request_for_quotation();
+}
+
+# workflow from sales quotation to sales order
+sub action_sales_order {
+ $_[0]->workflow_sales_or_purchase_order();
+}
+
+# workflow from rfq to purchase order
+sub action_purchase_order {
+ $_[0]->workflow_sales_or_purchase_order();
+}
+
+# workflow from purchase order to ap transaction
+sub action_save_and_ap_transaction {
+ my ($self) = @_;
+
+ $self->save_and_redirect_to(
+ controller => 'ap.pl',
+ action => 'add_from_purchase_order',
+ );
+}
+
+# set form elements in respect to a changed customer or vendor
+#
+# This action is called on an change of the customer/vendor picker.
+sub action_customer_vendor_changed {
+ my ($self) = @_;
+
+ setup_order_from_cv($self->order);
+
+ my $cv_method = $self->cv;
+
+ if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
+ $self->js->show('#cp_row');
+ } else {
+ $self->js->hide('#cp_row');
+ }
+
+ if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
+ $self->js->show('#shipto_selection');
+ } else {
+ $self->js->hide('#shipto_selection');
+ }
+
+ $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
+
+ $self->js
+ ->replaceWith('#order_cp_id', $self->build_contact_select)
+ ->replaceWith('#order_shipto_id', $self->build_shipto_select)
+ ->replaceWith('#shipto_inputs ', $self->build_shipto_inputs)
+ ->replaceWith('#business_info_row', $self->build_business_info_row)
+ ->val( '#order_taxzone_id', $self->order->taxzone_id)
+ ->val( '#order_taxincluded', $self->order->taxincluded)
+ ->val( '#order_currency_id', $self->order->currency_id)
+ ->val( '#order_payment_id', $self->order->payment_id)
+ ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
+ ->val( '#order_intnotes', $self->order->intnotes)
+ ->val( '#order_language_id', $self->order->$cv_method->language_id)
+ ->focus( '#order_' . $self->cv . '_id')
+ ->run('kivi.DeliveryOrder.update_exchangerate');
+
+ $self->js_redisplay_cvpartnumbers;
+ $self->js->render();
+}
+
+# open the dialog for customer/vendor details
+sub action_show_customer_vendor_details_dialog {
+ my ($self) = @_;
+
+ my $is_customer = 'customer' eq $::form->{vc};
+ my $cv;
+ if ($is_customer) {
+ $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
+ } else {
+ $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
+ }
+
+ my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
+ $details{discount_as_percent} = $cv->discount_as_percent;
+ $details{creditlimt} = $cv->creditlimit_as_number;
+ $details{business} = $cv->business->description if $cv->business;
+ $details{language} = $cv->language_obj->description if $cv->language_obj;
+ $details{delivery_terms} = $cv->delivery_term->description if $cv->delivery_term;
+ $details{payment_terms} = $cv->payment->description if $cv->payment;
+ $details{pricegroup} = $cv->pricegroup->pricegroup if $is_customer && $cv->pricegroup;
+
+ foreach my $entry (@{ $cv->shipto }) {
+ push @{ $details{SHIPTO} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
+ }
+ foreach my $entry (@{ $cv->contacts }) {
+ push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
+ }
+
+ $_[0]->render('common/show_vc_details', { layout => 0 },
+ is_customer => $is_customer,
+ %details);
+
+}
+
+# called if a unit in an existing item row is changed
+sub action_unit_changed {
+ my ($self) = @_;
+
+ my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
+ my $item = $self->order->items_sorted->[$idx];
+
+ my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
+ $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
+
+ $self->js
+ ->run('kivi.DeliveryOrder.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
+ $self->js_redisplay_line_values;
+ $self->js->render();
+}
+
+# add an item row for a new item entered in the input row
+sub action_add_item {
+ my ($self) = @_;
+
+ delete $::form->{add_item}->{create_part_type};
+
+ my $form_attr = $::form->{add_item};
+
+ return unless $form_attr->{parts_id};
+
+ my $item = new_item($self->order, $form_attr);
+
+ $self->order->add_items($item);
+
+ $self->get_item_cvpartnumber($item);
+
+ my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+ my $row_as_html = $self->p->render('delivery_order/tabs/_row',
+ ITEM => $item,
+ ID => $item_id,
+ SELF => $self,
+ in_out => $self->type_data->transfer,
+ );
+
+ if ($::form->{insert_before_item_id}) {
+ $self->js
+ ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+ } else {
+ $self->js
+ ->append('#row_table_id', $row_as_html);
+ }
+
+ if ( $item->part->is_assortment ) {
+ $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
+ foreach my $assortment_item ( @{$item->part->assortment_items} ) {
+ my $attr = { parts_id => $assortment_item->parts_id,
+ qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
+ unit => $assortment_item->unit,
+ description => $assortment_item->part->description,
+ };
+ my $item = new_item($self->order, $attr);
+
+ # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
+ $item->discount(1) unless $assortment_item->charge;
+
+ $self->order->add_items( $item );
+ $self->get_item_cvpartnumber($item);
+ my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+ my $row_as_html = $self->p->render('delivery_order/tabs/_row',
+ ITEM => $item,
+ ID => $item_id,
+ SELF => $self,
+ );
+ if ($::form->{insert_before_item_id}) {
+ $self->js
+ ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+ } else {
+ $self->js
+ ->append('#row_table_id', $row_as_html);
+ }
+ };
+ };
+
+ $self->js
+ ->val('.add_item_input', '')
+ ->run('kivi.DeliveryOrder.init_row_handlers')
+ ->run('kivi.DeliveryOrder.renumber_positions')
+ ->focus('#add_item_parts_id_name');
+
+ $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
+
+ $self->js->render();
+}
+
+# add item rows for multiple items at once
+sub action_add_multi_items {
+ my ($self) = @_;
+
+ my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
+ return $self->js->render() unless scalar @form_attr;
+
+ my @items;
+ foreach my $attr (@form_attr) {
+ my $item = new_item($self->order, $attr);
+ push @items, $item;
+ if ( $item->part->is_assortment ) {
+ foreach my $assortment_item ( @{$item->part->assortment_items} ) {
+ my $attr = { parts_id => $assortment_item->parts_id,
+ qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
+ unit => $assortment_item->unit,
+ description => $assortment_item->part->description,
+ };
+ my $item = new_item($self->order, $attr);
+
+ # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
+ $item->discount(1) unless $assortment_item->charge;
+ push @items, $item;
+ }
+ }
+ }
+ $self->order->add_items(@items);
+
+ foreach my $item (@items) {
+ $self->get_item_cvpartnumber($item);
+ my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+ my $row_as_html = $self->p->render('delivery_order/tabs/_row',
+ ITEM => $item,
+ ID => $item_id,
+ SELF => $self,
+ in_out => $self->type_data->transfer,
+ );
+
+ if ($::form->{insert_before_item_id}) {
+ $self->js
+ ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+ } else {
+ $self->js
+ ->append('#row_table_id', $row_as_html);
+ }
+ }
+
+ $self->js
+ ->run('kivi.Part.close_picker_dialogs')
+ ->run('kivi.DeliveryOrder.init_row_handlers')
+ ->run('kivi.DeliveryOrder.renumber_positions')
+ ->focus('#add_item_parts_id_name');
+
+ $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
+
+ $self->js->render();
+}
+
+sub action_update_exchangerate {
+ my ($self) = @_;
+
+ my $data = {
+ is_standard => $self->order->currency_id == $::instance_conf->get_currency_id,
+ currency_name => $self->order->currency->name,
+ };
+
+ $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
+}
+
+# redisplay item rows if they are sorted by an attribute
+sub action_reorder_items {
+ my ($self) = @_;
+
+ my %sort_keys = (
+ partnumber => sub { $_[0]->part->partnumber },
+ description => sub { $_[0]->description },
+ qty => sub { $_[0]->qty },
+ sellprice => sub { $_[0]->sellprice },
+ discount => sub { $_[0]->discount },
+ cvpartnumber => sub { $_[0]->{cvpartnumber} },
+ );
+
+ $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+ my $method = $sort_keys{$::form->{order_by}};
+ my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
+ if ($::form->{sort_dir}) {
+ if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
+ @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
+ } else {
+ @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
+ }
+ } else {
+ if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
+ @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
+ } else {
+ @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
+ }
+ }
+ $self->js
+ ->run('kivi.DeliveryOrder.redisplay_items', \@to_sort)
+ ->render;
+}
+
+# show the popup to choose a price/discount source
+sub action_price_popup {
+ my ($self) = @_;
+
+ my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
+ my $item = $self->order->items_sorted->[$idx];
+
+ $self->render_price_dialog($item);
+}
+
+# save the order in a session variable and redirect to the part controller
+sub action_create_part {
+ my ($self) = @_;
+
+ my $previousform = $::auth->save_form_in_session(non_scalars => 1);
+
+ my $callback = $self->url_for(
+ action => 'return_from_create_part',
+ type => $self->type, # type is needed for check_auth on return
+ previousform => $previousform,
+ );
+
+ flash_later('info', t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.'));
+
+ my @redirect_params = (
+ controller => 'Part',
+ action => 'add',
+ part_type => $::form->{add_item}->{create_part_type},
+ callback => $callback,
+ show_abort => 1,
+ );
+
+ $self->redirect_to(@redirect_params);
+}
+
+sub action_return_from_create_part {
+ my ($self) = @_;
+
+ $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
+
+ $::auth->restore_form_from_session(delete $::form->{previousform});
+
+ # set item ids to new fake id, to identify them as new items
+ foreach my $item (@{$self->order->items_sorted}) {
+ $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+ }
+
+ $self->get_unalterable_data();
+ $self->pre_render();
+
+ # trigger rendering values for second row/longdescription as hidden,
+ # because they are loaded only on demand. So we need to keep the values
+ # from the source.
+ $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+ $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
+
+ $self->render(
+ 'delivery_order/form',
+ title => $self->get_title_for('edit'),
+ %{$self->{template_args}}
+ );
+
+}
+
+sub action_stock_in_out_dialog {
+ my ($self) = @_;
+
+ my $part = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
+ my $unit = SL::DB::Unit->load_cached($::form->{unit}) or die "need unit";
+ my $stock = $::form->{stock};
+ my $row = $::form->{row};
+ my $item_id = $::form->{item_id};
+ my $qty = _parse_number($::form->{qty_as_number});
+
+ my $inout = $self->type_data->transfer;
+
+ my @contents = DO->get_item_availability(parts_id => $part->id);
+ my $stock_info = DO->unpack_stock_information(packed => $stock);
+
+ $self->merge_stock_data($stock_info, \@contents, $part, $unit);
+
+ $self->render("delivery_order/stock_dialog", { layout => 0 },
+ WHCONTENTS => $self->order->delivered ? $stock_info : \@contents,
+ part => $part,
+ do_qty => $qty,
+ do_unit => $unit->unit,
+ delivered => $self->order->delivered,
+ row => $row,
+ item_id => $item_id,
+ );
+}
+
+sub action_update_stock_information {
+ my ($self) = @_;
+
+ my $stock_info = $::form->{stock_info};
+ my $unit = $::form->{unit};
+ my $yaml = SL::YAML::Dump($stock_info);
+ my $stock_qty = $self->calculate_stock_in_out_from_stock_info($unit, $stock_info);
+
+ my $response = {
+ stock_info => $yaml,
+ stock_qty => $stock_qty,
+ };
+ $self->render(\ SL::JSON::to_json($response), { layout => 0, type => 'json', process => 0 });
+}
+
+sub merge_stock_data {
+ my ($self, $stock_info, $contents, $part, $unit) = @_;
+ # TODO rewrite to mapping
+
+ if (!$self->order->delivered) {
+ for my $row (@$contents) {
+ # row here is in parts units. stock is in item units
+ $row->{available_qty} = _format_number($part->unit_obj->convert_to($row->{qty}, $unit));
+
+ for my $sinfo (@{ $stock_info }) {
+ next if $row->{bin_id} != $sinfo->{bin_id} ||
+ $row->{warehouse_id} != $sinfo->{warehouse_id} ||
+ $row->{chargenumber} ne $sinfo->{chargenumber} ||
+ $row->{bestbefore} ne $sinfo->{bestbefore};
+
+ $row->{"stock_$_"} = $sinfo->{$_}
+ for qw(qty unit error delivery_order_items_stock_id);
+ }
+ }
+
+ } else {
+ for my $sinfo (@{ $stock_info }) {
+ my $bin = SL::DB::Bin->load_cached($sinfo->{bin_id});
+ $sinfo->{warehousedescription} = $bin->warehouse->description;
+ $sinfo->{bindescription} = $bin->description;
+ map { $sinfo->{"stock_$_"} = $sinfo->{$_} } qw(qty unit);
+ }
+ }
+}
+
+# load the second row for one or more items
+#
+# This action gets the html code for all items second rows by rendering a template for
+# the second row and sets the html code via client js.
+sub action_load_second_rows {
+ my ($self) = @_;
+
+ foreach my $item_id (@{ $::form->{item_ids} }) {
+ my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
+ my $item = $self->order->items_sorted->[$idx];
+
+ $self->js_load_second_row($item, $item_id, 0);
+ }
+
+ $self->js->run('kivi.DeliveryOrder.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
+
+ $self->js->render();
+}
+
+# update description, notes and sellprice from master data
+sub action_update_row_from_master_data {
+ my ($self) = @_;
+
+ foreach my $item_id (@{ $::form->{item_ids} }) {
+ my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
+ my $item = $self->order->items_sorted->[$idx];
+ my $texts = get_part_texts($item->part, $self->order->language_id);
+
+ $item->description($texts->{description});
+ $item->longdescription($texts->{longdescription});
+
+ my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
+
+ my $price_src;
+ if ($item->part->is_assortment) {
+ # add assortment items with price 0, as the components carry the price
+ $price_src = $price_source->price_from_source("");
+ $price_src->price(0);
+ } else {
+ $price_src = $price_source->best_price
+ ? $price_source->best_price
+ : $price_source->price_from_source("");
+ $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
+ $price_src->price(0) if !$price_source->best_price;
+ }
+
+
+ $item->sellprice($price_src->price);
+ $item->active_price_source($price_src);
+
+ $self->js
+ ->run('kivi.DeliveryOrder.update_sellprice', $item_id, $item->sellprice_as_number)
+ ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
+ ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
+ ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
+
+ if ($self->search_cvpartnumber) {
+ $self->get_item_cvpartnumber($item);
+ $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
+ }
+ }
+
+ $self->js_redisplay_line_values;
+
+ $self->js->render();
+}
+
+sub action_transfer_stock {
+ my ($self) = @_;
+
+ 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');
+
+ my $errors = $self->save;
+
+ if (@$errors) {
+ $self->js->flash('error', $_) for @$errors;
+ return $self->js->render;
+ }
+
+ my $order = $self->order;
+
+ # TODO move to type data
+ my $trans_type = $inout eq 'in'
+ ? SL::DB::Manager::TransferType->find_by(direction => "id", 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->qty($transfer->qty * -1) if $inout eq 'out';
+
+ 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;
+ }
+
+ SL::DB->client->with_transaction(sub {
+ $_->save for @transfer_requests;
+ $self->order->update_attributes(delivered => 1);
+ });
+
+ $self->js
+ ->flash("info", t8("Stock transfered"))
+ ->run('kivi.ActionBar.setDisabled', '#transfer_out_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', '#delete_action', t8('The parts for this order have already been transferred'))
+ ->replaceWith('#data-status-line', delivery_order_status_line($self->order))
+ ->render;
+
+}
+
+sub js_load_second_row {
+ my ($self, $item, $item_id, $do_parse) = @_;
+
+ if ($do_parse) {
+ # Parse values from form (they are formated while rendering (template)).
+ # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
+ # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
+ foreach my $var (@{ $item->cvars_by_config }) {
+ $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
+ }
+ $item->parse_custom_variable_values;
+ }
+
+ my $row_as_html = $self->p->render('delivery_order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
+
+ $self->js
+ ->html('#second_row_' . $item_id, $row_as_html)
+ ->data('#second_row_' . $item_id, 'loaded', 1);
+}
+
+sub js_redisplay_line_values {
+ my ($self) = @_;
+
+ my $is_sales = $self->order->is_sales;
+
+ # sales orders with margins
+ my @data;
+ if ($is_sales) {
+ @data = map {
+ [
+ $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
+ $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
+ $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
+ ]} @{ $self->order->items_sorted };
+ } else {
+ @data = map {
+ [
+ $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
+ ]} @{ $self->order->items_sorted };
+ }
+
+ $self->js
+ ->run('kivi.DeliveryOrder.redisplay_line_values', $is_sales, \@data);
+}
+
+sub js_redisplay_cvpartnumbers {
+ my ($self) = @_;
+
+ $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+ my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
+
+ $self->js
+ ->run('kivi.DeliveryOrder.redisplay_cvpartnumbers', \@data);
+}
+
+sub js_reset_order_and_item_ids_after_save {
+ my ($self) = @_;
+
+ $self->js
+ ->val('#id', $self->order->id)
+ ->val('#converted_from_oe_id', '')
+ ->val('#order_' . $self->nr_key(), $self->order->number);
+
+ my $idx = 0;
+ foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
+ next if !$self->order->items_sorted->[$idx]->id;
+ next if $form_item_id !~ m{^new};
+ $self->js
+ ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
+ ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
+ ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
+ } continue {
+ $idx++;
+ }
+ $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
+}
+
+#
+# helpers
+#
+
+sub init_type {
+ my ($self) = @_;
+
+ if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
+ die "Not a valid type for delivery order";
+ }
+
+ $self->type($::form->{type});
+}
+
+sub init_cv {
+ my ($self) = @_;
+
+ return $self->type_data->customervendor;
+}
+
+sub init_search_cvpartnumber {
+ my ($self) = @_;
+
+ my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
+ my $search_cvpartnumber;
+ $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
+ $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel() if $self->cv eq 'vendor';
+
+ return $search_cvpartnumber;
+}
+
+sub init_show_update_button {
+ my ($self) = @_;
+
+ !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
+}
+
+sub init_p {
+ SL::Presenter->get;
+}
+
+sub init_order {
+ $_[0]->make_order;
+}
+
+sub init_all_price_factors {
+ SL::DB::Manager::PriceFactor->get_all;
+}
+
+sub init_part_picker_classification_ids {
+ my ($self) = @_;
+
+ return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => $self->type_data->part_classification_query) } ];
+}
+
+sub check_auth {
+ my ($self) = @_;
+
+ $::auth->assert($self->type_data->access || 'DOES_NOT_EXIST');
+}
+
+# build the selection box for contacts
+#
+# Needed, if customer/vendor changed.
+sub build_contact_select {
+ my ($self) = @_;
+
+ select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
+ value_key => 'cp_id',
+ title_key => 'full_name_dep',
+ default => $self->order->cp_id,
+ with_empty => 1,
+ style => 'width: 300px',
+ );
+}
+
+# build the selection box for shiptos
+#
+# Needed, if customer/vendor changed.
+sub build_shipto_select {
+ my ($self) = @_;
+
+ select_tag('order.shipto_id',
+ [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
+ value_key => 'shipto_id',
+ title_key => 'displayable_id',
+ default => $self->order->shipto_id,
+ with_empty => 0,
+ style => 'width: 300px',
+ );
+}
+
+# build the inputs for the cusom shipto dialog
+#
+# Needed, if customer/vendor changed.
+sub build_shipto_inputs {
+ my ($self) = @_;
+
+ my $content = $self->p->render('common/_ship_to_dialog',
+ vc_obj => $self->order->customervendor,
+ cs_obj => $self->order->custom_shipto,
+ cvars => $self->order->custom_shipto->cvars_by_config,
+ id_selector => '#order_shipto_id');
+
+ div_tag($content, id => 'shipto_inputs');
+}
+
+# render the info line for business
+#
+# Needed, if customer/vendor changed.
+sub build_business_info_row
+{
+ $_[0]->p->render('delivery_order/tabs/_business_info_row', SELF => $_[0]);
+}
+
+
+sub load_order {
+ my ($self) = @_;
+
+ return if !$::form->{id};
+
+ $self->order(SL::DB::DeliveryOrder->new(id => $::form->{id})->load);
+
+ # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
+ # You need a custom shipto object to call cvars_by_config to get the cvars.
+ $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
+
+ $self->prepare_stock_info($_) for $self->order->items;
+
+ return $self->order;
+}
+
+# load or create a new order object
+#
+# And assign changes from the form to this object.
+# If the order is loaded from db, check if items are deleted in the form,
+# remove them form the object and collect them for removing from db on saving.
+# Then create/update items from form (via make_item) and add them.
+sub make_order {
+ my ($self) = @_;
+
+ # add_items adds items to an order with no items for saving, but they cannot
+ # be retrieved via items until the order is saved. Adding empty items to new
+ # order here solves this problem.
+ my $order;
+ $order = SL::DB::DeliveryOrder->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
+ $order ||= SL::DB::DeliveryOrder->new(orderitems => [], currency_id => $::instance_conf->get_currency_id(), order_type => $self->type_data->validate($::form->{type}));
+
+ my $cv_id_method = $self->cv . '_id';
+ if (!$::form->{id} && $::form->{$cv_id_method}) {
+ $order->$cv_id_method($::form->{$cv_id_method});
+ setup_order_from_cv($order);
+ }
+
+ my $form_orderitems = delete $::form->{order}->{orderitems};
+
+ $order->assign_attributes(%{$::form->{order}});
+
+ $self->setup_custom_shipto_from_form($order, $::form);
+
+ # remove deleted items
+ $self->item_ids_to_delete([]);
+ foreach my $idx (reverse 0..$#{$order->orderitems}) {
+ my $item = $order->orderitems->[$idx];
+ if (none { $item->id == $_->{id} } @{$form_orderitems}) {
+ splice @{$order->orderitems}, $idx, 1;
+ push @{$self->item_ids_to_delete}, $item->id;
+ }
+ }
+
+ my @items;
+ my $pos = 1;
+ foreach my $form_attr (@{$form_orderitems}) {
+ my $item = make_item($order, $form_attr);
+ $item->position($pos);
+ push @items, $item;
+ $pos++;
+ }
+
+ $self->prepare_stock_info($_) for $order->items, @items;
+
+ $order->add_items(grep {!$_->id} @items);
+
+ return $order;
+}
+
+# create or update items from form
+#
+# Make item objects from form values. For items already existing read from db.
+# Create a new item else. And assign attributes.
+sub make_item {
+ my ($record, $attr) = @_;
+
+ my $item;
+ $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
+
+ my $is_new = !$item;
+
+ # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
+ # they cannot be retrieved via custom_variables until the order/orderitem is
+ # saved. Adding empty custom_variables to new orderitem here solves this problem.
+ $item ||= SL::DB::DeliveryOrderItem->new(custom_variables => []);
+
+ # handle stock info
+ if (my $stock_info = delete $attr->{stock_info}) {
+ my %existing = map { $_->id => $_ } $item->delivery_order_stock_entries;
+ my @save;
+
+ for my $line (@{ DO->unpack_stock_information(packed => $stock_info) }) {
+ # lookup existing or make new
+ my $obj = delete $existing{$line->{delivery_order_items_stock_id}}
+ // SL::DB::DeliveryOrderItemsStock->new;
+
+ # assign attributes
+ $obj->$_($line->{$_}) for qw(bin_id warehouse_id chargenumber qty unit);
+ $obj->bestbefore_as_date($line->{bestfbefore})
+ if $line->{bestbefore} && $::instance_conf->get_show_bestbefore;
+ push @save, $obj if $obj->qty;
+ }
+
+ $item->delivery_order_stock_entries(@save);
+ }
+
+ $item->assign_attributes(%$attr);
+
+ if ($is_new) {
+ my $texts = get_part_texts($item->part, $record->language_id);
+ $item->longdescription($texts->{longdescription}) if !defined $attr->{longdescription};
+ $item->project_id($record->globalproject_id) if !defined $attr->{project_id};
+ $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
+ }
+
+ return $item;
+}
+
+# create a new item
+#
+# This is used to add one item
+sub new_item {
+ my ($record, $attr) = @_;
+
+ my $item = SL::DB::DeliveryOrderItem->new;
+
+ # Remove attributes where the user left or set the inputs empty.
+ # So these attributes will be undefined and we can distinguish them
+ # from zero later on.
+ for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
+ delete $attr->{$_} if $attr->{$_} eq '';
+ }
+
+ $item->assign_attributes(%$attr);
+
+ my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
+ my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+
+ $item->unit($part->unit) if !$item->unit;
+
+ my $price_src;
+ if ( $part->is_assortment ) {
+ # add assortment items with price 0, as the components carry the price
+ $price_src = $price_source->price_from_source("");
+ $price_src->price(0);
+ } elsif (defined $item->sellprice) {
+ $price_src = $price_source->price_from_source("");
+ $price_src->price($item->sellprice);
+ } else {
+ $price_src = $price_source->best_price
+ ? $price_source->best_price
+ : $price_source->price_from_source("");
+ $price_src->price(0) if !$price_source->best_price;
+ }
+
+ my $discount_src;
+ if (defined $item->discount) {
+ $discount_src = $price_source->discount_from_source("");
+ $discount_src->discount($item->discount);
+ } else {
+ $discount_src = $price_source->best_discount
+ ? $price_source->best_discount
+ : $price_source->discount_from_source("");
+ $discount_src->discount(0) if !$price_source->best_discount;
+ }
+
+ my %new_attr;
+ $new_attr{part} = $part;
+ $new_attr{description} = $part->description if ! $item->description;
+ $new_attr{qty} = 1.0 if ! $item->qty;
+ $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
+ $new_attr{sellprice} = $price_src->price;
+ $new_attr{discount} = $discount_src->discount;
+ $new_attr{active_price_source} = $price_src;
+ $new_attr{active_discount_source} = $discount_src;
+ $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
+ $new_attr{project_id} = $record->globalproject_id;
+ $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
+
+ # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
+ # they cannot be retrieved via custom_variables until the order/orderitem is
+ # saved. Adding empty custom_variables to new orderitem here solves this problem.
+ $new_attr{custom_variables} = [];
+
+ my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
+
+ $item->assign_attributes(%new_attr, %{ $texts });
+
+ return $item;
+}
+
+sub prepare_stock_info {
+ my ($self, $item) = @_;
+
+ $item->{stock_info} = SL::YAML::Dump([
+ map +{
+ delivery_order_items_stock_id => $_->id,
+ qty => $_->qty,
+ warehouse_id => $_->warehouse_id,
+ bin_id => $_->bin_id,
+ chargenumber => $_->chargenumber,
+ unit => $_->unit,
+ }, $item->delivery_order_stock_entries
+ ]);
+}
+
+sub setup_order_from_cv {
+ my ($order) = @_;
+
+ $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
+
+ $order->intnotes($order->customervendor->notes);
+
+ if ($order->is_sales) {
+ $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
+ $order->taxincluded(defined($order->customer->taxincluded_checked)
+ ? $order->customer->taxincluded_checked
+ : $::myconfig{taxincluded_checked});
+ }
+
+}
+
+# setup custom shipto from form
+#
+# The dialog returns form variables starting with 'shipto' and cvars starting
+# with 'shiptocvar_'.
+# Mark it to be deleted if a shipto from master data is selected
+# (i.e. order has a shipto).
+# Else, update or create a new custom shipto. If the fields are empty, it
+# will not be saved on save.
+sub setup_custom_shipto_from_form {
+ my ($self, $order, $form) = @_;
+
+ if ($order->shipto) {
+ $self->is_custom_shipto_to_delete(1);
+ } else {
+ my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
+
+ my $shipto_cvars = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
+ my $shipto_attrs = {map { $_ => delete $form->{$_}} grep { m{^shipto} } keys %$form};
+
+ $custom_shipto->assign_attributes(%$shipto_attrs);
+ $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
+ }
+}
+
+# get data for saving, printing, ..., that is not changed in the form
+#
+# Only cvars for now.
+sub get_unalterable_data {
+ my ($self) = @_;
+
+ foreach my $item (@{ $self->order->items }) {
+ # autovivify all cvars that are not in the form (cvars_by_config can do it).
+ # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
+ foreach my $var (@{ $item->cvars_by_config }) {
+ $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
+ }
+ $item->parse_custom_variable_values;
+ }
+}
+
+# delete the order
+#
+# And remove related files in the spool directory
+sub delete {
+ my ($self) = @_;
+
+ my $errors = [];
+ my $db = $self->order->db;
+
+ $db->with_transaction(
+ sub {
+ my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
+ $self->order->delete;
+ my $spool = $::lx_office_conf{paths}->{spool};
+ unlink map { "$spool/$_" } @spoolfiles if $spool;
+
+ $self->save_history('DELETED');
+
+ 1;
+ }) || push(@{$errors}, $db->error);
+
+ return $errors;
+}
+
+# save the order
+#
+# And delete items that are deleted in the form.
+sub save {
+ my ($self) = @_;
+
+ my $errors = [];
+ my $db = $self->order->db;
+
+ $db->with_transaction(sub {
+ # delete custom shipto if it is to be deleted or if it is empty
+ if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
+ $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
+ $self->order->custom_shipto(undef);
+ }
+
+ SL::DB::DeliveryOrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
+ $self->order->save(cascade => 1);
+
+ # link records
+ if ($::form->{converted_from_oe_id}) {
+ my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
+ foreach my $converted_from_oe_id (@converted_from_oe_ids) {
+ my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
+ $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/ && $self->order->is_type(PURCHASE_DELIVERY_ORDER_TYPE);
+ $src->link_to_record($self->order);
+ }
+ if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
+ my $idx = 0;
+ foreach (@{ $self->order->items_sorted }) {
+ my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
+ next if !$from_id;
+ SL::DB::RecordLink->new(from_table => 'orderitems',
+ from_id => $from_id,
+ to_table => 'orderitems',
+ to_id => $_->id
+ )->save;
+ $idx++;
+ }
+ }
+ }
+
+ $self->save_history('SAVED');
+
+ 1;
+ }) || push(@{$errors}, $db->error);
+
+ return $errors;
+}
+
+sub workflow_sales_or_request_for_quotation {
+ my ($self) = @_;
+
+ # always save
+ my $errors = $self->save();
+
+ if (scalar @{ $errors }) {
+ $self->js->flash('error', $_) for @{ $errors };
+ return $self->js->render();
+ }
+
+ my $destination_type = $self->type_data->workflow("to_quotation_type");
+
+ $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
+ $self->{converted_from_oe_id} = delete $::form->{id};
+
+ # set item ids to new fake id, to identify them as new items
+ foreach my $item (@{$self->order->items_sorted}) {
+ $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+ }
+
+ # change form type
+ $::form->{type} = $destination_type;
+ $self->type($self->init_type);
+ $self->cv ($self->init_cv);
+ $self->check_auth;
+
+ $self->get_unalterable_data();
+ $self->pre_render();
+
+ # trigger rendering values for second row as hidden, because they
+ # are loaded only on demand. So we need to keep the values from the
+ # source.
+ $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+
+ $self->render(
+ 'delivery_order/form',
+ title => $self->get_title_for('edit'),
+ %{$self->{template_args}}
+ );
+}
+
+sub workflow_sales_or_purchase_order {
+ my ($self) = @_;
+
+ # always save
+ my $errors = $self->save();
+
+ if (scalar @{ $errors }) {
+ $self->js->flash('error', $_) foreach @{ $errors };
+ return $self->js->render();
+ }
+
+ my $destination_type = $self->type_data->workflow("to_order_type");
+
+ # check for direct delivery
+ # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
+ my $custom_shipto;
+ if ($self->type_data->workflow("to_order_copy_shipto") && $::form->{use_shipto} && $self->order->shipto) {
+ $custom_shipto = $self->order->shipto->clone('SL::DB::DeliveryOrder');
+ }
+
+ $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
+ $self->{converted_from_oe_id} = delete $::form->{id};
+
+ # set item ids to new fake id, to identify them as new items
+ foreach my $item (@{$self->order->items_sorted}) {
+ $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+ }
+
+ if ($self->type_data->workflow("to_order_copy_shipto")) {
+ if ($::form->{use_shipto}) {
+ $self->order->custom_shipto($custom_shipto) if $custom_shipto;
+ } else {
+ # remove any custom shipto if not wanted
+ $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
+ }
+ }
+
+ # change form type
+ $::form->{type} = $destination_type;
+ $self->type($self->init_type);
+ $self->cv ($self->init_cv);
+ $self->check_auth;
+
+ $self->get_unalterable_data();
+ $self->pre_render();
+
+ # trigger rendering values for second row as hidden, because they
+ # are loaded only on demand. So we need to keep the values from the
+ # source.
+ $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+
+ $self->render(
+ 'delivery_order/form',
+ title => $self->get_title_for('edit'),
+ %{$self->{template_args}}
+ );
+}
+
+sub pre_render {
+ my ($self) = @_;
+
+ $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
+ $self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
+ $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
+ $self->{all_languages} = SL::DB::Manager::Language->get_all_sorted();
+ $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
+ deleted => 0 ] ],
+ sort_by => 'name');
+ $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
+ deleted => 0 ] ],
+ sort_by => 'name');
+ $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
+ obsolete => 0 ] ]);
+ $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
+ $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
+ $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
+ $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
+
+ my $print_form = Form->new('');
+ $print_form->{type} = $self->type;
+ $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
+ $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
+ form => $print_form,
+ options => {dialog_name_prefix => 'print_options.',
+ show_headers => 1,
+ no_queue => 1,
+ no_postscript => 1,
+ no_opendocument => 0,
+ no_html => 1},
+ );
+
+ foreach my $item (@{$self->order->orderitems}) {
+ my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
+ $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
+ $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
+ }
+
+ if ($self->order->${\ $self->type_data->nr_key } && $::instance_conf->get_webdav) {
+ my $webdav = SL::Webdav->new(
+ type => $self->type,
+ number => $self->order->number,
+ );
+ my @all_objects = $webdav->get_all_objects;
+ @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
+ type => t8('File'),
+ link => File::Spec->catfile($_->full_filedescriptor),
+ } } @all_objects;
+ }
+
+ $self->{template_args}{in_out} = $self->type_data->transfer;
+
+ $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+ $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.DeliveryOrder kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
+ calculate_qty kivi.Validator follow_up show_history);
+ $self->setup_edit_action_bar;
+}
+
+sub setup_edit_action_bar {
+ my ($self, %params) = @_;
+
+ my $deletion_allowed = $self->type_data->show_menu("delete");
+
+ for my $bar ($::request->layout->get('actionbar')) {
+ $bar->add(
+ combobox => [
+ action => [
+ t8('Save'),
+ call => [ 'kivi.DeliveryOrder.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
+ $::instance_conf->get_order_warn_no_deliverydate,
+ ],
+ ],
+ action => [
+ t8('Save as new'),
+ call => [ 'kivi.DeliveryOrder.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
+ disabled => $self->type eq 'supplier_delivery_order' ? t8('Need a workflow for Supplier Delivery Order')
+ : !$self->order->id ? t8('This object has not been saved yet.')
+ : undef,
+ ],
+ ], # end of combobox "Save"
+
+ combobox => [
+ action => [
+ t8('Workflow'),
+ ],
+ action => [
+ t8('Save and Quotation'),
+ submit => [ '#order_form', { action => "DeliveryOrder/sales_quotation" } ],
+ only_if => $self->type_data->show_menu("save_and_quotation"),
+ ],
+ action => [
+ t8('Save and RFQ'),
+ submit => [ '#order_form', { action => "DeliveryOrder/request_for_quotation" } ],
+ only_if => $self->type_data->show_menu("save_and_rfq"),
+ ],
+ action => [
+ t8('Save and Sales Order'),
+ submit => [ '#order_form', { action => "DeliveryOrder/sales_order" } ],
+ only_if => $self->type_data->show_menu("save_and_sales_order"),
+ ],
+ action => [
+ t8('Save and Purchase Order'),
+ call => [ 'kivi.DeliveryOrder.purchase_order_check_for_direct_delivery' ],
+ only_if => $self->type_data->show_menu("save_and_purchase_order"),
+ ],
+ action => [
+ t8('Save and Delivery Order'),
+ call => [ 'kivi.DeliveryOrder.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
+ $::instance_conf->get_order_warn_no_deliverydate,
+ ],
+ only_if => $self->type_data->show_menu("save_and_delivery_order"),
+ ],
+ action => [
+ t8('Save and Invoice'),
+ call => [ 'kivi.DeliveryOrder.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
+ only_if => $self->type_data->show_menu("save_and_invoice"),
+ ],
+ action => [
+ t8('Save and AP Transaction'),
+ call => [ 'kivi.DeliveryOrder.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
+ only_if => $self->type_data->show_menu("save_and_ap_transaction"),
+ ],
+
+ ], # end of combobox "Workflow"
+
+ combobox => [
+ action => [
+ t8('Export'),
+ ],
+ action => [
+ t8('Save and preview PDF'),
+ call => [ 'kivi.DeliveryOrder.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
+ $::instance_conf->get_order_warn_no_deliverydate,
+ ],
+ ],
+ action => [
+ t8('Save and print'),
+ call => [ 'kivi.DeliveryOrder.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
+ $::instance_conf->get_order_warn_no_deliverydate,
+ ],
+ ],
+ action => [
+ t8('Save and E-mail'),
+ id => 'save_and_email_action',
+ call => [ 'kivi.DeliveryOrder.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
+ $::instance_conf->get_order_warn_no_deliverydate,
+ ],
+ disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+ ],
+ action => [
+ t8('Download attachments of all parts'),
+ call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
+ disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+ only_if => $::instance_conf->get_doc_storage,
+ ],
+ ], # end of combobox "Export"
+
+ action => [
+ t8('Delete'),
+ id => 'delete_action',
+ call => [ 'kivi.DeliveryOrder.delete_order' ],
+ confirm => $::locale->text('Do you really want to delete this object?'),
+ disabled => !$self->order->id ? t8('This object has not been saved yet.') :
+ $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
+ only_if => $self->type_data->show_menu("delete"),
+ ],
+
+ combobox => [
+ action => [
+ t8('Transfer out'),
+ id => 'transfer_out_action',
+ call => [ 'kivi.DeliveryOrder.save', 'transfer_stock' ],
+ disabled => !$self->order->id ? t8('This object has not been saved yet.') :
+ $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
+ only_if => $self->type_data->properties('transfer') eq 'out',
+ confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
+ ],
+ action => [
+ t8('Transfer in'),
+ id => 'transfer_in_action',
+ call => [ 'kivi.DeliveryOrder.save', 'transfer_stock' ],
+ disabled => !$self->order->id ? t8('This object has not been saved yet.') :
+ $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
+ only_if => $self->type_data->properties('transfer') eq 'in',
+ confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
+ ],
+ ],
+
+ combobox => [
+ action => [
+ t8('more')
+ ],
+ action => [
+ t8('Follow-Up'),
+ call => [ 'kivi.DeliveryOrder.follow_up_window' ],
+ disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+ only_if => $::auth->assert('productivity', 1),
+ ],
+ action => [
+ t8('History'),
+ call => [ 'set_history_window', $self->order->id, 'id' ],
+ disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
+ ],
+ ], # end of combobox "more"
+ );
+ }
+}
+
+sub generate_pdf {
+ my ($order, $pdf_ref, $params) = @_;
+
+ my @errors = ();
+
+ my $print_form = Form->new('');
+ $print_form->{type} = $order->type;
+ $print_form->{formname} = $params->{formname} || $order->type;
+ $print_form->{format} = $params->{format} || 'pdf';
+ $print_form->{media} = $params->{media} || 'file';
+ $print_form->{groupitems} = $params->{groupitems};
+ $print_form->{printer_id} = $params->{printer_id};
+ $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
+
+ $order->language($params->{language});
+ $order->flatten_to_form($print_form, format_amounts => 1);
+
+ my $template_ext;
+ my $template_type;
+ if ($print_form->{format} =~ /(opendocument|oasis)/i) {
+ $template_ext = 'odt';
+ $template_type = 'OpenDocument';
+ }
+
+ # search for the template
+ my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
+ name => $print_form->{formname},
+ extension => $template_ext,
+ email => $print_form->{media} eq 'email',
+ language => $params->{language},
+ printer_id => $print_form->{printer_id},
+ );
+
+ if (!defined $template_file) {
+ push @errors, $::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files);
+ }
+
+ return @errors if scalar @errors;
+
+ $print_form->throw_on_error(sub {
+ eval {
+ $print_form->prepare_for_printing;
+
+ $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
+ format => $print_form->{format},
+ template_type => $template_type,
+ template => $template_file,
+ variables => $print_form,
+ variable_content_types => {
+ longdescription => 'html',
+ partnotes => 'html',
+ notes => 'html',
+ },
+ );
+ 1;
+ } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
+ });
+
+ return @errors;
+}
+
+sub get_files_for_email_dialog {
+ my ($self) = @_;
+
+ my %files = map { ($_ => []) } qw(versions files vc_files part_files);
+
+ return %files if !$::instance_conf->get_doc_storage;
+
+ if ($self->order->id) {
+ $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
+ $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
+ $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
+ $files{project_files} = [ SL::File->get_all( object_id => $self->order->globalproject_id, object_type => 'project', file_type => 'attachment') ];
+ }
+
+ my @parts =
+ uniq_by { $_->{id} }
+ map {
+ +{ id => $_->part->id,
+ partnumber => $_->part->partnumber }
+ } @{$self->order->items_sorted};
+
+ foreach my $part (@parts) {
+ my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
+ push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
+ }
+
+ foreach my $key (keys %files) {
+ $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
+ }
+
+ return %files;
+}
+
+sub get_title_for {
+ my ($self, $action) = @_;
+
+ return '' if none { lc($action)} qw(add edit);
+ return $self->type_data->text($action);
+}
+
+sub get_item_cvpartnumber {
+ my ($self, $item) = @_;
+
+ return if !$self->search_cvpartnumber;
+ return if !$self->order->customervendor;
+
+ if ($self->cv eq 'vendor') {
+ my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
+ $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
+ } elsif ($self->cv eq 'customer') {
+ my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
+ $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
+ }
+}
+
+sub get_part_texts {
+ my ($part_or_id, $language_or_id, %defaults) = @_;
+
+ my $part = ref($part_or_id) ? $part_or_id : SL::DB::Part->load_cached($part_or_id);
+ my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
+ my $texts = {
+ description => $defaults{description} // $part->description,
+ longdescription => $defaults{longdescription} // $part->notes,
+ };
+
+ return $texts unless $language_id;
+
+ my $translation = SL::DB::Manager::Translation->get_first(
+ where => [
+ parts_id => $part->id,
+ language_id => $language_id,
+ ]);
+
+ $texts->{description} = $translation->translation if $translation && $translation->translation;
+ $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
+
+ return $texts;
+}
+
+sub nr_key {
+ return $_[0]->type_data->nr_key;
+}
+
+sub save_and_redirect_to {
+ my ($self, %params) = @_;
+
+ my $errors = $self->save();
+
+ if (scalar @{ $errors }) {
+ $self->js->flash('error', $_) foreach @{ $errors };
+ return $self->js->render();
+ }
+
+ flash_later('info', $self->type_data->text("saved"));
+
+ $self->redirect_to(%params, id => $self->order->id);
+}
+
+sub save_history {
+ my ($self, $addition) = @_;
+
+ my $number_type = $self->nr_key;
+ my $snumbers = $number_type . '_' . $self->order->$number_type;
+
+ SL::DB::History->new(
+ trans_id => $self->order->id,
+ employee_id => SL::DB::Manager::Employee->current->id,
+ what_done => $self->order->type,
+ snumbers => $snumbers,
+ addition => $addition,
+ )->save;
+}
+
+sub store_pdf_to_webdav_and_filemanagement {
+ my($order, $content, $filename) = @_;
+
+ my @errors;
+
+ # copy file to webdav folder
+ if ($order->number && $::instance_conf->get_webdav_documents) {
+ my $webdav = SL::Webdav->new(
+ type => $order->type,
+ number => $order->number,
+ );
+ my $webdav_file = SL::Webdav::File->new(
+ webdav => $webdav,
+ filename => $filename,
+ );
+ eval {
+ $webdav_file->store(data => \$content);
+ 1;
+ } or do {
+ push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
+ };
+ }
+ if ($order->id && $::instance_conf->get_doc_storage) {
+ eval {
+ SL::File->save(object_id => $order->id,
+ object_type => $order->type,
+ mime_type => 'application/pdf',
+ source => 'created',
+ file_type => 'document',
+ file_name => $filename,
+ file_contents => $content);
+ 1;
+ } or do {
+ push @errors, t8('Storing PDF in storage backend failed: #1', $@);
+ };
+ }
+
+ return @errors;
+}
+
+sub calculate_stock_in_out_from_stock_info {
+ my ($self, $unit, $stock_info) = @_;
+
+ return "" if !$unit;
+
+ my %units_by_name = map { $_->name => $_ } @{ SL::DB::Manager::Unit->get_all };
+
+ my $sum = sum0 map {
+ $units_by_name{$_->{unit}}->convert_to($_->{qty}, $units_by_name{$unit})
+ } @$stock_info;
+
+ my $content = _format_number($sum, 2) . ' ' . $unit;
+
+ return $content;
+}
+
+sub calculate_stock_in_out {
+ my ($self, $item, $stock_info) = @_;
+
+ return "" if !$item->part || !$item->part->unit || !$item->unit;
+
+ my $sum = sum0 map {
+ $_->unit_obj->convert_to($_->qty, $item->unit_obj)
+ } $item->delivery_order_stock_entries;
+
+ my $content = _format_number($sum, 2);
+
+ return $content;
+}
+
+sub init_type_data {
+ SL::Controller::DeliveryOrder::TypeData->new($_[0]);
+}
+
+sub init_valid_types {
+ $_[0]->type_data->valid_types;
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Order - controller for orders
+
+=head1 SYNOPSIS
+
+This is a new form to enter orders, completely rewritten with the use
+of controller and java script techniques.
+
+The aim is to provide the user a better experience and a faster workflow. Also
+the code should be more readable, more reliable and better to maintain.
+
+=head2 Key Features
+
+=over 4
+
+=item *
+
+One input row, so that input happens every time at the same place.
+
+=item *
+
+Use of pickers where possible.
+
+=item *
+
+Possibility to enter more than one item at once.
+
+=item *
+
+Item list in a scrollable area, so that the workflow buttons stay at
+the bottom.
+
+=item *
+
+Reordering item rows with drag and drop is possible. Sorting item rows is
+possible (by partnumber, description, qty, sellprice and discount for now).
+
+=item *
+
+No C<update> is necessary. All entries and calculations are managed
+with ajax-calls and the page only reloads on C<save>.
+
+=item *
+
+User can see changes immediately, because of the use of java script
+and ajax.
+
+=back
+
+=head1 CODE
+
+=head2 Layout
+
+=over 4
+
+=item * C<SL/Controller/Order.pm>
+
+the controller
+
+=item * C<template/webpages/delivery_order/form.html>
+
+main form
+
+=item * C<template/webpages/delivery_order/tabs/basic_data.html>
+
+Main tab for basic_data.
+
+This is the only tab here for now. "linked records" and "webdav" tabs are
+reused from generic code.
+
+=over 4
+
+=item * C<template/webpages/delivery_order/tabs/_business_info_row.html>
+
+For displaying information on business type
+
+=item * C<template/webpages/delivery_order/tabs/_item_input.html>
+
+The input line for items
+
+=item * C<template/webpages/delivery_order/tabs/_row.html>
+
+One row for already entered items
+
+=item * C<template/webpages/delivery_order/tabs/_tax_row.html>
+
+Displaying tax information
+
+=back
+
+=item * C<js/kivi.DeliveryOrder.js>
+
+java script functions
+
+=back
+
+=head1 TODO
+
+=over 4
+
+=item * testing
+
+=item * price sources: little symbols showing better price / better discount
+
+=item * select units in input row?
+
+=item * check for direct delivery (workflow sales order -> purchase order)
+
+=item * access rights
+
+=item * display weights
+
+=item * mtime check
+
+=item * optional client/user behaviour
+
+(transactions has to be set - department has to be set -
+ force project if enabled in client config - transport cost reminder)
+
+=back
+
+=head1 KNOWN BUGS AND CAVEATS
+
+=over 4
+
+=item *
+
+Customer discount is not displayed as a valid discount in price source popup
+(this might be a bug in price sources)
+
+(I cannot reproduce this (Bernd))
+
+=item *
+
+No indication that <shift>-up/down expands/collapses second row.
+
+=item *
+
+Inline creation of parts is not currently supported
+
+=item *
+
+Table header is not sticky in the scrolling area.
+
+=item *
+
+Sorting does not include C<position>, neither does reordering.
+
+This behavior was implemented intentionally. But we can discuss, which behavior
+should be implemented.
+
+=back
+
+=head1 To discuss / Nice to have
+
+=over 4
+
+=item *
+
+How to expand/collapse second row. Now it can be done clicking the icon or
+<shift>-up/down.
+
+=item *
+
+Possibility to select PriceSources in input row?
+
+=item *
+
+This controller uses a (changed) copy of the template for the PriceSource
+dialog. Maybe there could be used one code source.
+
+=item *
+
+Rounding-differences between this controller (PriceTaxCalculator) and the old
+form. This is not only a problem here, but also in all parts using the PTC.
+There exists a ticket and a patch. This patch should be testet.
+
+=item *
+
+An indicator, if the actual inputs are saved (like in an
+editor or on text processing application).
+
+=item *
+
+A warning when leaving the page without saveing unchanged inputs.
+
+
+=back
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
--- /dev/null
+package SL::Controller::DeliveryOrder::TypeData;
+
+use strict;
+use Exporter qw(import);
+use Scalar::Util qw(weaken);
+use SL::Locale::String qw(t8);
+use SL::DB::DeliveryOrder::TypeData qw(:types :subs);
+
+my @export_types = qw(SALES_DELIVERY_ORDER_TYPE PURCHASE_DELIVERY_ORDER_TYPE SUPPLIER_DELIVERY_ORDER_TYPE RMA_DELIVERY_ORDER_TYPE);
+
+our @EXPORT_OK = (@export_types);
+our %EXPORT_TAGS = (types => \@export_types);
+
+use Rose::Object::MakeMethods::Generic scalar => [ qw(c) ];
+
+sub new {
+ my ($class, $controller) = @_;
+ my $o = bless {}, $class;
+
+ if ($controller) {
+ $o->c($controller);
+ weaken($o->{c});
+ }
+
+ return $o;
+}
+
+sub validate {
+ my ($self, $string) = @_;
+ validate_type($string);
+}
+
+sub text {
+ my ($self, $string) = @_;
+ get3($self->c->type, "text", $string);
+}
+
+sub show_menu {
+ my ($self, $string) = @_;
+ get3($self->c->type, "show_menu", $string);
+}
+
+sub workflow {
+ my ($self, $string) = @_;
+ get3($self->c->type, "workflow", $string);
+}
+
+sub properties {
+ my ($self, $string) = @_;
+ get3($self->c->type, "properties", $string);
+}
+
+sub access {
+ get($_[0]->c->type, "right");
+}
+
+sub is_quotation {
+ get3($_[0]->c->type, "properties", "is_quotation");
+}
+
+sub customervendor {
+ get3($_[0]->c->type, "properties", "customervendor");
+}
+
+sub is_customer {
+ get3($_[0]->c->type, "properties", "is_customer");
+}
+
+sub nr_key {
+ get3($_[0]->c->type, "properties", "nr_key");
+}
+
+sub transfer {
+ get3($_[0]->c->type, "properties", "transfer");
+}
+
+sub part_classification_query {
+ my ($self, $string) = @_;
+ get($self->c->type, "part_classification_query");
+}
+
+sub set_reqdate_by_type {
+ my ($self) = @_;
+
+ if (!$self->c->order->reqdate) {
+ $self->c->order->reqdate(DateTime->today_local->next_workday(extra_days => 1));
+ }
+}
+
+sub get_reqdate_by_type {
+ my ($self, $reqdate, $saved_reqdate) = @_;
+
+ if ($reqdate == $saved_reqdate) {
+ return DateTime->today_local->next_workday(extra_days => 1);
+ } else {
+ return $reqdate;
+ }
+}
+
+1;
);
}
+sub action_save_and_supplier_delivery_order {
+ my ($self) = @_;
+
+ $self->save_and_redirect_to(
+ controller => 'controller.pl',
+ action => 'DeliveryOrder/add_from_order',
+ type => 'supplier_delivery_order',
+ );
+}
+
# save the order and redirect to the frontend subroutine for a new
# invoice
sub action_save_and_invoice {
],
only_if => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
],
+ action => [
+ t8('Save and Supplier Delivery Order'),
+ call => [ 'kivi.Order.save', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
+ $::instance_conf->get_order_warn_no_deliverydate,
+ ],
+ checks => [ 'kivi.Order.check_save_active_periodic_invoices',
+ @req_trans_cost_art, @req_cusordnumber,
+ ],
+ only_if => (any { $self->type eq $_ } (purchase_order_type()))
+ ],
action => [
t8('Save and Invoice'),
call => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
{ title => t8('Sales quotation'), type => 'sales_quotation', model => 'Order', number => 'quonumber', },
{ title => t8('Sales Order'), type => 'sales_order', model => 'Order', number => 'ordnumber', },
{ title => t8('Sales delivery order'), type => 'sales_delivery_order', model => 'DeliveryOrder', number => 'donumber', },
+ { title => t8('RMA delivery order'), type => 'rma_delivery_order', model => 'DeliveryOrder', number => 'rdonumber', },
{ title => t8('Sales Invoice'), type => 'invoice', model => 'Invoice', number => 'invnumber', },
{ title => t8('Request for Quotation'), type => 'request_quotation', model => 'Order', number => 'quonumber', },
{ title => t8('Purchase Order'), type => 'purchase_order', model => 'Order', number => 'ordnumber', },
{ title => t8('Purchase delivery order'), type => 'purchase_delivery_order', model => 'DeliveryOrder', number => 'donumber', },
+ { title => t8('Supplier delivery order'), type => 'supplier_delivery_order', model => 'DeliveryOrder', number => 'sdonumber', },
{ title => t8('Purchase Invoice'), type => 'purchase_invoice', model => 'PurchaseInvoice', number => 'invnumber', },
{ title => t8('Letter'), type => 'letter', model => 'Letter', number => 'letternumber', description => 'subject', description_title => t8('Subject'), date => 'date', project => undef },
{ title => t8('Email'), type => 'email_journal', model => 'EmailJournal', number => 'id', description => 'subject', description_title => t8('Subject'), },
use SL::DB::Part;
use SL::DB::Unit;
+use SL::DB::DeliveryOrder::TypeData qw(:types);
+
use SL::Helper::Number qw(_format_total _round_total);
use List::Util qw(first);
}
sub type {
- return shift->customer_id ? 'sales_delivery_order' : 'purchase_delivery_order';
+ goto &order_type;
+}
+
+sub is_type {
+ return shift->type eq shift;
}
sub displayable_type {
goto &transdate;
}
+sub number {
+ goto &donumber;
+}
+
sub _clone_orderitem_cvar {
my ($cvar) = @_;
ordnumber payment_id reqdate salesman_id shippingpoint shipvia taxincluded taxzone_id transaction_description vendor_id billing_address_id
)),
closed => 0,
- is_sales => !!$source->customer_id,
delivered => 0,
+ order_type => $params{type},
transdate => DateTime->today_local,
);
$args{shipto_id} = $source->shipto_id;
}
+ # infer type from legacy fields if not given
+ $args{order_type} //= $source->customer_id ? 'sales_delivery_order'
+ : $source->vendor_id ? 'purchase_delivery_order'
+ : $source->is_sales ? 'sales_delivery_order'
+ : croak "need some way to set delivery order type from source";
+
my $delivery_order = $class->new(%args);
$delivery_order->assign_attributes(%{ $params{attributes} }) if $params{attributes};
my $items = delete($params{items}) || $source->items_sorted;
my %item_parents;
- my @items = map {
+ # do not copy items when converting to supplier delivery order
+ my @items = $delivery_order->is_type(SUPPLIER_DELIVERY_ORDER_TYPE) ? () : map {
my $source_item = $_;
my $source_item_id = $_->$item_parent_id_column;
my @custom_variables = map { _clone_orderitem_cvar($_) } @{ $source_item->custom_variables };
} else {
my %args = (
is_sales => 1,
+ order_type => 'sales_delivery_order',
delivered => 0,
customer_id => $sources->[0]->customer_id,
taxzone_id => $sources->[0]->customer->taxzone_id,
return $delivery_order;
}
+# legacy for compatibility
+# use type_data cusomtervendor and transfer direction instead
+sub is_sales {
+ if ($_[0]->order_type) {
+ return SL::DB::DeliveryOrder::TypeData::get3($_[0]->order_type, "properties", "is_customer");
+ }
+ return $_[0]{is_sales};
+}
+
sub customervendor {
- $_[0]->is_sales ? $_[0]->customer : $_[0]->vendor;
+ SL::DB::DeliveryOrder::TypeData::get3($_[0]->order_type, "properties", "is_customer") ? $_[0]->customer : $_[0]->vendor;
}
sub convert_to_invoice {
--- /dev/null
+package SL::DB::DeliveryOrder::TypeData;
+
+use strict;
+use Carp;
+use Exporter qw(import);
+use Scalar::Util qw(weaken);
+use SL::Locale::String qw(t8);
+
+use constant {
+ SALES_DELIVERY_ORDER_TYPE => 'sales_delivery_order',
+ PURCHASE_DELIVERY_ORDER_TYPE => 'purchase_delivery_order',
+ SUPPLIER_DELIVERY_ORDER_TYPE => 'supplier_delivery_order',
+ RMA_DELIVERY_ORDER_TYPE => 'rma_delivery_order',
+};
+
+my @export_types = qw(SALES_DELIVERY_ORDER_TYPE PURCHASE_DELIVERY_ORDER_TYPE SUPPLIER_DELIVERY_ORDER_TYPE RMA_DELIVERY_ORDER_TYPE);
+my @export_subs = qw(valid_types validate_type is_valid_type get get3);
+
+our @EXPORT_OK = (@export_types, @export_subs);
+our %EXPORT_TAGS = (types => \@export_types, subs => \@export_subs);
+
+my %type_data = (
+ SALES_DELIVERY_ORDER_TYPE() => {
+ text => {
+ delete => t8('Delivery Order has been deleted'),
+ saved => t8('Delivery Order has been saved'),
+ add => t8("Add Sales Delivery Order"),
+ edit => t8("Edit Sales Delivery Order"),
+ attachment => t8("sales_delivery_order_list"),
+ },
+ show_menu => {
+ save_and_quotation => 0,
+ save_and_rfq => 0,
+ save_and_sales_order => 0,
+ save_and_purchase_order => 0,
+ save_and_delivery_order => 0,
+ save_and_ap_transaction => 0,
+ save_and_invoice => 0,
+ delete => sub { $::instance_conf->get_sales_delivery_order_show_delete },
+ new_controller => 0,
+ },
+ properties => {
+ customervendor => "customer",
+ is_customer => 1,
+ nr_key => "donumber",
+ transfer => 'out',
+ transnumber => 'sdonumber',
+ },
+ part_classification_query => [ "used_for_sale" => 1 ],
+ right => "sales_delivery_order_edit",
+ },
+ PURCHASE_DELIVERY_ORDER_TYPE() => {
+ text => {
+ delete => t8('Delivery Order has been deleted'),
+ saved => t8('Delivery Order has been saved'),
+ add => t8("Add Purchase Delivery Order"),
+ edit => t8("Edit Purchase Delivery Order"),
+ attachment => t8("purchase_delivery_order_list"),
+ },
+ show_menu => {
+ save_and_quotation => 0,
+ save_and_rfq => 0,
+ save_and_sales_order => 0,
+ save_and_purchase_order => 0,
+ save_and_delivery_order => 0,
+ save_and_ap_transaction => 0,
+ save_and_invoice => 0,
+ delete => sub { $::instance_conf->get_sales_delivery_order_show_delete },
+ new_controller => 0,
+ },
+ properties => {
+ customervendor => "vendor",
+ is_customer => 0,
+ nr_key => "donumber",
+ transfer => 'in',
+ transnumber => 'pdonumber',
+ },
+ part_classification_query => [ "used_for_purchase" => 1 ],
+ right => "purchase_delivery_order_edit",
+ },
+ SUPPLIER_DELIVERY_ORDER_TYPE() => {
+ text => {
+ delete => t8('Supplier Delivery Order has been deleted'),
+ saved => t8('Supplier Delivery Order has been saved'),
+ add => t8("Add Supplier Delivery Order"),
+ edit => t8("Edit Supplier Delivery Order"),
+ attachment => t8("supplier_delivery_order_list"),
+ },
+ show_menu => {
+ save_and_quotation => 0,
+ save_and_rfq => 0,
+ save_and_sales_order => 0,
+ save_and_purchase_order => 0,
+ save_and_delivery_order => 0,
+ save_and_ap_transaction => 0,
+ save_and_invoice => 0,
+ delete => sub { $::instance_conf->get_sales_delivery_order_show_delete },
+ new_controller => 1,
+ },
+ properties => {
+ customervendor => "vendor",
+ is_customer => 0,
+ nr_key => "donumber",
+ transfer => 'out',
+ transnumber => 'sudonumber',
+ },
+ part_classification_query => [ "used_for_purchase" => 1 ],
+ right => "purchase_delivery_order_edit",
+ },
+ RMA_DELIVERY_ORDER_TYPE() => {
+ text => {
+ delete => t8('Delivery Order has been deleted'),
+ saved => t8('Delivery Order has been saved'),
+ add => t8("Add RMA Delivery Order"),
+ edit => t8("Edit RMA Delivery Order"),
+ attachment => t8("rma_delivery_order_list"),
+ },
+ show_menu => {
+ save_and_quotation => 0,
+ save_and_rfq => 0,
+ save_and_sales_order => 0,
+ save_and_purchase_order => 0,
+ save_and_delivery_order => 0,
+ save_and_ap_transaction => 0,
+ save_and_invoice => 0,
+ delete => sub { $::instance_conf->get_sales_delivery_order_show_delete },
+ new_controller => 1,
+ },
+ properties => {
+ customervendor => "customer",
+ is_customer => 1,
+ nr_key => "donumber",
+ transfer => 'in',
+ transnumber => 'rdonumber',
+ },
+ part_classification_query => [ "used_for_sale" => 1 ],
+ right => "sales_delivery_order_edit",
+ },
+);
+
+my @valid_types = (
+ SALES_DELIVERY_ORDER_TYPE,
+ PURCHASE_DELIVERY_ORDER_TYPE,
+ SUPPLIER_DELIVERY_ORDER_TYPE,
+ RMA_DELIVERY_ORDER_TYPE,
+);
+
+my %valid_types = map { $_ => $_ } @valid_types;
+
+sub valid_types {
+ \@valid_types;
+}
+
+sub is_valid_type {
+ !!exists $type_data{$_[0]};
+}
+
+sub validate_type {
+ my ($type) = @_;
+
+ return $valid_types{$type} // croak "invalid type '$type'";
+}
+
+sub get {
+ my ($type, $key) = @_;
+
+ croak "invalid type '$type'" unless exists $type_data{$type};
+
+ my $ret = $type_data{$type}->{$key} // die "unknown property '$key'";
+
+ ref $ret eq 'CODE'
+ ? $ret->()
+ : $ret;
+}
+
+sub get3 {
+ my ($type, $topic, $key) = @_;
+
+ croak "invalid type '$type'" unless exists $type_data{$type};
+
+ my $ret = $type_data{$type}{$topic}{$key} // croak "unknown property '$key' in topic '$topic' for type '$type'";
+
+ ref $ret eq 'CODE'
+ ? $ret->()
+ : $ret;
+}
+
+1;
. " (" . $self->delivery_order->transdate->to_kivitendo . ")";
};
+sub effective_project {
+ my ($self) = @_;
+
+ $self->project // $self->delivery_order->globalproject;
+}
+
__END__
=pod
class => 'SL::DB::Inventory',
column_map => { id => 'delivery_order_items_stock_id' },
},
+ unit_obj => {
+ type => 'many to one',
+ class => 'SL::DB::Unit',
+ column_map => { unit => 'name' },
+ },
);
__PACKAGE__->meta->initialize;
purchase_order => { number_column => 'ordnumber', number_range_column => 'ponumber', scoping => \&oe_scoping, },
sales_delivery_order => { number_column => 'donumber', number_range_column => 'sdonumber', scoping => \&do_scoping, },
purchase_delivery_order => { number_column => 'donumber', number_range_column => 'pdonumber', scoping => \&do_scoping, },
+ supplier_delivery_order => { number_column => 'donumber', number_range_column => 'sudonumber', scoping => \&do_scoping, },
+ rma_delivery_order => { number_column => 'donumber', number_range_column => 'rdonumber', scoping => \&do_scoping, },
customer => { number_column => 'customernumber', number_range_column => 'customernumber', },
vendor => { number_column => 'vendornumber', number_range_column => 'vendornumber', },
part => { number_column => 'partnumber', number_range_column => 'articlenumber', scoping => \&parts_scoping, },
package SL::DB::Inventory;
use strict;
+use Carp;
+use DateTime;
+use SL::DBUtils qw(selectrow_query);
use SL::DB::MetaSetup::Inventory;
use SL::DB::Manager::Inventory;
__PACKAGE__->meta->initialize;
+__PACKAGE__->before_save(\&_before_save_create_trans_id);
+__PACKAGE__->before_save(\&_before_save_set_shippingdate);
+__PACKAGE__->before_save(\&_before_save_set_employee);
+
# part accessor is badly named
sub part {
goto &parts;
}
+sub new_from {
+ my ($class, $obj) = @_;
+
+ if ('SL::DB::DeliveryOrderItemsStock' eq ref $obj) {
+ return $class->new_from_delivery_order_stock($obj);
+ }
+
+ croak "unknown obj type (@{[ ref $obj ]}) for SL::DB::Inventory::new_from";
+}
+
+sub new_from_delivery_order_stock {
+ my ($class, $stock) = @_;
+
+ my $project = $stock->delivery_order_item->effective_project;
+
+ return $class->new(
+ delivery_order_items_stock_id => $stock->id,
+ parts_id => $stock->delivery_order_item->parts_id,
+ qty => $stock->unit_obj->convert_to($stock->qty => $stock->delivery_order_item->part->unit_obj),
+ warehouse_id => $stock->warehouse_id,
+ bin_id => $stock->bin_id,
+ chargenumber => $stock->chargenumber,
+ bestbefore => $stock->bestbefore,
+ project_id => $project ? $project->id : undef,
+ # trans_type - not set here, set in controller
+ );
+}
+
+sub _before_save_create_trans_id {
+ my ($self, %params) = @_;
+
+ return 1 if $self->trans_id;
+
+ my ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')|);
+
+ $self->trans_id($trans_id);
+
+ return 1;
+}
+
+sub _before_save_set_shippingdate {
+ my ($self, %params) = @_;
+
+ return 1 if $self->shippingdate;
+
+ $self->shippingdate(DateTime->now);
+
+ return 1;
+}
+
+sub _before_save_set_employee {
+ my ($self, %params) = @_;
+
+ return 1 if $self->employee_id;
+
+ $self->employee(SL::DB::Manager::Employee->current);
+
+ return 1;
+}
1;
use SL::DB::Helper::Sorted;
use SL::DB::Helper::Filtered;
+use SL::DB::DeliveryOrder::TypeData qw(validate_type);
+
sub object_class { 'SL::DB::DeliveryOrder' }
__PACKAGE__->make_manager_methods;
sub type_filter {
my $class = shift;
my $type = lc(shift || '');
+ my $prefix = shift // '';
- return ('!customer_id' => undef) if $type eq 'sales_delivery_order';
- return ('!vendor_id' => undef) if $type eq 'purchase_delivery_order';
-
- die "Unknown type $type";
+ return "${prefix}order_type" => validate_type($type);
}
sub _sort_spec {
purchase_delivery_order_show_delete => { type => 'boolean', default => 'true' },
purchase_order_show_delete => { type => 'boolean', default => 'true' },
quick_search_modules => { type => 'array' },
+ rdonumber => { type => 'text' },
reqdate_interval => { type => 'integer', default => '0' },
reqdate_on => { type => 'boolean', default => 'true' },
require_transaction_description_ps => { type => 'boolean', default => 'false', not_null => 1 },
stocktaking_cutoff_date => { type => 'date' },
stocktaking_qty_threshold => { type => 'numeric', default => '0', precision => 25, scale => 5 },
stocktaking_warehouse_id => { type => 'integer' },
+ sudonumber => { type => 'text' },
taxnumber => { type => 'text' },
templates => { type => 'text' },
transfer_default => { type => 'boolean', default => 'true' },
globalproject_id => { type => 'integer' },
id => { type => 'integer', not_null => 1, sequence => 'id' },
intnotes => { type => 'text' },
- is_sales => { type => 'boolean' },
itime => { type => 'timestamp', default => 'now()' },
language_id => { type => 'integer' },
mtime => { type => 'timestamp' },
notes => { type => 'text' },
+ order_type => { type => 'text', not_null => 1 },
ordnumber => { type => 'text' },
oreqnumber => { type => 'text' },
payment_id => { type => 'integer' },
use SL::Common;
use SL::CVar;
use SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData qw(:types is_valid_type);
use SL::DB::Status;
use SL::DBUtils;
use SL::Helper::ShippedQty;
dord.transaction_description, dord.itime::DATE AS insertdate,
pr.projectnumber AS globalprojectnumber,
dep.description AS department,
+ dord.order_type,
e.name AS employee,
sm.name AS salesman
FROM delivery_orders dord
LEFT JOIN department dep ON (dord.department_id = dep.id)
|;
- push @where, ($form->{type} eq 'sales_delivery_order' ? '' : 'NOT ') . qq|COALESCE(dord.is_sales, FALSE)|;
+ if ($form->{type} && is_valid_type($form->{type})) {
+ push @where, 'dord.order_type = ?';
+ push @values, $form->{type};
+ }
if ($form->{department_id}) {
push @where, qq|dord.department_id = ?|;
$query = qq|SELECT nextval('id')|;
($form->{id}) = selectrow_query($form, $dbh, $query);
- $query = qq|INSERT INTO delivery_orders (id, donumber, employee_id, currency_id, taxzone_id) VALUES (?, '', ?, (SELECT currency_id FROM defaults LIMIT 1), ?)|;
- do_query($form, $dbh, $query, $form->{id}, conv_i($form->{employee_id}), $form->{taxzone_id});
+ $query = qq|INSERT INTO delivery_orders (id, donumber, employee_id, currency_id, taxzone_id, order_type) VALUES (?, '', ?, (SELECT currency_id FROM defaults LIMIT 1), ?, ?)|;
+ do_query($form, $dbh, $query, $form->{id}, conv_i($form->{employee_id}), $form->{taxzone_id}, SALES_DELIVERY_ORDER_TYPE);
}
my $project_id;
shippingpoint = ?, shipvia = ?, notes = ?, intnotes = ?, closed = ?,
delivered = ?, department_id = ?, language_id = ?, shipto_id = ?, billing_address_id = ?,
globalproject_id = ?, employee_id = ?, salesman_id = ?, cp_id = ?, transaction_description = ?,
- is_sales = ?, taxzone_id = ?, taxincluded = ?, payment_id = ?, currency_id = (SELECT id FROM currencies WHERE name = ?),
+ order_type = ?, taxzone_id = ?, taxincluded = ?, payment_id = ?, currency_id = (SELECT id FROM currencies WHERE name = ?),
delivery_term_id = ?
WHERE id = ?|;
conv_i($form->{globalproject_id}), conv_i($form->{employee_id}),
conv_i($form->{salesman_id}), conv_i($form->{cp_id}),
$form->{transaction_description},
- $form->{type} =~ /^sales/ ? 't' : 'f',
+ $form->{type} =~ /^sales/ ? SALES_DELIVERY_ORDER_TYPE : PURCHASE_DELIVERY_ORDER_TYPE,
conv_i($form->{taxzone_id}), $form->{taxincluded} ? 't' : 'f', conv_i($form->{payment_id}), $form->{currency},
conv_i($form->{delivery_term_id}),
conv_i($form->{id}));
use SL::DB::Invoice;
use SL::DB::InvoiceItem;
+use SL::DB::DeliveryOrder::TypeData qw(:types);
use SL::DB::Employee;
use SL::Dev::Part qw(new_part);
use SL::Dev::CustomerVendor qw(new_vendor new_customer);
die "illegal customer" unless ref($customer) eq 'SL::DB::Customer';
my $delivery_order = SL::DB::DeliveryOrder->new(
- 'is_sales' => 'true',
+ order_type => SALES_DELIVERY_ORDER_TYPE,
'closed' => undef,
customer_id => $customer->id,
taxzone_id => $customer->taxzone_id,
die "illegal customer" unless ref($vendor) eq 'SL::DB::Vendor';
my $delivery_order = SL::DB::DeliveryOrder->new(
- 'is_sales' => 'false',
+ order_type => PURCHASE_DELIVERY_ORDER_TYPE,
'closed' => undef,
vendor_id => $vendor->id,
taxzone_id => $vendor->taxzone_id,
storno_invoice => $main::locale->text('Storno Invoice'),
sales_delivery_order => $main::locale->text('Delivery Order'),
purchase_delivery_order => $main::locale->text('Delivery Order'),
+ supplier_delivery_order => $main::locale->text('Supplier Delivery Order'),
+ rma_delivery_order => $main::locale->text('RMA Delivery Order'),
dunning => $main::locale->text('Dunning'),
dunning1 => $main::locale->text('Payment Reminder'),
dunning2 => $main::locale->text('Dunning'),
use strict;
use Exporter qw(import);
use List::Util qw(max min);
+use List::UtilsBy qw(rev_nsort_by);
use Config;
our @EXPORT_OK = qw(
($form->{type} eq 'final_invoice') ? (
opthash("final_invoice", $form->{PD}{final_invoice}, $locale->text('Final Invoice')),
) : undef,
- ($form->{type} =~ /_delivery_order$/) ? (
+ ($form->{type} =~ /^supplier_delivery_order$/) ? (
+ opthash('supplier_delivery_order', $form->{PD}{supplier_delivery_order}, $locale->text('Supplier Delivery Order')),
+ ) : undef,
+ ($form->{type} =~ /(sales|purchase)_delivery_order$/) ? (
opthash($form->{type}, $form->{PD}{$form->{type}}, $locale->text('Delivery Order')),
opthash('pick_list', $form->{PD}{pick_list}, $locale->text('Pick List')),
) : undef,
use strict;
+use SL::DB::DeliveryOrder::TypeData ();
+use SL::Locale::String qw(t8);
use SL::Presenter::EscapedText qw(escape is_escaped);
use Exporter qw(import);
-our @EXPORT_OK = qw(sales_delivery_order purchase_delivery_order);
+our @EXPORT_OK = qw(sales_delivery_order purchase_delivery_order delivery_order_status_line);
use Carp;
return _do_record($delivery_order, 'sales_delivery_order', %params);
}
+sub rma_delivery_order {
+ my ($delivery_order, %params) = @_;
+
+ return _do_new_record($delivery_order, 'rma_delivery_order', %params);
+}
+
sub purchase_delivery_order {
my ($delivery_order, %params) = @_;
return _do_record($delivery_order, 'purchase_delivery_order', %params);
}
+sub supplier_delivery_order {
+ my ($delivery_order, %params) = @_;
+
+ return _do_new_record($delivery_order, 'supplier_delivery_order', %params);
+}
+
+sub _do_new_record {
+ my ($delivery_order, $type, %params) = @_;
+
+ $params{display} ||= 'inline';
+
+ croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
+
+ my $text = join '', (
+ $params{no_link} ? '' : '<a href="contoller.pl?action=DeliveryOrder/edit&type=' . $type . '&id=' . escape($delivery_order->id) . '">',
+ escape($delivery_order->donumber),
+ $params{no_link} ? '' : '</a>',
+ );
+ is_escaped($text);
+}
+
sub _do_record {
my ($delivery_order, $type, %params) = @_;
is_escaped($text);
}
+sub stock_status {
+ my ($delivery_order) = @_;
+
+ my $in_out = SL::DB::DeliveryOrder::TypeData::get3($delivery_order->type, "properties", "transfer");
+
+ if ($in_out eq 'in') {
+ return escape($delivery_order->delivered ? t8('transferred in') : t8('not transferred in yet'));
+ }
+
+ if ($in_out eq 'out') {
+ return escape($delivery_order->delivered ? t8('transferred out') : t8('not transferred out yet'));
+ }
+}
+
+sub closed_status {
+ my ($delivery_order) = @_;
+
+ return escape($delivery_order->closed ? t8('Closed') : t8('Open'))
+}
+
+sub status_line {
+ my ($delivery_order) = @_;
+
+ return "" unless $delivery_order->id;
+
+ stock_status($delivery_order) . " ; " . closed_status($delivery_order)
+}
+
+sub delivery_order_status_line { goto &status_line };
+
1;
__END__
$output .= _sales_quotation_list( $groups{sales_quotations}, %params) if $groups{sales_quotations};
$output .= _sales_order_list( $groups{sales_orders}, %params) if $groups{sales_orders};
$output .= _sales_delivery_order_list( $groups{sales_delivery_orders}, %params) if $groups{sales_delivery_orders};
+ $output .= _rma_delivery_order_list( $groups{rma_delivery_orders}, %params) if $groups{rma_delivery_orders};
$output .= _sales_invoice_list( $groups{sales_invoices}, %params) if $groups{sales_invoices};
$output .= _ar_transaction_list( $groups{ar_transactions}, %params) if $groups{ar_transactions};
$output .= _request_quotation_list( $groups{purchase_quotations}, %params) if $groups{purchase_quotations};
$output .= _purchase_order_list( $groups{purchase_orders}, %params) if $groups{purchase_orders};
$output .= _purchase_delivery_order_list($groups{purchase_delivery_orders}, %params) if $groups{purchase_delivery_orders};
+ $output .= _supplier_delivery_order_list($groups{supplier_delivery_orders}, %params) if $groups{supplier_delivery_orders};
$output .= _purchase_invoice_list( $groups{purchase_invoices}, %params) if $groups{purchase_invoices};
$output .= _ap_transaction_list( $groups{ap_transactions}, %params) if $groups{ap_transactions};
shop_orders => sub { (ref($_[0]) eq 'SL::DB::ShopOrder') && $_[0]->id },
sales_quotations => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('sales_quotation') },
sales_orders => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('sales_order') },
- sales_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder') && $_[0]->is_sales },
+ sales_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder') && $_[0]->is_type('sales_delivery_order') },
+ rma_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder') && $_[0]->is_type('rma_delivery_order') },
sales_invoices => sub { (ref($_[0]) eq 'SL::DB::Invoice') && $_[0]->invoice },
ar_transactions => sub { (ref($_[0]) eq 'SL::DB::Invoice') && !$_[0]->invoice },
purchase_quotations => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('request_quotation') },
purchase_orders => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('purchase_order') },
- purchase_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder') && !$_[0]->is_sales },
+ purchase_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder') && $_[0]->is_type('purchase_delivery_order') },
+ supplier_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder') && $_[0]->is_type('supplier_delivery_order') },
purchase_invoices => sub { (ref($_[0]) eq 'SL::DB::PurchaseInvoice') && $_[0]->invoice },
ap_transactions => sub { (ref($_[0]) eq 'SL::DB::PurchaseInvoice') && !$_[0]->invoice },
sepa_collections => sub { (ref($_[0]) eq 'SL::DB::SepaExportItem') && $_[0]->ar_id },
);
}
+sub _rma_delivery_order_list {
+ my ($list, %params) = @_;
+
+ return record_list(
+ $list,
+ title => $::locale->text('RMA Delivery Orders'),
+ type => 'rma_delivery_order',
+ columns => [
+ [ $::locale->text('Delivery Order Date'), 'transdate' ],
+ [ $::locale->text('Delivery Order Number'), sub { $_[0]->presenter->rma_delivery_order(display => 'table-cell') } ],
+ [ $::locale->text('Order Number'), 'ordnumber' ],
+ [ $::locale->text('Customer'), 'customer' ],
+ [ $::locale->text('Transaction description'), 'transaction_description' ],
+ [ $::locale->text('Project'), 'globalproject', ],
+ [ $::locale->text('Delivered'), 'delivered' ],
+ [ $::locale->text('Closed'), 'closed' ],
+ ],
+ %params,
+ );
+}
+
sub _purchase_delivery_order_list {
my ($list, %params) = @_;
);
}
+sub _supplier_delivery_order_list {
+ my ($list, %params) = @_;
+
+ return record_list(
+ $list,
+ title => $::locale->text('Supplier Delivery Orders'),
+ type => 'supplier_delivery_order',
+ columns => [
+ [ $::locale->text('Delivery Order Date'), 'transdate' ],
+ [ $::locale->text('Delivery Order Number'), sub { $_[0]->presenter->supplier_delivery_order(display => 'table-cell') } ],
+ [ $::locale->text('Order Number'), 'ordnumber' ],
+ [ $::locale->text('Vendor'), 'vendor' ],
+ [ $::locale->text('Transaction description'), 'transaction_description' ],
+ [ $::locale->text('Project'), 'globalproject', ],
+ [ $::locale->text('Delivered'), 'delivered' ],
+ [ $::locale->text('Closed'), 'closed' ],
+ ],
+ %params,
+ );
+}
+
sub _sales_invoice_list {
my ($list, %params) = @_;
=item * sales delivery orders
+=item * rma delivery orders
+
=item * sales invoices
=item * AR transactions
=item * purchase delivery orders
+=item * supplier delivery orders
+
=item * purchase invoices
=item * AP transactions
use SL::DBUtils;
use SL::PrefixedNumber;
use SL::DB;
+use SL::DB::DeliveryOrder::TypeData;
use Rose::Object::MakeMethods::Generic
(
scalar => [ qw(type id number save dbh dbh_provided business_id) ],
);
-my @SUPPORTED_TYPES = qw(invoice invoice_for_advance_payment final_invoice credit_note customer vendor sales_delivery_order purchase_delivery_order sales_order purchase_order sales_quotation request_quotation part service assembly assortment letter);
+my @SUPPORTED_TYPES = qw(invoice invoice_for_advance_payment final_invoice credit_note customer vendor sales_delivery_order purchase_delivery_order sales_order purchase_order sales_quotation request_quotation part service assembly assortment letter),
+ @{ SL::DB::DeliveryOrder::TypeData::valid_types() },
+);
sub new {
my $class = shift;
$filters{numberfield} = "${type}number";
$filters{table} = $type;
- } elsif ($type =~ /_delivery_order$/) {
- $filters{trans_number} = "donumber";
- $filters{numberfield} = $type eq 'sales_delivery_order' ? "sdonumber" : "pdonumber";
+ } elsif ($type =~ /_delivery_order$/ && SL::DB::DeliveryOrder::TypeData::is_valid_type($type)) {
+ $filters{trans_number} = SL::DB::DeliveryOrder::TypeData::get3($type, 'properties', 'nr_key'),
+ $filters{numberfield} = SL::DB::DeliveryOrder::TypeData::get3($type, 'properties', 'transnumber'),
$filters{table} = "delivery_orders";
- $filters{where} = $type =~ /^sales/ ? '(customer_id IS NOT NULL)' : '(vendor_id IS NOT NULL)';
+ $filters{where} = "order_type = ?";
+ $filters{values} = [ $::form->{type} ];
} elsif ($type =~ /_order$/) {
$filters{trans_number} = "ordnumber";
my @values = ($self->number);
push @where, $filters{where} if $filters{where};
+ push @values, @{ $filters{values} } if $filters{values};
if ($self->id) {
push @where, qq|id <> ?|;
SQL
do_query($form, $self->dbh, "LOCK TABLE " . $filters{table}) || die $self->dbh->errstr;
- my %numbers_in_use = selectall_as_map($form, $self->dbh, $query, $filters{trans_number}, 'in_use');
+ my %numbers_in_use = selectall_as_map($form, $self->dbh, $query, $filters{trans_number}, 'in_use', @{ $filters{values} // [] });
my $business_number;
($business_number) = selectfirst_array_query($form, $self->dbh, qq|SELECT customernumberinit FROM business WHERE id = ? FOR UPDATE|, $self->business_id) if $self->business_id;
purchase_order => 'lieferantenbestellungen',
sales_delivery_order => 'verkaufslieferscheine',
purchase_delivery_order => 'einkaufslieferscheine',
+ supplier_delivery_order => 'beistelllieferscheine',
+ rma_delivery_order => 'retourenlieferscheine',
credit_note => 'gutschriften',
invoice => 'rechnungen',
invoice_for_advance_payment => 'rechnungen',
use List::Util qw(max sum);
use POSIX qw(strftime);
+use SL::Controller::DeliveryOrder;
use SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData qw(:types validate_type);
use SL::DO;
use SL::IR;
use SL::IS;
# end of main
sub check_do_access {
- $main::auth->assert($main::form->{type} . '_edit');
+ validate_type($::form->{type});
+
+ my $right = SL::DB::DeliveryOrder::TypeData::get($::form->{type}, "right");
+ $main::auth->assert($right);
}
sub set_headings {
'align' => 'center',
};
- $row->{donumber}->{link} = $edit_url . "&id=" . E($dord->{id}) . "&callback=${callback}";
+ $row->{donumber}->{link} = SL::DB::DeliveryOrder::TypeData::get3($dord->{order_type}, "show_menu", "new_controller")
+ ? SL::Controller::DeliveryOrder->url_for(action => "edit", id => $dord->{id}, type => $dord->{order_type})
+ : $edit_url . "&id=" . E($dord->{id}) . "&callback=${callback}";
$row->{ordnumber}->{link} = $edit_order_url . "&id=" . E($dord->{oe_id}) . "&callback=${callback}" if $dord->{oe_id};
$report->add_data($row);
use SL::DB::Contact;
use SL::DB::Currency;
use SL::DB::Customer;
+use SL::DB::DeliveryOrder::TypeData qw();
use SL::DB::Default;
use SL::DB::Language;
use SL::DB::Printer;
}
$obj->items(@items) if @items;
- $obj->is_sales(!!$obj->customer_id) if $class eq 'SL::DB::DeliveryOrder';
+
+ if ($class eq 'SL::DB::DeliveryOrder' && !$obj->order_type) {
+ $obj->order_type(SL::DB::DeliveryOrder::TypeData::validate_type($::form->{type}));
+ }
if ($class eq 'SL::DB::Invoice') {
my $paid = $factor *
--- /dev/null
+namespace('kivi.DeliveryOrder', function(ns) {
+ ns.check_cv = function() {
+ if ($('#type').val() == 'sales_delivery_order') {
+ if ($('#order_customer_id').val() === '') {
+ alert(kivi.t8('Please select a customer.'));
+ return false;
+ }
+ } else {
+ if ($('#order_vendor_id').val() === '') {
+ alert(kivi.t8('Please select a vendor.'));
+ return false;
+ }
+ }
+ return true;
+ };
+
+ ns.check_duplicate_parts = function(question) {
+ var id_arr = $('[name="order.orderitems[].parts_id"]').map(function() { return this.value; }).get();
+
+ var i, obj = {}, pos = [];
+
+ for (i = 0; i < id_arr.length; i++) {
+ var id = id_arr[i];
+ if (obj.hasOwnProperty(id)) {
+ pos.push(i + 1);
+ }
+ obj[id] = 0;
+ }
+
+ if (pos.length > 0) {
+ question = question || kivi.t8("Do you really want to continue?");
+ return confirm(kivi.t8("There are duplicate parts at positions") + "\n"
+ + pos.join(', ') + "\n"
+ + question);
+ }
+ return true;
+ };
+
+ ns.check_valid_reqdate = function() {
+ if ($('#order_reqdate_as_date').val() === '') {
+ alert(kivi.t8('Please select a delivery date.'));
+ return false;
+ } else {
+ return true;
+ }
+ };
+
+ ns.save = function(action, warn_on_duplicates, warn_on_reqdate) {
+ if (!ns.check_cv()) return;
+ if (warn_on_duplicates && !ns.check_duplicate_parts()) return;
+ if (warn_on_reqdate && !ns.check_valid_reqdate()) return;
+
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/' + action });
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.delete_order = function() {
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/delete' });
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.show_print_options = function(warn_on_duplicates, warn_on_reqdate) {
+ if (!ns.check_cv()) return;
+ if (warn_on_duplicates && !ns.check_duplicate_parts(kivi.t8("Do you really want to print?"))) return;
+ if (warn_on_reqdate && !ns.check_valid_reqdate()) return;
+
+ kivi.popup_dialog({
+ id: 'print_options',
+ dialog: {
+ title: kivi.t8('Print options'),
+ width: 800,
+ height: 300
+ }
+ });
+ };
+
+ ns.open_stock_in_out_dialog = function(clicked, in_out) {
+ var $row = $(clicked).parents("tbody").first();
+ var id = $row.find('[name="orderitem_ids[+]"]').val();
+ $row.uniqueId();
+
+ kivi.popup_dialog({
+ id: "stock_in_out_dialog",
+ url: "controller.pl?action=DeliveryOrder/stock_in_out_dialog",
+ data: {
+ id: $("#id").val(),
+ type: $("#type").val(),
+ parts_id: $row.find("[name$=parts_id]").val(),
+ unit: $row.find("[name$=unit]").val(),
+ qty_as_number: $row.find("[name$=qty_as_number]").val(),
+ stock: $row.find("[name$=stock_info]").val(),
+ item_id: id,
+ row: $row.attr("id"),
+ },
+ dialog: { title: kivi.t8('Transfer stock') }
+ });
+ };
+
+ ns.save_updated_stock = function() {
+ // stock information is saved in DOM as a yaml dump.
+ // we don't want to do this in javascript so we do a tiny roundtrip to the backend
+
+ let data = [];
+ $("#stock-in-out-table tr.listrow").each((i,row) => {
+ let qty = kivi.parse_amount($(row).find(".data-qty").val());
+
+ if (qty === 0) return;
+
+ data.push({
+ qty: qty,
+ warehouse_id: $(row).find(".data-warehouse-id").val(),
+ bin_id: $(row).find(".data-bin-id").val(),
+ chargenumber: $(row).find(".data-chargenumber").val(),
+ bestbefore: $(row).find(".data-bestbefore").val(),
+ unit: $(row).find(".data-unit").val(),
+ delivery_order_items_stock_id: $(row).find(".data-stock-id").val(),
+ });
+ });
+
+ let row = $(".data-row").val();
+
+ $.post("controller.pl",
+ kivi.serialize({
+ action: "DeliveryOrder/update_stock_information",
+ unit: $("#" + row).find("[name$=unit]").val(),
+ stock_info: data,
+ row: row
+ }),
+ (data) => {
+ $("#" + row + " .data-stock-info").val(data.stock_info);
+ $("#" + row + " .data-stock-qty").text(data.stock_qty)
+ $("#stock_in_out_dialog").dialog("close");
+ }
+ );
+ };
+
+ ns.print = function() {
+ $('#print_options').dialog('close');
+
+ var data = $('#order_form').serializeArray();
+ data = data.concat($('#print_options_form').serializeArray());
+ data.push({ name: 'action', value: 'DeliveryOrder/print' });
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ var email_dialog;
+
+ ns.setup_send_email_dialog = function() {
+ kivi.SalesPurchase.show_all_print_options_elements();
+ kivi.SalesPurchase.show_print_options_elements([ 'sendmode', 'media', 'copies', 'remove_draft' ], false);
+
+ $('#print_options_form table').first().remove().appendTo('#email_form_print_options');
+
+ var to_focus = $('#email_form_to').val() === '' ? 'to' : 'subject';
+ $('#email_form_' + to_focus).focus();
+ };
+
+ ns.finish_send_email_dialog = function() {
+ kivi.SalesPurchase.show_all_print_options_elements();
+
+ $('#email_form_print_options table').first().remove().prependTo('#print_options_form');
+ return true;
+ };
+
+ ns.show_email_dialog = function(html) {
+ var id = 'send_email_dialog';
+ var dialog_params = {
+ id: id,
+ width: 800,
+ height: 600,
+ title: kivi.t8('Send email'),
+ modal: true,
+ beforeClose: kivi.DeliveryOrder.finish_send_email_dialog,
+ close: function(event, ui) {
+ email_dialog.remove();
+ }
+ };
+
+ $('#' + id).remove();
+
+ email_dialog = $('<div style="display:none" id="' + id + '"></div>').appendTo('body');
+ email_dialog.html(html);
+ email_dialog.dialog(dialog_params);
+
+ kivi.DeliveryOrder.setup_send_email_dialog();
+
+ $('.cancel').click(ns.close_email_dialog);
+
+ return true;
+ };
+
+ ns.send_email = function() {
+ // push button only once -> slow response from mail server
+ ns.email_dialog_disable_send();
+
+ var data = $('#order_form').serializeArray();
+ data = data.concat($('[name^="email_form."]').serializeArray());
+ data = data.concat($('[name^="print_options."]').serializeArray());
+ data.push({ name: 'action', value: 'DeliveryOrder/send_email' });
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.email_dialog_disable_send = function() {
+ // disable mail send event to prevent
+ // impatient users to send multiple times
+ $('#send_email').prop('disabled', true);
+ };
+
+ ns.close_email_dialog = function() {
+ email_dialog.dialog("close");
+ };
+
+ ns.set_number_in_title = function(elt) {
+ $('#nr_in_title').html($(elt).val());
+ };
+
+ ns.reload_cv_dependent_selections = function() {
+ $('#order_shipto_id').val('');
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/customer_vendor_changed' });
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.reformat_number = function(event) {
+ $(event.target).val(kivi.format_amount(kivi.parse_amount($(event.target).val()), -2));
+ };
+
+ ns.reformat_number_as_null_number = function(event) {
+ if ($(event.target).val() === '') {
+ return;
+ }
+ ns.reformat_number(event);
+ };
+
+ ns.update_exchangerate = function(event) {
+ if (!ns.check_cv()) {
+ $('#order_currency_id').val($('#old_currency_id').val());
+ return;
+ }
+
+ var rate_input = $('#order_exchangerate_as_null_number');
+ // unset exchangerate if currency changed
+ if ($('#order_currency_id').val() !== $('#old_currency_id').val()) {
+ rate_input.val('');
+ }
+
+ // only set exchangerate if unset
+ if (rate_input.val() !== '') {
+ return;
+ }
+
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/update_exchangerate' });
+
+ $.ajax({
+ url: 'controller.pl',
+ data: data,
+ method: 'POST',
+ dataType: 'json',
+ success: function(data){
+ if (!data.is_standard) {
+ $('#currency_name').text(data.currency_name);
+ if (data.exchangerate) {
+ rate_input.val(data.exchangerate);
+ } else {
+ rate_input.val('');
+ }
+ $('#exchangerate_settings').show();
+ } else {
+ rate_input.val('');
+ $('#exchangerate_settings').hide();
+ }
+ if ($('#order_currency_id').val() != $('#old_currency_id').val() ||
+ !data.is_standard && data.exchangerate != $('#old_exchangerate').val()) {
+ kivi.display_flash('warning', kivi.t8('You have changed the currency or exchange rate. Please check prices.'));
+ }
+ $('#old_currency_id').val($('#order_currency_id').val());
+ $('#old_exchangerate').val(data.exchangerate);
+ }
+ });
+ };
+
+ ns.exchangerate_changed = function(event) {
+ if (kivi.parse_amount($('#order_exchangerate_as_null_number').val()) != kivi.parse_amount($('#old_exchangerate').val())) {
+ kivi.display_flash('warning', kivi.t8('You have changed the currency or exchange rate. Please check prices.'));
+ $('#old_exchangerate').val($('#order_exchangerate_as_null_number').val());
+ }
+ };
+
+ ns.unit_change = function(event) {
+ var row = $(event.target).parents("tbody").first();
+ var item_id_dom = $(row).find('[name="orderitem_ids[+]"]');
+ var sellprice_dom = $(row).find('[name="order.orderitems[].sellprice_as_number"]');
+ var select_elt = $(row).find('[name="order.orderitems[].unit"]');
+
+ var oldval = $(select_elt).data('oldval');
+ $(select_elt).data('oldval', $(select_elt).val());
+
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/unit_changed' },
+ { name: 'item_id', value: item_id_dom.val() },
+ { name: 'old_unit', value: oldval },
+ { name: 'sellprice_dom_id', value: sellprice_dom.attr('id') });
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.update_sellprice = function(item_id, price_str) {
+ var row = $('#item_' + item_id).parents("tbody").first();
+ var price_elt = $(row).find('[name="order.orderitems[].sellprice_as_number"]');
+ var html_elt = $(row).find('[name="sellprice_text"]');
+ price_elt.val(price_str);
+ html_elt.html(price_str);
+ };
+
+ ns.load_second_row = function(row) {
+ var item_id_dom = $(row).find('[name="orderitem_ids[+]"]');
+ var div_elt = $(row).find('[name="second_row"]');
+
+ if ($(div_elt).data('loaded') == 1) {
+ return;
+ }
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/load_second_rows' },
+ { name: 'item_ids[]', value: item_id_dom.val() });
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.load_all_second_rows = function() {
+ var rows = $('.row_entry').filter(function(idx, elt) {
+ return $(elt).find('[name="second_row"]').data('loaded') != 1;
+ });
+
+ var item_ids = $.map(rows, function(elt) {
+ var item_id = $(elt).find('[name="orderitem_ids[+]"]').val();
+ return { name: 'item_ids[]', value: item_id };
+ });
+
+ if (item_ids.length == 0) {
+ return;
+ }
+
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/load_second_rows' });
+ data = data.concat(item_ids);
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.hide_second_row = function(row) {
+ $(row).children().not(':first').hide();
+ $(row).data('expanded', 0);
+ var elt = $(row).find('.expand');
+ elt.attr('src', "image/expand.svg");
+ elt.attr('alt', kivi.t8('Show details'));
+ elt.attr('title', kivi.t8('Show details'));
+ };
+
+ ns.show_second_row = function(row) {
+ $(row).children().not(':first').show();
+ $(row).data('expanded', 1);
+ var elt = $(row).find('.expand');
+ elt.attr('src', "image/collapse.svg");
+ elt.attr('alt', kivi.t8('Hide details'));
+ elt.attr('title', kivi.t8('Hide details'));
+ };
+
+ ns.toggle_second_row = function(row) {
+ if ($(row).data('expanded') == 1) {
+ ns.hide_second_row(row);
+ } else {
+ ns.show_second_row(row);
+ }
+ };
+
+ ns.init_row_handlers = function() {
+ kivi.run_once_for('.reformat_number', 'on_change_reformat', function(elt) {
+ $(elt).change(ns.reformat_number);
+ });
+
+ kivi.run_once_for('.unitselect', 'on_change_unit_with_oldval', function(elt) {
+ $(elt).data('oldval', $(elt).val());
+ $(elt).change(ns.unit_change);
+ });
+
+ kivi.run_once_for('.row_entry', 'on_kbd_click_show_hide', function(elt) {
+ $(elt).keydown(function(event) {
+ var row;
+ if (event.keyCode == 40 && event.shiftKey === true) {
+ // shift arrow down
+ event.preventDefault();
+ row = $(event.target).parents(".row_entry").first();
+ ns.load_second_row(row);
+ ns.show_second_row(row);
+ return false;
+ }
+ if (event.keyCode == 38 && event.shiftKey === true) {
+ // shift arrow up
+ event.preventDefault();
+ row = $(event.target).parents(".row_entry").first();
+ ns.hide_second_row(row);
+ return false;
+ }
+ });
+ });
+
+ kivi.run_once_for('.expand', 'expand_second_row', function(elt) {
+ $(elt).click(function(event) {
+ event.preventDefault();
+ var row = $(event.target).parents(".row_entry").first();
+ ns.load_second_row(row);
+ ns.toggle_second_row(row);
+ return false;
+ })
+ });
+
+ };
+
+ ns.redisplay_line_values = function(is_sales, data) {
+ $('.row_entry').each(function(idx, elt) {
+ $(elt).find('[name="linetotal"]').html(data[idx][0]);
+ if (is_sales && $(elt).find('[name="second_row"]').data('loaded') == 1) {
+ var mt = data[idx][1];
+ var mp = data[idx][2];
+ var h = '<span';
+ if (mt[0] === '-') h += ' class="plus0"';
+ h += '>' + mt + ' ' + mp + '%';
+ h += '</span>';
+ $(elt).find('[name="linemargin"]').html(h);
+ }
+ });
+ };
+
+ ns.redisplay_cvpartnumbers = function(data) {
+ $('.row_entry').each(function(idx, elt) {
+ $(elt).find('[name="cvpartnumber"]').html(data[idx][0]);
+ });
+ };
+
+ ns.renumber_positions = function() {
+ $('.row_entry [name="position"]').each(function(idx, elt) {
+ $(elt).html(idx+1);
+ });
+ $('.row_entry').each(function(idx, elt) {
+ $(elt).data("position", idx+1);
+ });
+ };
+
+ ns.reorder_items = function(order_by) {
+ var dir = $('#' + order_by + '_header_id a img').attr("data-sort-dir");
+ $('#row_table_id thead a img').remove();
+
+ var src;
+ if (dir == "1") {
+ dir = "0";
+ src = "image/up.png";
+ } else {
+ dir = "1";
+ src = "image/down.png";
+ }
+
+ $('#' + order_by + '_header_id a').append('<img border=0 data-sort-dir=' + dir + ' src=' + src + ' alt="' + kivi.t8('sort items') + '">');
+
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/reorder_items' },
+ { name: 'order_by', value: order_by },
+ { name: 'sort_dir', value: dir });
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.redisplay_items = function(data) {
+ var old_rows = $('.row_entry').detach();
+ var new_rows = [];
+ $(data).each(function(idx, elt) {
+ new_rows.push(old_rows[elt.old_pos - 1]);
+ });
+ $(new_rows).appendTo($('#row_table_id'));
+ ns.renumber_positions();
+ };
+
+ ns.get_insert_before_item_id = function(wanted_pos) {
+ if (wanted_pos === '') return;
+
+ var insert_before_item_id;
+ // selection by data does not seem to work if data is changed at runtime
+ // var elt = $('.row_entry [data-position="' + wanted_pos + '"]');
+ $('.row_entry').each(function(idx, elt) {
+ if ($(elt).data("position") == wanted_pos) {
+ insert_before_item_id = $(elt).find('[name="orderitem_ids[+]"]').val();
+ return false;
+ }
+ });
+
+ return insert_before_item_id;
+ };
+
+ ns.add_item = function() {
+ if ($('#add_item_parts_id').val() === '') return;
+ if (!ns.check_cv()) return;
+
+ $('#row_table_id thead a img').remove();
+
+ var insert_before_item_id = ns.get_insert_before_item_id($('#add_item_position').val());
+
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/add_item' },
+ { name: 'insert_before_item_id', value: insert_before_item_id });
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.open_multi_items_dialog = function() {
+ if (!ns.check_cv()) return;
+
+ var pp = $("#add_item_parts_id").data("part_picker");
+ pp.o.multiple=1;
+ pp.open_dialog();
+ };
+
+ ns.add_multi_items = function(data) {
+ var insert_before_item_id = ns.get_insert_before_item_id($('#multi_items_position').val());
+ data = data.concat($('#order_form').serializeArray());
+ data.push({ name: 'action', value: 'DeliveryOrder/add_multi_items' },
+ { name: 'insert_before_item_id', value: insert_before_item_id });
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.delete_order_item_row = function(clicked) {
+ var row = $(clicked).parents("tbody").first();
+ $(row).remove();
+
+ ns.renumber_positions();
+ };
+
+ ns.row_table_scroll_down = function() {
+ $('#row_table_scroll_id').scrollTop($('#row_table_scroll_id')[0].scrollHeight);
+ };
+
+ ns.show_longdescription_dialog = function(clicked) {
+ var row = $(clicked).parents("tbody").first();
+ var position = $(row).find('[name="position"]').html();
+ var partnumber = $(row).find('[name="partnumber"]').html();
+ var description_elt = $(row).find('[name="order.orderitems[].description"]');
+ var longdescription_elt = $(row).find('[name="order.orderitems[].longdescription"]');
+
+ var params = {
+ runningnumber: position,
+ partnumber: partnumber,
+ description: description_elt.val(),
+ default_longdescription: longdescription_elt.val(),
+ set_function: function(val) {
+ longdescription_elt.val(val);
+ }
+ };
+
+ kivi.SalesPurchase.edit_longdescription_with_params(params);
+ };
+
+ ns.price_chooser_item_row = function(clicked) {
+ if (!ns.check_cv()) return;
+ var row = $(clicked).parents("tbody").first();
+ var item_id_dom = $(row).find('[name="orderitem_ids[+]"]');
+
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/price_popup' },
+ { name: 'item_id', value: item_id_dom.val() });
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.show_vc_details_dialog = function() {
+ if (!ns.check_cv()) return;
+ var vc;
+ var vc_id;
+ var title;
+ if ($('#order_customer_id').val()) {
+ vc = 'customer';
+ vc_id = $('#order_customer_id').val();
+ title = kivi.t8('Customer details');
+ } else {
+ vc = 'vendor';
+ vc_id = $('#order_vendor_id').val();
+ title = kivi.t8('Vendor details');
+ }
+
+ kivi.popup_dialog({
+ url: 'controller.pl',
+ data: { action: 'DeliveryOrder/show_customer_vendor_details_dialog',
+ type : $('#type').val(),
+ vc : vc,
+ vc_id : vc_id
+ },
+ id: 'jq_customer_vendor_details_dialog',
+ dialog: {
+ title: title,
+ width: 800,
+ height: 650
+ }
+ });
+ return true;
+ };
+
+ ns.update_row_from_master_data = function(clicked) {
+ var row = $(clicked).parents("tbody").first();
+ var item_id_dom = $(row).find('[name="orderitem_ids[+]"]');
+
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/update_row_from_master_data' });
+ data.push({ name: 'item_ids[]', value: item_id_dom.val() });
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.update_all_rows_from_master_data = function() {
+ var item_ids = $.map($('.row_entry'), function(elt) {
+ var item_id = $(elt).find('[name="orderitem_ids[+]"]').val();
+ return { name: 'item_ids[]', value: item_id };
+ });
+
+ if (item_ids.length == 0) {
+ return;
+ }
+
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/update_row_from_master_data' });
+ data = data.concat(item_ids);
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+ ns.show_calculate_qty_dialog = function(clicked) {
+ var row = $(clicked).parents("tbody").first();
+ var input_id = $(row).find('[name="order.orderitems[].qty_as_number"]').attr('id');
+ var formula_id = $(row).find('[name="formula[+]"]').attr('id');
+
+ calculate_qty_selection_dialog("", input_id, "", formula_id);
+ return true;
+ };
+
+ ns.edit_custom_shipto = function() {
+ if (!ns.check_cv()) return;
+
+ kivi.SalesPurchase.edit_custom_shipto();
+ };
+
+ ns.purchase_order_check_for_direct_delivery = function() {
+ if ($('#type').val() != 'sales_order') {
+ kivi.submit_form_with_action($('#order_form'), 'DeliveryOrder/purchase_order');
+ }
+
+ var empty = true;
+ var shipto;
+ if ($('#order_shipto_id').val() !== '') {
+ empty = false;
+ shipto = $('#order_shipto_id option:selected').text();
+ } else {
+ $('#shipto_inputs [id^="shipto"]').each(function(idx, elt) {
+ if (!empty) return true;
+ if (/^shipto_to_copy/.test($(elt).prop('id'))) return true;
+ if (/^shiptocp_gender/.test($(elt).prop('id'))) return true;
+ if (/^shiptocvar_/.test($(elt).prop('id'))) return true;
+ if ($(elt).val() !== '') {
+ empty = false;
+ return false;
+ }
+ });
+ var shipto_elements = [];
+ $([$('#shiptoname').val(), $('#shiptostreet').val(), $('#shiptozipcode').val(), $('#shiptocity').val()]).each(function(idx, elt) {
+ if (elt !== '') shipto_elements.push(elt);
+ });
+ shipto = shipto_elements.join('; ');
+ }
+
+ var use_it = false;
+ if (!empty) {
+ ns.direct_delivery_dialog(shipto);
+ } else {
+ kivi.submit_form_with_action($('#order_form'), 'DeliveryOrder/purchase_order');
+ }
+ };
+
+ ns.direct_delivery_callback = function(accepted) {
+ $('#direct-delivery-dialog').dialog('close');
+
+ if (accepted) {
+ $('<input type="hidden" name="use_shipto">').appendTo('#order_form').val('1');
+ }
+
+ kivi.submit_form_with_action($('#order_form'), 'DeliveryOrder/purchase_order');
+ };
+
+ ns.direct_delivery_dialog = function(shipto) {
+ $('#direct-delivery-dialog').remove();
+
+ var text1 = kivi.t8('You have entered or selected the following shipping address for this customer:');
+ var text2 = kivi.t8('Do you want to carry this shipping address over to the new purchase order so that the vendor can deliver the goods directly to your customer?');
+ var html = '<div id="direct-delivery-dialog"><p>' + text1 + '</p><p>' + shipto + '</p><p>' + text2 + '</p>';
+ html = html + '<hr><p>';
+ html = html + '<input type="button" value="' + kivi.t8('Yes') + '" size="30" onclick="kivi.DeliveryOrder.direct_delivery_callback(true)">';
+ html = html + ' ';
+ html = html + '<input type="button" value="' + kivi.t8('No') + '" size="30" onclick="kivi.DeliveryOrder.direct_delivery_callback(false)">';
+ html = html + '</p></div>';
+ $(html).hide().appendTo('#order_form');
+
+ kivi.popup_dialog({id: 'direct-delivery-dialog',
+ dialog: {title: kivi.t8('Carry over shipping address'),
+ height: 300,
+ width: 500 }});
+ };
+
+ ns.follow_up_window = function() {
+ var id = $('#id').val();
+ var type = $('#type').val();
+
+ var number_info = '';
+ if ($('#type').val() == 'sales_order' || $('#type').val() == 'purchase_order') {
+ number_info = $('#order_ordnumber').val();
+ } else if ($('#type').val() == 'sales_quotation' || $('#type').val() == 'request_quotation') {
+ number_info = $('#order_quonumber').val();
+ }
+
+ var name_info = '';
+ if ($('#type').val() == 'sales_order' || $('#type').val() == 'sales_quotation') {
+ name_info = $('#order_customer_id_name').val();
+ } else if ($('#type').val() == 'purchase_order' || $('#type').val() == 'request_quotation') {
+ name_info = $('#order_vendor_id_name').val();
+ }
+
+ var info = '';
+ if (number_info !== '') { info += ' (' + number_info + ')' }
+ if (name_info !== '') { info += ' (' + name_info + ')' }
+
+ if (!$('#follow_up_rowcount').lenght) {
+ $('<input type="hidden" name="follow_up_rowcount" id="follow_up_rowcount">').appendTo('#order_form');
+ $('<input type="hidden" name="follow_up_trans_id_1" id="follow_up_trans_id_1">').appendTo('#order_form');
+ $('<input type="hidden" name="follow_up_trans_type_1" id="follow_up_trans_type_1">').appendTo('#order_form');
+ $('<input type="hidden" name="follow_up_trans_info_1" id="follow_up_trans_info_1">').appendTo('#order_form');
+ $('<input type="hidden" name="follow_up_trans_subject_1" id="follow_up_trans_subject_1">').appendTo('#order_form');
+ }
+ $('#follow_up_rowcount').val(1);
+ $('#follow_up_trans_id_1').val(id);
+ $('#follow_up_trans_type_1').val(type);
+ $('#follow_up_trans_info_1').val(info);
+ $('#follow_up_trans_subject_1').val($('#order_transaction_description').val());
+
+ follow_up_window();
+ };
+
+ ns.create_part = function() {
+ var data = $('#order_form').serializeArray();
+ data.push({ name: 'action', value: 'DeliveryOrder/create_part' });
+
+ $.post("controller.pl", data, kivi.eval_json_result);
+ };
+
+});
+
+$(function() {
+ $('#order_customer_id').change(kivi.DeliveryOrder.reload_cv_dependent_selections);
+ $('#order_vendor_id').change(kivi.DeliveryOrder.reload_cv_dependent_selections);
+
+ $('#order_currency_id').change(kivi.DeliveryOrder.update_exchangerate);
+ $('#order_transdate_as_date').change(kivi.DeliveryOrder.update_exchangerate);
+ $('#order_exchangerate_as_null_number').change(kivi.DeliveryOrder.exchangerate_changed);
+
+ $('#add_item_parts_id').on('set_item:PartPicker', function(e,o) { $('#add_item_description').val(o.description) });
+ $('#add_item_parts_id').on('set_item:PartPicker', function(e,o) { $('#add_item_unit').val(o.unit) });
+
+ $('.add_item_input').keydown(function(event) {
+ if (event.keyCode == 13) {
+ event.preventDefault();
+ kivi.DeliveryOrder.add_item();
+ return false;
+ }
+ });
+
+ kivi.DeliveryOrder.init_row_handlers();
+
+ $('#row_table_id').on('sortstop', function(event, ui) {
+ $('#row_table_id thead a img').remove();
+ kivi.DeliveryOrder.renumber_positions();
+ });
+
+ $('#expand_all').on('click', function(event) {
+ event.preventDefault();
+ if ($('#expand_all').data('expanded') == 1) {
+ $('#expand_all').data('expanded', 0);
+ $('#expand_all').attr('src', 'image/expand.svg');
+ $('#expand_all').attr('alt', kivi.t8('Show all details'));
+ $('#expand_all').attr('title', kivi.t8('Show all details'));
+ $('.row_entry').each(function(idx, elt) {
+ kivi.DeliveryOrder.hide_second_row(elt);
+ });
+ } else {
+ $('#expand_all').data('expanded', 1);
+ $('#expand_all').attr('src', "image/collapse.svg");
+ $('#expand_all').attr('alt', kivi.t8('Hide all details'));
+ $('#expand_all').attr('title', kivi.t8('Hide all details'));
+ kivi.DeliveryOrder.load_all_second_rows();
+ $('.row_entry').each(function(idx, elt) {
+ kivi.DeliveryOrder.show_second_row(elt);
+ });
+ }
+ return false;
+ });
+
+ $('.reformat_number_as_null_number').change(kivi.DeliveryOrder.reformat_number_as_null_number);
+
+});
return kivi._shell_escape(elt);
}).join(' ');
};
+
+ ns.serialize = function(source, target = [], prefix, in_array = false) {
+ let arr_prefix = first => in_array ? (first ? "[+]" : "[]") : "";
+
+ if (Array.isArray(source) ) {
+ source.forEach(( val, i ) => {
+ ns.serialize(val, target, prefix + arr_prefix(i == 0), true);
+ });
+ } else if (typeof source === "object") {
+ let first = true;
+ for (let key in source) {
+ ns.serialize(source[key], target, (prefix !== undefined ? prefix + arr_prefix(first) + "." : "") + key);
+ first = false;
+ }
+ } else {
+ target.push({ name: prefix + arr_prefix(false), value: source });
+ }
+
+ return target;
+ };
});
kivi = namespace('kivi');
"Today":"heute",
"Toggle marker":"Markierung umschalten",
"Transaction description":"Vorgangsbezeichnung",
+"Transfer stock":"Lagerbewegungen",
"Tue":"Di",
"Tuesday":"Dienstag",
"Update":"Erneuern",
'Add Purchase Order' => 'Lieferantenauftrag erfassen',
'Add Quotation' => 'Angebot erfassen',
'Add RFQ' => 'Preisanfrage erfassen',
+ 'Add RMA Delivery Order' => 'Retouren-Lieferschein erfassen',
'Add Request for Quotation' => 'Anfrage erfassen',
'Add Requirement Spec' => 'Pflichtenheft erfassen',
'Add Requirement Spec Template' => 'Pflichtenheftvorlage erfassen',
'Add Sales Order' => 'Auftrag erfassen',
'Add Service' => 'Dienstleistung erfassen',
'Add Storno Credit Note' => 'Gutschrift Storno hinzufügen',
+ 'Add Supplier Delivery Order' => 'Beistell-Lieferschein erfassen',
'Add Transaction' => 'Dialogbuchen',
'Add User' => 'Neuer Benutzer',
'Add User Group' => 'Neue Benutzergruppe',
'Delivery Order Date' => 'Lieferscheindatum',
'Delivery Order Date missing!' => 'Lieferscheindatum fehlt!',
'Delivery Order Number' => 'Lieferscheinnummer',
+ 'Delivery Order Type' => 'Lieferschein Typ',
'Delivery Order created' => 'Lieferschein erstellt',
'Delivery Order deleted!' => 'Lieferschein gelöscht!',
+ 'Delivery Order has been deleted' => 'Lieferschein wurde gelöscht',
+ 'Delivery Order has been saved' => 'Lieferschein wurde gespeichert',
'Delivery Order(s) for full qty created' => 'Lieferschein(e) mit kompletter Menge erstellt',
'Delivery Orders' => 'Lieferscheine',
'Delivery Plan' => 'Lieferplan',
'Do you really want to mark the selected entries as booked?' => 'Möchten Sie die ausgewählten Einträge wirklich als gebucht markieren?',
'Do you really want to print?' => 'Wollen Sie wirklich drucken?',
'Do you really want to revert to this version?' => 'Möchten Sie wirklich auf diese Version zurücksetzen?',
+ 'Do you really want to transfer the stock and set this order to delivered?' => 'Wollen Sie wirklich alle Lagerbewegungen durchführen?',
'Do you really want to undo the selected SEPA exports? You have to reassign the export again.' => 'Möchten Sie wirklich die ausgewählten SEPA-Exports rückgängig machen? Der Export muss anschließend neu erzeugt werden.',
'Do you really want to unimport the selected documents?' => 'Möchten Sie wirklich diese Dateien an die Quelle zurückgeben?',
'Do you want to <b>limit</b> your search?' => 'Möchten Sie Ihre Suche <b>spezialisieren</b>?',
'Edit Purchase Delivery Order' => 'Lieferschein (Einkauf) bearbeiten',
'Edit Purchase Order' => 'Lieferantenauftrag bearbeiten',
'Edit Quotation' => 'Angebot bearbeiten',
+ 'Edit RMA Delivery Order' => 'Retouren-Lieferschein bearbeiten',
'Edit Request for Quotation' => 'Anfrage bearbeiten',
'Edit SEPA strings' => 'Begriffe bei SEPA-Überweisungen bearbeiten',
'Edit Sales Delivery Order' => 'Lieferschein (Verkauf) bearbeiten',
'Edit Storno Credit Note' => 'Storno Gutschrift bearbeiten',
'Edit Storno Invoice' => 'Stornorechnung bearbeiten',
'Edit Storno Invoice for Advance Payment' => 'Storno-Anzahlungsrechnung bearbeiten',
+ 'Edit Supplier Delivery Order' => 'Beistell-Lieferschein bearbeiten',
'Edit User' => 'Benutzerdaten bearbeiten',
'Edit User Group' => 'Benutzergruppe bearbeiten',
'Edit Vendor' => 'Lieferant editieren',
'Invoices with payments cannot be canceled.' => 'Rechnungen mit Zahlungen können nicht storniert werden.',
'Invoices, Credit Notes & AR Transactions' => 'Rechnungen, Gutschriften & Debitorenbuchungen',
'Is Searchable' => 'Durchsuchbar',
- 'Is sales' => 'Verkauf',
'Is this a summary account to record' => 'Sammelkonto für',
'It can be changed later but must be unique within the installation.' => 'Er ist nachträglich änderbar, muss aber im System eindeutig sein.',
'It is not allowed that a summary account occurs in a drop-down menu!' => 'Ein Sammelkonto darf nicht in Aufklappmenüs aufgenommen werden!',
'Name of the goal/source (if field names remote_name and remote_name_1 exist they will be combined into field "remote_name")' => 'Name des Ziel- oder Quellkontos (wenn die Spalten remote_name und remote_name_1 existieren werden diese zu Feld "remote_name" zusammengefügt)',
'Need a image title' => 'Benötige einen Titel für das Bild',
'Need a valid Shop Part for updating Part' => 'Benötige eine gültiges Shop Part Objekt, um den Artikel zu aktualisieren.',
+ 'Need a workflow for Supplier Delivery Order' => 'Benötige zwingend einen Workflow-Vorgänger für den Beistell-Lieferschein',
'Need at least one original position for the workflow Order to Delivery Order!' => 'Benötige mindestens eine Position die aus dem Auftrag übernommen wurde, ansonsten ist der Workflow inkosistent.',
'Need charge number!' => 'Benötige Chargennummer!',
'Negative reductions are possible to model price increases.' => 'Negative Abschläge sind möglich um Aufschläge zu modellieren.',
'No shipto city' => 'Die Stadt für die Lieferadresse fehlt',
'No shipto selected to delete' => 'Keine Lieferadresse zum Löschen ausgewählt',
'No start date given, setting to #1' => 'Kein Startdatum gegeben, setze Startdatum auf #1',
+ 'No stock to transfer' => 'Keine Lagerbewegungen vorhanden',
'No such job #1 in the database.' => 'Hintergrund-Job #1 existiert nicht mehr.',
'No summary account' => 'Kein Sammelkonto',
'No superuser credentials were entered.' => 'Es wurden keine Super-Benutzer-Anmeldedaten eingegeben.',
'Part Notes' => 'Bemerkungen',
'Part Number' => 'Artikelnummer',
'Part Number missing!' => 'Artikelnummer fehlt!',
- 'Part Test' => '',
+ 'Part Test' => 'Artikel-Test',
'Part Type' => 'Artikel-Typ',
'Part Unit' => 'Einheit',
'Part classifications' => 'Artikel-Klassifizierungen',
'RFQ Date' => 'Anfragedatum',
'RFQ Number' => 'Anfragenummer',
'RFQs' => 'Preisanfragen',
+ 'RMA Delivery Order' => 'Retouren-Lieferschein',
+ 'RMA Delivery Orders' => 'Retouren-Lieferscheine',
+ 'RMA delivery order' => 'Retouren-Lieferschein',
'ROP' => 'Mindestlagerbestand',
'Ranges of numbers' => 'Nummernkreise',
'Re-numbering all sections and function blocks in the order they are currently shown cannot be undone.' => 'Das Neu-Nummerieren aller Abschnitte und Funktionsblöcke kann nicht rückgängig gemacht werden.',
'Save and Quotation' => 'Speichern und Angebot',
'Save and RFQ' => 'Speichern und Lieferantenanfrage',
'Save and Sales Order' => 'Speichern und Kundenauftrag',
+ 'Save and Supplier Delivery Order' => 'Speichern und Beistelllieferschein',
'Save and close' => 'Speichern und schließen',
'Save and execute' => 'Speichern und ausführen',
'Save and keep open' => 'Speichern und geöffnet lassen',
'Stock Qty for Date' => 'Lagerbestand am',
'Stock for part #1' => 'Bestand für Artikel #1',
'Stock levels' => 'Lagerbestände',
+ 'Stock transfered' => 'Lagerbewegungen ausgeführt',
'Stock value' => 'Bestandswert',
'StockInfo' => 'Lagerinfo',
'Stocked Qty' => 'Lagermenge',
'Sun' => 'So',
'Sunday' => 'Sonntag',
'Superuser name' => 'Datenbankadministrator',
+ 'Supplier Delivery Order' => 'Beistell-Lieferschein',
+ 'Supplier Delivery Order has been deleted' => 'Beistell-Lieferschein wurde gelöscht',
+ 'Supplier Delivery Order has been saved' => 'Beistell-Lieferschein wurde gespeichert',
+ 'Supplier Delivery Orders' => 'Beistell-Lieferscheine',
+ 'Supplier delivery order' => 'Beistell-Lieferschein',
'Supplies' => 'Lieferungen',
'Surname' => 'Nachname',
'Switzerland' => 'Schweiz',
'The partnumber is missing.' => 'Die Artikelnummer fehlt.',
'The parts for this delivery order have already been transferred in.' => 'Die Artikel dieses Lieferscheins wurden bereits eingelagert.',
'The parts for this delivery order have already been transferred out.' => 'Die Artikel dieses Lieferscheins wurden bereits ausgelagert.',
+ 'The parts for this order have already been transferred' => 'Die Artikel in diesem Lieferschein wurden schon umgelagert',
'The parts have been removed.' => 'Die Waren wurden aus dem Lager entnommen.',
'The parts have been transferred.' => 'Die Waren wurden umgelagert.',
'The partsgroup is missing.' => 'Die Warengruppe fehlt.',
'Transfer out via default' => 'Auslagern über Standard-Lagerplatz',
'Transfer qty' => 'Umlagermenge',
'Transfer services via default' => 'Falls Ein- /Auslagern über Standardlagerplatz aktiviert ist, auch die Dienstleistungen standardmässig Ein- und Auslagern',
+ 'Transfer stock' => 'Lagerbewegungen',
'Transfer successful' => 'Lagervorgang erfolgreich',
'Transfer undone.' => 'Zurücklagerung erfolgreich',
'Transferred' => 'Übernommen',
'return_material' => 'Materialrückgabe',
'revert deleted' => 'löschen rückgängig',
'rfq_list' => 'anfragenliste',
+ 'rma_delivery_order_list' => 'lieferscheinliste_rma',
'running' => 'läuft',
'sales tax identification number' => 'USt-IdNr.',
'sales_delivery_order_list' => 'lieferscheinliste_verkauf',
'stocktaking' => 'Inventur',
'succeeded' => 'erfolgreich',
'sum' => 'Summe',
+ 'supplier_delivery_order_list' => 'lieferscheinliste_beistell',
'tax_chartaccno' => 'Automatikkonto',
'tax_percent' => 'Prozentsatz',
'tax_rate' => 'Prozent',
params:
action: search
type: purchase_delivery_order
+- parent: ap_reports
+ id: ap_reports_supplier_delivery_orders
+ name: Supplier Delivery Orders
+ order: 350
+ access: purchase_delivery_order_edit
+ module: do.pl
+ params:
+ action: search
+ type: supplier_delivery_order
- parent: ap_reports
id: ap_reports_vendor_invoices_ap_transactions
name: Vendor Invoices & AP Transactions
--- /dev/null
+-- @tag: deliveryorder_transnumbers
+-- @description: Nummernkreise für neue lieferscheintypen
+-- @depends: release_3_5_8
+
+ALTER TABLE defaults ADD COLUMN sudonumber TEXT;
+ALTER TABLE defaults ADD COLUMN rdonumber TEXT;
+
+UPDATE defaults SET
+ sudonumber = '0',
+ rdonumber = '0';
+
--- /dev/null
+-- @tag: deliveryorder_type
+-- @description: Persistente Typen in Lieferscheinen
+-- @depends: release_3_5_8
+
+ALTER TABLE delivery_orders ADD COLUMN order_type TEXT;
+
+UPDATE delivery_orders SET order_type = 'sales_delivery_order' WHERE customer_id IS NOT NULL;
+UPDATE delivery_orders SET order_type = 'purchase_delivery_order' WHERE vendor_id IS NOT NULL;
+
+ALTER TABLE delivery_orders ALTER COLUMN order_type SET NOT NULL;
+
+
--- /dev/null
+use Test::More;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Carp;
+use Data::Dumper;
+use Support::TestSetup;
+use Test::Exception;
+
+use SL::DB::Order;
+use SL::DB::Customer;
+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 SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData qw(:types);
+
+use SL::Dev::ALL qw(:ALL);
+
+Support::TestSetup::login();
+
+
+#######
+
+my $order1 = SL::Dev::Record::create_purchase_order(
+ save => 1,
+ taxincluded => 0,
+);
+
+my $delivery_order = SL::DB::DeliveryOrder->new_from($order1);
+
+is $delivery_order->type, PURCHASE_DELIVERY_ORDER_TYPE, "new_from purchase order gives purchase delivery order";
+is scalar @{ $delivery_order->items }, 2, "purchase delivery order keeps items";
+is $delivery_order->vendor_id, $order1->vendor_id, "purchase delivery order keeps vendor";
+
+my $supplier_delivery_order = SL::DB::DeliveryOrder->new_from($order1, type => SUPPLIER_DELIVERY_ORDER_TYPE);
+
+is $supplier_delivery_order->type, SUPPLIER_DELIVERY_ORDER_TYPE, "new_from purchase order with given type gives supplier delivery order";
+is scalar @{ $supplier_delivery_order->items }, 0, "supplier delivery order ignores items";
+is $supplier_delivery_order->vendor_id, $order1->vendor_id, "supplier delivery order keeps vendor";
+
+done_testing();
use SL::DB::Invoice;
use SL::DB::Order;
use SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData qw(:types);
use SL::DB::Part;
use SL::DB::Unit;
use SL::DB::TaxZone;
employee_id => $employee->id,
salesman_id => $employee->id,
taxzone_id => $taxzone->id,
+ order_type => SALES_DELIVERY_ORDER_TYPE,
%params,
)->save;
}
% einkaufslieferschein (purchase_delivery_order)
\newcommand{\einkaufslieferschein} {Eingangslieferschein}
+\newcommand{\beistelllieferschein} {Beistell-Lieferschein}
% Brief/letter
\newcommand{\ihrzeichen}{Ihr Zeichen}
% einkaufslieferschein (purchase_delivery_order)
\newcommand{\einkaufslieferschein} {Purchase delivery order}
+\newcommand{\beistelllieferschein} {Supplier Delivery Order}
% Brief/letter
\newcommand{\ihrzeichen}{Your reference}
% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
\input{insettings.tex}
+<%if template_meta.formname == "supplier_delivery_order"%>
+ \renewcommand{\einkaufslieferschein} {\beistelllieferschein}
+<%end%>
+
% laufende Kopfzeile:
\ourhead{}{}{\einkaufslieferschein}{<%donumber%>}{<%dodate%>}
--- /dev/null
+purchase_delivery_order.tex
\ No newline at end of file
--- /dev/null
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+<h1>[% FORM.title %] <span id='nr_in_title'>[%- SELF.order.number -%]</span></h1>
+
+<div id="print_options" style="display:none">
+ <form method="post" action="controller.pl" id="print_options_form">
+ [% SELF.print_options %]
+ <br>
+ [% L.button_tag('kivi.DeliveryOrder.print()', LxERP.t8('Print')) %]
+ <a href="#" onclick="$('#print_options').dialog('close');">[% LxERP.t8("Cancel") %]</a>
+ </form>
+</div>
+
+<div id="shipto_dialog" class="hidden"></div>
+
+<form method="post" action="controller.pl" id="order_form">
+ [% L.hidden_tag('callback', FORM.callback) %]
+ [% L.hidden_tag('type', FORM.type) %]
+ [% L.hidden_tag('id', SELF.order.id) %]
+ [% L.hidden_tag('converted_from_oe_id', SELF.converted_from_oe_id) %]
+
+ [%- INCLUDE 'common/flash.html' %]
+
+ <div class="tabwidget" id="order_tabs">
+ <ul>
+ <li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
+[%- IF INSTANCE_CONF.get_webdav %]
+ <li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
+[%- END %]
+[%- IF SELF.order.id AND INSTANCE_CONF.get_doc_storage %]
+ <li><a href="controller.pl?action=File/list&file_type=document&object_type=[% HTML.escape(FORM.type) %]&object_id=[% HTML.url(SELF.order.id) %]">[% 'Documents' | $T8 %]</a></li>
+ <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=[% HTML.escape(FORM.type) %]&object_id=[% HTML.url(SELF.order.id) %]">[% 'Attachments' | $T8 %]</a></li>
+[%- END %]
+[%- IF SELF.order.id %]
+ <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=DeliveryOrder&object_id=[% HTML.url(SELF.order.id) %]">[% 'Linked Records' | $T8 %]</a></li>
+[%- END %]
+ </ul>
+
+ [% PROCESS "delivery_order/tabs/basic_data.html" %]
+ [% PROCESS 'webdav/_list.html' %]
+ <div id="ui-tabs-1">
+ [%- LxERP.t8("Loading...") %]
+ </div>
+
+ <div id="shipto_inputs" class="hidden">
+ [%- PROCESS 'common/_ship_to_dialog.html'
+ vc_obj=SELF.order.customervendor
+ cs_obj=SELF.order.custom_shipto
+ cvars=SELF.order.custom_shipto.cvars_by_config
+ id_selector='#order_shipto_id' %]
+ </div>
+
+ </div>
+
+</form>
--- /dev/null
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+ [%- IF delivered %]
+ [%- SET RO = ' readonly' %]
+ [%- END %]
+
+ <table>
+ <tr>
+ <td>[% 'Part Number' | $T8 %]</td>
+ <td>[% part.partnumber | html %]</td>
+ </tr>
+ <tr>
+ <td>[% 'Description' | $T8 %]</td>
+ <td>[% part.description | html %]</td>
+ </tr>
+ <tr>
+ <td>[% 'Qty according to delivery order' | $T8 %]</td>
+ <td>[% LxERP.format_amount(do_qty) %] [% do_unit | html %]</td>
+ </tr>
+ </table>
+
+ [%- UNLESS WHCONTENTS.size %]
+ <p>[% 'There are no items in stock.' | $T8 %]</p>
+ [%- ELSE %]
+
+ [% L.hidden_tag("in_out", in_out) %]
+ [% L.hidden_tag("parts_id", parts_id) %]
+ [% L.hidden_tag("do_qty", do_qty) %]
+ [% L.hidden_tag("do_unit", do_unit) %]
+ [% L.hidden_tag("row", row, class="data-row") %]
+ [% L.hidden_tag("item_id", item_id) %]
+
+ <p>
+ <table id="stock-in-out-table">
+ <tr class="listheading">
+ <th> </th>
+ <th>[% 'Warehouse' | $T8 %]</th>
+ <th>[% 'Bin' | $T8 %]</th>
+ <th>[% 'Charge Number' | $T8 %]</th>
+ [% IF INSTANCE_CONF.get_show_bestbefore %]
+ <th>[% 'Best Before' | $T8 %]</th>
+ [% END %]
+ [%- UNLESS delivered %]
+ <th align="right">[% 'Available qty' | $T8 %]</th>
+ [%- END %]
+ <th align="right">[% 'Qty' | $T8 %]</th>
+ <th align="right">[% 'Unit' | $T8 %]</th>
+ </tr>
+
+ [%- FOREACH row = WHCONTENTS %]
+ <tr [% IF row.stock_error %] class="error"[% ELSE %]class="listrow"[% END %]>
+ <td>[% loop.count %]</td>
+ <td>[% row.warehousedescription | html %]</td>
+ <td>[% row.bindescription | html %]</td>
+ <td>[% row.chargenumber | html %]</td>
+ [% IF INSTANCE_CONF.get_show_bestbefore %]
+ <td>[% row.bestbefore | html %]</td>
+ [% END %]
+
+ [%- IF delivered %]
+
+ <td>[% LxERP.format_amount(row.stock_qty) | html %]</td>
+ <td>
+ [% row.stock_unit | html %]
+ [% L.hidden_tag("unit", row.stock_unit, class="data-unit") %]
+ </td>
+
+ [%- ELSE %]
+
+ <td>[% row.available_qty | html %]</td>
+ <td>
+ [% L.input_tag("qty", row.stock_qty ? LxERP.format_amount(row.stock_qty)
+ : (WHCONTENTS.size == 1) && (!row.stock_qty) ? LxERP.format_amount(do_qty)
+ : "", class="numeric data-qty", size="12") %]</td>
+ <td>[% L.select_tag("unit_" _ loop.count, part.unit_obj.convertible_units, value_key="name", default=row.stock_unit, class="data-unit") %]</td>
+
+ [%- END %]
+ <td style="display:none">
+ [% L.hidden_tag("warehouse_id", row.warehouse_id, class="data-warehouse-id") %]
+ [% L.hidden_tag("bin_id", row.bin_id, class="data-bin-id") %]
+ [% L.hidden_tag("chargenumber", row.chargenumber, class="data-chargenumber") %]
+ [% L.hidden_tag("delivery_order_items_stock_id", row.delivery_order_items_stock_id, class="data-stock-id") %]
+ [% L.hidden_tag("bestbefore", row.bestbefore, class="data-bestbefore") IF INSTANCE_CONF.get_show_bestbefore %]
+ </td>
+ </tr>
+
+ [%- END %]
+ </table>
+ </p>
+
+ <hr size="3" noshade>
+
+ <p>[% L.button_tag('kivi.DeliveryOrder.save_updated_stock()', LxERP.t8('Save')) IF !delivered %]</p>
+
+ [%- END %]
+ </form>
+
+
--- /dev/null
+[%- USE T8 %][%- USE HTML %]
+
+<tr id='business_info_row' [%- IF !SELF.order.customervendor.business_id %]style='display:none'[%- END %]>
+ <th align="right">[%- IF SELF.cv == 'customer' -%]
+ [%- 'Customer type' | $T8 -%]
+ [%- ELSE -%]
+ [%- 'Vendor type' | $T8 -%]
+ [%- END -%]</th>
+ <td>[% HTML.escape(SELF.order.customervendor.business.description) %]; [% 'Trade Discount' | $T8 %] [% SELF.order.customervendor.business.discount_as_percent %] %</td>
+</tr>
--- /dev/null
+[%- USE T8 %][%- USE HTML %][%- USE LxERP %][%- USE L %][%- USE P %]
+
+ <div>
+ <table id="input_row_table_id">
+ <thead>
+ <tr class="listheading">
+ <th class="listheading" nowrap >[%- '+' | $T8 %] </th>
+ <th class="listheading" nowrap >[%- 'position' | $T8 %] </th>
+ <th class="listheading" nowrap >[%- 'Part' | $T8 %] </th>
+ <th class="listheading" nowrap >[%- 'Description' | $T8 %] </th>
+ <th class="listheading" nowrap width="5" >[%- 'Qty' | $T8 %] </th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr valign="top" class="listrow">
+ <td class="tooltipster-html" title="[%- 'Create a new part' | $T8 -%]">
+ [% SET type_options = [[ 'part', LxERP.t8('Part') ], [ 'assembly', LxERP.t8('Assembly') ]] %]
+ [%- IF INSTANCE_CONF.get_feature_experimental_assortment %]
+ [%- type_options.push([ 'assortment', LxERP.t8('Assortment')]) %]
+ [%- END %]
+ [% L.select_tag('add_item.create_part_type', type_options) %]
+ [% L.button_tag('kivi.DeliveryOrder.create_part()', LxERP.t8('+')) %]
+ </td>
+ <td>[% L.input_tag('add_item.position', '', size = 5, class="add_item_input numeric") %]</td>
+ <td>
+ [%- SET PARAM_KEY = SELF.type_data.properties('is_customer') ? 'with_customer_partnumber' : 'with_makemodel' -%]
+ [%- SET PARAM_VAL = SELF.search_cvpartnumber -%]
+ [% P.part.picker('add_item.parts_id', SELF.created_part, style='width: 300px', class="add_item_input",
+ fat_set_item=1,
+ multiple_pos_input=1,
+ action={set_multi_items='kivi.DeliveryOrder.add_multi_items'},
+ classification_id=SELF.part_picker_classification_ids.as_list.join(','),
+ $PARAM_KEY=PARAM_VAL) %]</td>
+ <td>[% L.input_tag('add_item.description', SELF.created_part.description, class="add_item_input") %]</td>
+ <td>
+ [% L.input_tag('add_item.qty_as_number', '', size = 5, class="add_item_input numeric") %]
+ [% L.hidden_tag('add_item.unit', SELF.created_part.unit, class="add_item_input") %]
+ </td>
+ <td>[% L.button_tag('kivi.DeliveryOrder.add_item()', LxERP.t8('Add part')) %]</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
--- /dev/null
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+
+<tbody class="row_entry listrow" data-position="[%- HTML.escape(ITEM.position) -%]"[%- IF MYCONFIG.show_form_details -%] data-expanded="1"[%- END -%]>
+ <tr>
+ <td align="center">
+ [%- IF MYCONFIG.show_form_details %]
+ [% L.img_tag(src="image/collapse.svg",
+ alt=LxERP.t8('Hide details'), title=LxERP.t8('Hide details'), class="expand") %]
+ [%- ELSE %]
+ [% L.img_tag(src="image/expand.svg",
+ alt=LxERP.t8('Show details'), title=LxERP.t8('Show details'), class="expand") %]
+ [%- END %]
+ [% L.hidden_tag("orderitem_ids[+]", ID) %]
+ [% L.hidden_tag("converted_from_orderitems_ids[+]", ITEM.converted_from_orderitems_id) %]
+ [% L.hidden_tag("order.orderitems[+].id", ITEM.id, id='item_' _ ID) %]
+ [% L.hidden_tag("order.orderitems[].stock_info", ITEM.stock_info, class="data-stock-info") %]
+ [% L.hidden_tag("order.orderitems[].parts_id", ITEM.parts_id) %]
+ </td>
+ <td>
+ <div name="position" class="numeric">
+ [% HTML.escape(ITEM.position) %]
+ </div>
+ </td>
+ <td align="center">
+ <img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop">
+ </td>
+ <td align="center">
+ [%- L.button_tag("kivi.DeliveryOrder.delete_order_item_row(this)",
+ LxERP.t8("X"),
+ confirm=LxERP.t8("Are you sure?")) %]
+ </td>
+ [%- IF SELF.show_update_button -%]
+ <td align="center">
+ [%- L.img_tag(src="image/rotate_cw.svg",
+ alt=LxERP.t8('Update from master data'),
+ title= LxERP.t8('Update from master data'),
+ onclick="if (!confirm('" _ LxERP.t8("Are you sure to update this position from master data?") _ "')) return false; kivi.DeliveryOrder.update_row_from_master_data(this);",
+ id='update_from_master') %]
+ </td>
+ [%- END -%]
+ <td>
+ <div name="partnumber">
+ [%- P.link_tag(SELF.url_for(controller='Part', action='edit', 'part.id'=ITEM.part.id), ITEM.part.partnumber, target="_blank", title=LxERP.t8('Open in new window')) -%]
+ </div>
+ </td>
+ [%- IF SELF.search_cvpartnumber -%]
+ <td>
+ <div name="cvpartnumber">[% HTML.escape(ITEM.cvpartnumber) %]</div>
+ </td>
+ [%- END -%]
+ <td>
+ <div name="partclassification">[% ITEM.part.presenter.typeclass_abbreviation %]</div>
+ </td>
+ <td>
+ [% L.areainput_tag("order.orderitems[].description",
+ ITEM.description,
+ size='40',
+ style='width: 300px') %]
+ [%- L.hidden_tag("order.orderitems[].longdescription", ITEM.longdescription) %]
+ [%- L.button_tag("kivi.DeliveryOrder.show_longdescription_dialog(this)", LxERP.t8("L")) %]
+ </td>
+ [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%]
+ <td nowrap>
+ [%- L.div_tag(LxERP.format_amount(ITEM.shipped_qty, 2, 0) _ ' ' _ ITEM.unit, name="shipped_qty", class="numeric") %]
+ </td>
+ [%- END -%]
+ <td nowrap>
+ [%- L.input_tag("order.orderitems[].qty_as_number",
+ ITEM.qty_as_number,
+ size = 5,
+ class="reformat_number numeric") %]
+ [%- IF ITEM.part.formel -%]
+ [%- L.button_tag("kivi.DeliveryOrder.show_calculate_qty_dialog(this)", LxERP.t8("*/")) %]
+ [%- L.hidden_tag("formula[+]", ITEM.part.formel) -%]
+ [%- END -%]
+ </td>
+ <td nowrap>
+ [%- L.select_tag("order.orderitems[].unit",
+ ITEM.part.available_units,
+ default = ITEM.unit,
+ title_key = 'name',
+ value_key = 'name',
+ class = 'unitselect') %]
+ </td>
+
+ <td>
+ <span id="stock_[% ID %]" class="data-stock-qty">[% SELF.calculate_stock_in_out(ITEM) %] [% ITEM.unit %]</span>
+ [% P.button_tag("kivi.DeliveryOrder.open_stock_in_out_dialog(this, '" _ in_out _"')", "?") %]
+ </td>
+ </tr>
+
+ <tr [%- IF !MYCONFIG.show_form_details -%]style="display:none"[%- END -%]>
+ <td colspan="100%">
+ [%- IF MYCONFIG.show_form_details || ITEM.render_second_row %]
+ <div name="second_row" data-loaded="1">
+ [%- PROCESS delivery_order/tabs/_second_row.html ITEM=ITEM TYPE=SELF.type %]
+ </div>
+ [%- ELSE %]
+ <div name="second_row" id="second_row_[% ID %]">
+ [%- LxERP.t8("Loading...") %]
+ </div>
+ [%- END %]
+ </td>
+ </tr>
+
+</tbody>
--- /dev/null
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+
+<table>
+ <tr><td colspan="100%">
+ [%- IF (TYPE == "sales_order" || TYPE == "purchase_order") %]
+ <b>[%- 'Serial No.' | $T8 %]</b>
+ [%- L.input_tag("order.orderitems[].serialnumber", ITEM.serialnumber, size = 15) %]
+ [%- END %]
+ <b>[%- 'Project' | $T8 %]</b>
+ [% P.project.picker("order.orderitems[].project_id", ITEM.project_id, size = 15) %]
+ [%- IF (TYPE == "sales_order" || TYPE == "purchase_order") %]
+ <b>[%- 'Reqdate' | $T8 %]</b>
+ [% L.date_tag("order.orderitems[].reqdate_as_date", ITEM.reqdate_as_date) %]
+ [%- END %]
+ <b>[%- 'On Hand' | $T8 %]</b>
+ <span[%- IF ITEM.part.onhand < ITEM.part.rop -%] class="numeric plus0"[%- END -%]>
+ [%- ITEM.part.onhand_as_number -%] [%- ITEM.part.unit -%]
+ </span>
+ </td></tr>
+
+ <tr>
+ [%- SET n = 0 %]
+ [%- FOREACH var = ITEM.cvars_by_config %]
+ [%- NEXT UNLESS (var.config.processed_flags.editable && ITEM.part.cvar_by_name(var.config.name).is_valid) %]
+ [%- SET n = n + 1 %]
+ <th>
+ [% var.config.description %]
+ </th>
+ <td>
+ [% L.hidden_tag('order.orderitems[].custom_variables[+].config_id', var.config.id) %]
+ [% L.hidden_tag('order.orderitems[].custom_variables[].id', var.id) %]
+ [% L.hidden_tag('order.orderitems[].custom_variables[].sub_module', var.sub_module) %]
+ [% INCLUDE 'common/render_cvar_input.html' var_name='order.orderitems[].custom_variables[].unparsed_value' %]
+ </td>
+ [%- IF (n % (MYCONFIG.form_cvars_nr_cols || 3)) == 0 %]
+
+ </tr><tr>
+ [%- END %]
+ [%- END %]
+ </tr>
+</table>
--- /dev/null
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+
+[%- INCLUDE 'generic/set_longdescription.html' %]
+
+<div id="ui-tabs-basic-data">
+ <table width="100%">
+ <tr valign="top">
+ <td>
+ <table width="100%">
+ <tr>
+ <th align="right">[%- SELF.cv == "customer" ? LxERP.t8('Customer') : LxERP.t8('Vendor') -%]</th>
+ [% SET cv_id = SELF.cv _ '_id' %]
+ <td>
+ [% P.customer_vendor.picker("order.${SELF.cv}" _ '_id', SELF.order.$cv_id, type=SELF.cv, style='width: 300px') %]
+ [% P.button_tag("kivi.DeliveryOrder.show_vc_details_dialog()", LxERP.t8("Details (one letter abbreviation)")) %]
+ </td>
+ </tr>
+
+ <tr id='cp_row' [%- IF !SELF.order.${SELF.cv}.contacts.size %]style='display:none'[%- END %]>
+ <th align="right">[% 'Contact Person' | $T8 %]</th>
+ <td>[% L.select_tag('order.cp_id',
+ SELF.order.${SELF.cv}.contacts,
+ default=SELF.order.cp_id,
+ title_key='full_name_dep',
+ value_key='cp_id',
+ with_empty=1,
+ style='width: 300px') %]</td>
+ </tr>
+
+ <tr>
+ <th align="right">[% 'Shipping Address' | $T8 %]</th>
+ <td>
+ <span id='shipto_selection' [%- IF !SELF.order.${SELF.cv}.shipto.size %]style='display:none'[%- END %]>
+ [% shiptos = [ { shipto_id => "", displayable_id => LxERP.t8("No/individual shipping address") } ] ;
+ FOREACH s = SELF.order.${SELF.cv}.shipto ;
+ shiptos.push(s) ;
+ END ;
+ L.select_tag('order.shipto_id',
+ shiptos,
+ default=SELF.order.shipto_id,
+ title_key='displayable_id',
+ value_key='shipto_id',
+ with_empty=0,
+ style='width: 300px') %]
+ </span>
+ [% L.button_tag("kivi.DeliveryOrder.edit_custom_shipto()", LxERP.t8("Custom shipto")) %]
+ </td>
+ </tr>
+
+ [%- PROCESS delivery_order/tabs/_business_info_row.html SELF=SELF %]
+
+[%- IF SELF.all_languages.size %]
+ <tr>
+ <th align="right">[% 'Language' | $T8 %]</th>
+ <td>
+ [% L.select_tag('order.language_id', SELF.all_languages, default=SELF.order.language_id, title_key='description', with_empty=1, style='width:300px') %]
+ </td>
+ </tr>
+[%- END %]
+
+[%- IF SELF.all_departments.size %]
+ <tr>
+ <th align="right">[% 'Department' | $T8 %]</th>
+ <td>
+ [% L.select_tag('order.department_id', SELF.all_departments, default=SELF.order.department_id, title_key='description', with_empty=1, style='width:300px') %]
+ </td>
+ </tr>
+[%- END %]
+
+ <tr>
+ <th align="right">[% 'Shipping Point' | $T8 %]</th>
+ <td>[% L.input_tag('order.shippingpoint', SELF.order.shippingpoint, style='width: 300px') %]</td>
+ </tr>
+
+ <tr>
+ <th align="right">[% 'Ship via' | $T8 %]</th>
+ <td>[% L.input_tag('order.shipvia', SELF.order.shipvia, style='width: 300px') %]</td>
+ </tr>
+
+ <tr>
+ <th align="right">[% 'Transaction description' | $T8 %]</th>
+ <td>[% L.input_tag('order.transaction_description', SELF.order.transaction_description, 'data-validate'=INSTANCE_CONF.get_require_transaction_description_ps ? 'required' : '', style='width: 300px') %]</td>
+ </tr>
+
+ <tr>
+ <th align="right">[% 'Project Number' | $T8 %]</th>
+ <td>[% P.project.picker('order.globalproject_id', SELF.order.globalproject_id, style='width: 300px') %]</td>
+ </tr>
+
+ </table>
+ </td>
+
+ <td align="right">
+ <table>
+
+ <tr>
+ <td colspan="2" align="center" id="data-status-line">[% SELF.order.presenter.status_line %]</td>
+ </tr>
+
+ <tr>
+ <th align="right">[% 'Employee' | $T8 %]</th>
+ <td>[% L.select_tag('order.employee_id',
+ SELF.all_employees,
+ default=(SELF.order.employee_id ? SELF.order.employee_id : SELF.current_employee_id),
+ title_key='safe_name') %]</td>
+ </tr>
+
+ [% IF SELF.cv == 'customer' %]
+ <tr>
+ <th align="right">[% 'Salesman' | $T8 %]</th>
+ <td>[% L.select_tag('order.salesman_id',
+ SELF.all_salesmen,
+ default=(SELF.order.salesman_id ? SELF.order.salesman_id : SELF.current_employee_id),
+ title_key='safe_name') %]</td>
+ </tr>
+ [% END %]
+
+ <tr>
+ <th width="70%" align="right" nowrap>[% 'Delivery Order Number' | $T8 %]</th>
+ <td>[% L.input_tag('order.donumber', SELF.order.donumber, size = 11, onchange='kivi.DeliveryOrder.set_number_in_title(this)') %]</td>
+ </tr>
+
+ <tr>
+ <th width="70%" align="right" nowrap>[% 'Order Number' | $T8 %]</th>
+ <td>[% L.input_tag('order.ordnumber', SELF.order.ordnumber, size = 11) %]</td>
+ </tr>
+
+ <tr>
+ <th width="70%" align="right" nowrap>[% IF SELF.type_data.properties('is_customer') %][% 'Customer Order Number' | $T8 %][% ELSE %][% 'Vendor Order Number' | $T8 %][% END %]</th>
+ <td>[% L.input_tag('order.cusordnumber', SELF.order.cusordnumber, size = 11) %]</td>
+ </tr>
+
+ <tr>
+ <th width="70%" align="right" nowrap>[% 'Delivery Order Date' | $T8 %]</th>
+ <td>[% L.date_tag('order.transdate_as_date', SELF.order.transdate_as_date) %]</td>
+ </tr>
+
+ <tr>
+ <th width="70%" align="right" nowrap>[% 'Reqdate' | $T8 %]</th>
+ <td>[% L.date_tag('order.reqdate_as_date', SELF.order.reqdate_as_date, class=reqdate_class) %]</td>
+ </tr>
+
+ <tr>
+ <th width="70%" align="right" nowrap>[% 'Insert Date' | $T8 %]</th>
+ <td>[% SELF.order.itime_as_date %]</td>
+ </tr>
+ </table>
+
+ </td>
+ </tr>
+ </table>
+
+ [%- PROCESS delivery_order/tabs/_item_input.html SELF=SELF %]
+
+ [% L.button_tag('kivi.DeliveryOrder.open_multi_items_dialog()', LxERP.t8('Add multiple items')) %]
+
+ <table width="100%">
+ <tr>
+ <td>
+ [%- IF SELF.positions_scrollbar_height -%]
+ [%- SET scroll_style = 'style="overflow-y: auto; height:' _ SELF.positions_scrollbar_height _ 'vh;"' -%]
+ [%- ELSE -%]
+ [%- SET scroll_style = '' -%]
+ [%- END -%]
+ <div id="row_table_scroll_id" [%- scroll_style -%]>
+ <table id="row_table_id" width="100%">
+ <thead>
+ <tr class="listheading">
+ <th class="listheading" style='text-align:center' nowrap width="1">
+ [%- IF MYCONFIG.show_form_details %]
+ [%- L.img_tag(src="image/collapse.svg", alt=LxERP.t8('Hide all details'), title=LxERP.t8('Hide all details'), id='expand_all', "data-expanded"="1") %]
+ [%- ELSE %]
+ [%- L.img_tag(src="image/expand.svg", alt=LxERP.t8('Show all details'), title=LxERP.t8('Show all details'), id='expand_all') %]
+ [%- END %]
+ </th>
+ <th class="listheading" nowrap width="3" >[%- 'position' | $T8 %] </th>
+ <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></th>
+ <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/close.png" alt="[%- LxERP.t8('delete item') %]"></th>
+ [%- IF SELF.show_update_button -%]
+ <th class="listheading" style='text-align:center' nowrap width="1">
+ [%- L.img_tag(src="image/rotate_cw.svg",
+ alt=LxERP.t8('Update from master data'),
+ title= LxERP.t8('Update from master data'),
+ onclick="if (!confirm('" _ LxERP.t8("Are you sure to update all positions from master data?") _ "')) return false; kivi.DeliveryOrder.update_all_rows_from_master_data();",
+ id='update_from_master') %]
+ </th>
+ [%- END %]
+ <th id="partnumber_header_id" class="listheading" nowrap width="15"><a href='#' onClick='javascript:kivi.DeliveryOrder.reorder_items("partnumber")'> [%- 'Partnumber' | $T8 %]</a></th>
+ [%- IF SELF.search_cvpartnumber -%]
+ <th id="cvpartnumber_header_id" class="listheading" nowrap width="15"><a href='#' onClick='javascript:kivi.DeliveryOrder.reorder_items("cvpartnumber")' > [%- SELF.cv == "customer" ? LxERP.t8('Customer Part Number') : LxERP.t8('Model') %]</a></th>
+ [%- END -%]
+ <th id="partclass_header_id" class="listheading" nowrap width="2">[%- 'Type' | $T8 %]</th>
+ <th id="description_header_id" class="listheading" nowrap ><a href='#' onClick='javascript:kivi.DeliveryOrder.reorder_items("description")'>[%- 'Description' | $T8 %]</a></th>
+ [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%]
+ <th id="shipped_qty_header_id" class="listheading" nowrap width="5" ><a href='#' onClick='javascript:kivi.DeliveryOrder.reorder_items("shipped_qty")'>[%- 'Delivered' | $T8 %]</a></th>
+ [%- END -%]
+ <th id="qty_header_id" class="listheading" nowrap width="5" ><a href='#' onClick='javascript:kivi.DeliveryOrder.reorder_items("qty")'> [%- 'Qty' | $T8 %]</a></th>
+ <th class="listheading" nowrap width="5" >[%- 'Unit' | $T8 %] </th>
+ [% IF in_out == 'in' %]
+ <th class="listheading" nowrap width="5" >[%- 'Transfer To Stock' | $T8 %] </th>
+ [% END %]
+ [% IF in_out == 'out' %]
+ <th class="listheading" nowrap width="5" >[%- 'Release From Stock' | $T8 %] </th>
+ [% END %]
+ </tr>
+ </thead>
+
+ [%- FOREACH item = SELF.order.items_sorted %]
+ [%- PROCESS delivery_order/tabs/_row.html ITEM=item ID=(item.id||item.new_fake_id) -%]
+ [%- END %]
+
+ </table>
+ </div>
+
+ </td>
+ </tr>
+
+ <tr>
+ </tr>
+
+ <tr>
+ <td colspan="100%" width="100%">
+ <table width="100%">
+ <tr>
+ <td>
+ <table>
+ <tr>
+ <th align="left">[% 'Notes' | $T8 %]</th>
+ <th align="left">[% 'Internal Notes' | $T8 %]</th>
+ </tr>
+ <tr valign="top">
+ <td>
+ [% L.textarea_tag('order.notes', SELF.order.notes, wrap="soft", style="width: 350px; height: 150px", class="texteditor") %]
+ </td>
+ <td>
+ [% L.textarea_tag('order.intnotes', SELF.order.intnotes, wrap="soft", style="width: 350px; height: 150px") %]
+ </td>
+ </tr>
+ </table>
+ </td>
+
+ <td>
+ <table>
+ <tr>
+ <th align="right">[% 'Payment Terms' | $T8 %]</th>
+ <td>[% L.select_tag('order.payment_id',
+ SELF.all_payment_terms,
+ default = SELF.order.payment_id,
+ with_empty = 1,
+ title_key = 'description',
+ style = 'width: 250px') %]</td>
+ </tr>
+ <tr>
+ <th align="right">[% 'Delivery Terms' | $T8 %]</th>
+ <td>[% L.select_tag('order.delivery_term_id',
+ SELF.all_delivery_terms,
+ default = SELF.order.delivery_term_id,
+ with_empty = 1,
+ title_key = 'description',
+ style = 'width: 250px') %]</td>
+ </tr>
+ </table>
+ </td>
+
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ </table>
+
+ [% L.hidden_tag('order.taxzone_id', SELF.order.taxzone_id) %]
+
+</div>
+
+[% L.sortable_element('#row_table_id') %]