1 package SL::Controller::DeliveryOrder;
4 use parent qw(SL::Controller::Base);
6 use SL::Helper::Flash qw(flash_later);
7 use SL::Helper::Number qw(_format_number _parse_number);
8 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
9 use SL::Presenter::DeliveryOrder qw(delivery_order_status_line);
10 use SL::Locale::String qw(t8);
11 use SL::SessionFile::Random;
16 use SL::Util qw(trim);
18 use SL::DBUtils qw(selectall_hashref_query);
23 use SL::DB::Order::TypeData qw(:types);
25 use SL::DB::PartClassification;
26 use SL::DB::PartsGroup;
29 use SL::DB::Reclamation;
30 use SL::DB::Reclamation::TypeData qw(:types);
31 use SL::DB::RecordLink;
33 use SL::DB::Translation;
34 use SL::DB::TransferType;
35 use SL::DB::ValidityToken;
36 use SL::DB::EmailJournal;
37 use SL::DB::Warehouse;
39 use SL::DB::Helper::RecordLink qw(set_record_link_conversions RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
40 use SL::DB::Helper::TypeDataProxy;
41 use SL::DB::Helper::Record qw(get_object_name_from_type get_class_from_type);
42 use SL::DB::DeliveryOrder;
43 use SL::DB::DeliveryOrder::TypeData qw(:types);
44 use SL::DB::Manager::DeliveryOrderItem;
45 use SL::DB::DeliveryOrderItemsStock;
46 use SL::Model::Record;
48 use SL::Helper::CreatePDF qw(:all);
49 use SL::Helper::PrintOptions;
50 use SL::Helper::ShippedQty;
51 use SL::Helper::Inventory;
52 use SL::Helper::DateTime;
53 use SL::Helper::UserPreferences::DisplayPreferences;
54 use SL::Helper::UserPreferences::PositionsScrollbar;
55 use SL::Helper::UserPreferences::UpdatePositions;
57 use SL::Controller::Helper::GetModels;
59 use List::Util qw(first sum0);
60 use List::UtilsBy qw(sort_by uniq_by);
61 use List::MoreUtils qw(any none pairwise first_index);
62 use English qw(-no_match_vars);
67 use Rose::Object::MakeMethods::Generic (
68 scalar => [qw(item_ids_to_delete is_custom_shipto_to_delete)],
69 'scalar --get_set_init' => [ qw(
70 order valid_types type cv p all_price_factors search_cvpartnumber
71 show_update_button part_picker_classification_ids type_data
77 __PACKAGE__->run_before('check_auth',
79 update_stock_information
82 __PACKAGE__->run_before('check_auth_for_edit',
84 update_stock_information edit
85 stock_in_out_dialog load_second_rows
88 __PACKAGE__->run_before('get_unalterable_data',
90 save save_as_new workflow_new_record workflow_invoice
91 save_and_ap_transaction print send_email
104 if (!$::form->{form_validity_token}) {
105 $::form->{form_validity_token} = SL::DB::ValidityToken->create(
106 scope => SL::DB::ValidityToken::SCOPE_DELIVERY_ORDER_SAVE()
111 'delivery_order/form',
112 title => $self->get_title_for('add'),
113 %{$self->{template_args}}
117 sub action_add_from_record {
119 my $from_type = $::form->{from_type};
120 my $from_id = $::form->{from_id};
122 die "No 'from_type' was given." unless ($from_type);
123 die "No 'from_id' was given." unless ($from_id);
126 if (defined($::form->{from_item_ids})) {
127 my %use_item = map { $_ => 1 } @{$::form->{from_item_ids}};
128 $flags{item_filter} = sub {
130 return %use_item{$item->{RECORD_ITEM_ID()}};
134 my $record = SL::Model::Record->get_record($from_type, $from_id);
136 # If we are coming from an order workflow, only consider not delivered
138 if (ref $record eq 'SL::DB::Order') {
139 # Calculate shipped qtys here to prevent calling calculate for every item
140 # via the items method.
141 SL::Helper::ShippedQty->new->calculate($record)->write_to(\@{$record->items});
143 my @items_with_not_delivered_qty =
145 map {$_->qty($_->qty - $_->shipped_qty); $_}
146 @{$record->items_sorted};
148 $flags{items} = \@items_with_not_delivered_qty;
151 my $delivery_order = SL::Model::Record->new_from_workflow($record, $self->type, %flags);
152 $self->order($delivery_order);
153 $self->reinit_after_new_order();
158 sub action_add_from_email_journal {
160 die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
165 sub action_edit_with_email_journal_workflow {
167 die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
168 $::form->{workflow_email_journal_id} = delete $::form->{email_journal_id};
169 $::form->{workflow_email_attachment_id} = delete $::form->{email_attachment_id};
170 $::form->{workflow_email_callback} = delete $::form->{callback};
172 $self->action_edit();
175 # edit an existing order
178 die "No 'id' was given." unless $::form->{id};
184 'delivery_order/form',
185 title => $self->get_title_for('edit'),
186 %{$self->{template_args}}
194 SL::Model::Record->delete($self->order);
195 flash_later('info', $self->type_data->text("delete"));
197 my @redirect_params = (
202 $self->redirect_to(@redirect_params);
209 if ( $self->order->delivered ) {
210 $self->js->flash('error', t8('This record has already been delivered.'));
211 return $self->js->render();
216 flash_later('info', $self->type_data->text("saved"));
219 if ($::form->{back_to_caller}) {
220 @redirect_params = $::form->{callback} ? ($::form->{callback})
221 : (controller => 'LoginScreen', action => 'user_login');
227 id => $self->order->id,
228 callback => $::form->{callback},
232 $self->redirect_to(@redirect_params);
235 # save the order as new document an open it for edit
236 sub action_save_as_new {
239 my $order = $self->order;
242 $self->js->flash('error', t8('This object has not been saved yet.'));
243 return $self->js->render();
246 my $saved_order = SL::DB::DeliveryOrder->new(id => $order->id)->load;
248 # Create new record from current one
249 my $new_order = SL::Model::Record->clone_for_save_as_new($saved_order, $order);
250 $self->order($new_order);
252 if (!$::form->{form_validity_token}) {
253 $::form->{form_validity_token} = SL::DB::ValidityToken->create(
254 scope => SL::DB::ValidityToken::SCOPE_DELIVERY_ORDER_SAVE()
259 $self->action_save();
262 # close a already saved order (potentially already delivered)
263 sub action_close_order {
266 $self->order->update_attributes(
271 ->flash("info", t8("The record has been closed."))
272 ->run('kivi.ActionBar.setDisabled', '#close_order',
273 t8('This record has already been closed.'))
274 ->html('#data-status-line', delivery_order_status_line($self->order))
280 # This is called if "print" is pressed in the print dialog.
281 # If PDF creation was requested and succeeded, the pdf is offered for download
282 # via send_file (which uses ajax in this case).
286 if ( !$self->order->delivered ) {
288 $self->js_reset_order_and_item_ids_after_save;
291 my $redirect_url = $self->url_for(
294 id => $self->order->id,
297 my $format = $::form->{print_options}->{format};
298 my $media = $::form->{print_options}->{media};
299 my $formname = $::form->{print_options}->{formname};
300 my $copies = $::form->{print_options}->{copies};
301 my $groupitems = $::form->{print_options}->{groupitems};
302 my $printer_id = $::form->{print_options}->{printer_id};
304 # only pdf and opendocument by now
305 if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
306 flash_later('error', t8('Format \'#1\' is not supported yet/anymore.', $format));
307 return $self->js->redirect_to($redirect_url)->render;
310 # only screen or printer by now
311 if (none { $media eq $_ } qw(screen printer)) {
312 flash_later('error', t8('Media \'#1\' is not supported yet/anymore.', $media));
313 return $self->js->redirect_to($redirect_url)->render;
316 # create a form for generate_attachment_filename
317 my $form = Form->new;
318 $form->{$self->nr_key()} = $self->order->number;
319 $form->{type} = $self->type;
320 $form->{format} = $format;
321 $form->{formname} = $formname;
323 '_' . $self->order->language->template_code if $self->order->language;
324 my $pdf_filename = $form->generate_attachment_filename();
326 my @errors = generate_pdf($self->order, \$pdf, {
328 formname => $formname,
329 language => $self->order->language,
330 printer_id => $printer_id,
331 groupitems => $groupitems
333 if (scalar @errors) {
334 flash_later('error', t8('Generating the document failed: #1', $errors[0]));
335 return $self->js->redirect_to($redirect_url)->render;
338 if ($media eq 'screen') {
340 flash_later('info', t8('The document has been created.'));
343 type => SL::MIME->mime_type_from_ext($pdf_filename),
344 name => $pdf_filename,
348 } elsif ($media eq 'printer') {
350 my $printer_id = $::form->{print_options}->{printer_id};
351 SL::DB::Printer->new(id => $printer_id)->load->print_document(
356 flash_later('info', t8('The document has been printed.'));
359 my @warnings = store_pdf_to_webdav_and_filemanagement(
360 $self->order, $pdf, $pdf_filename, $formname
362 if (scalar @warnings) {
363 flash_later('warning', $_) for @warnings;
366 $self->save_history('PRINTED');
368 $self->js->redirect_to($redirect_url)->render;
371 sub action_preview_pdf {
374 if ( !$self->order->delivered ) {
376 $self->js_reset_order_and_item_ids_after_save;
379 my $redirect_url = $self->url_for(
382 id => $self->order->id,
386 my $media = 'screen';
387 my $formname = $self->type;
390 # create a form for generate_attachment_filename
391 my $form = Form->new;
392 $form->{$self->nr_key()} = $self->order->number;
393 $form->{type} = $self->type;
394 $form->{format} = $format;
395 $form->{formname} = $formname;
397 '_' . $self->order->language->template_code if $self->order->language;
398 my $pdf_filename = $form->generate_attachment_filename();
401 my @errors = generate_pdf($self->order, \$pdf, {
403 formname => $formname,
404 language => $self->order->language,
406 if (scalar @errors) {
407 flash_later('error', t8('Conversion to PDF failed: #1', $errors[0]));
408 return $self->js->redirect_to($redirect_url)->render;
410 $self->save_history('PREVIEWED');
411 flash_later('info', t8('The PDF has been previewed'));
415 type => SL::MIME->mime_type_from_ext($pdf_filename),
416 name => $pdf_filename,
419 $self->js->redirect_to($redirect_url)->render;
422 # open the email dialog
423 sub action_save_and_show_email_dialog {
426 if (!$self->order->delivered) {
428 $self->js_reset_order_and_item_ids_after_save;
431 my $cv = $self->order->customervendor
432 or return $self->js->flash('error',
433 $self->cv eq 'customer' ?
434 t8('Cannot send E-mail without customer given')
435 : t8('Cannot send E-mail without vendor given')
438 my $form = Form->new;
439 $form->{$self->nr_key()} = $self->order->number;
440 $form->{cusordnumber} = $self->order->cusordnumber;
441 $form->{formname} = $self->type;
442 $form->{type} = $self->type;
444 '_' . $self->order->language->template_code if $self->order->language;
445 $form->{language_id} =
446 $self->order->language->id if $self->order->language;
447 $form->{format} = 'pdf';
449 $self->order->contact->cp_id if $self->order->contact;
453 ($self->order->contact ? $self->order->contact->cp_email : undef)
454 || ($cv->is_customer ? $cv->delivery_order_mail : undef)
456 $email_form->{cc} = $cv->cc;
457 $email_form->{bcc} = join ', ', grep $_, $cv->bcc;
458 # Todo: get addresses from shipto, if any
459 $email_form->{subject} = $form->generate_email_subject();
460 $email_form->{attachment_filename} = $form->generate_attachment_filename();
461 $email_form->{message} = $form->generate_email_body();
462 $email_form->{js_send_function} = 'kivi.DeliveryOrder.send_email()';
464 my %files = $self->get_files_for_email_dialog();
466 my @employees_with_email = grep {
467 my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
468 $user && !!trim($user->get_config_value('email'));
469 } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
471 my $dialog_html = $self->render(
472 'common/_send_email_dialog', { output => 0 },
473 email_form => $email_form,
474 show_bcc => $::auth->assert('email_bcc', 'may fail'),
476 is_customer => $self->type_data->properties("is_customer"),
477 ALL_EMPLOYEES => \@employees_with_email,
478 ALL_PARTNER_EMAIL_ADDRESSES => $cv->get_all_email_addresses(),
482 ->run('kivi.DeliveryOrder.show_email_dialog', $dialog_html)
488 sub action_send_email {
491 if ( !$self->order->delivered ) {
496 $self->js->run('kivi.Order.close_email_dialog');
501 my @redirect_params = (
504 id => $self->order->id,
507 # Set the error handler to reload the document and display errors later,
508 # because the document is already saved and saving can have some side effects
509 # such as generating a document number, project number or record links,
510 # which will be up to date when the document is reloaded.
511 # Hint: Do not use "die" here and try to catch exceptions in subroutine
512 # calls. You should use "$::form->error" which respects the error handler.
513 local $::form->{__ERROR_HANDLER} = sub {
514 flash_later('error', $_[0]);
515 $self->redirect_to(@redirect_params);
516 $::dispatcher->end_request;
519 # move $::form->{email_form} to $::form
520 my $email_form = delete $::form->{email_form};
522 if ($email_form->{additional_to}) {
523 $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
524 delete $email_form->{additional_to};
527 my %field_names = (to => 'email');
528 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
530 # for Form::cleanup which may be called in Form::send_email
531 $::form->{cwd} = getcwd();
532 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
534 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
535 $::form->{media} = 'email';
537 $::form->{attachment_policy} //= '';
539 # Is an old file version available?
541 if ($::form->{attachment_policy} eq 'old_file') {
542 $attfile = SL::File->get_all(
543 object_id => $self->order->id,
544 object_type => $::form->{formname},
545 file_type => 'document',
546 print_variant => $::form->{formname},
550 if ( $::form->{attachment_policy} ne 'no_file'
551 && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
553 my @errors = generate_pdf($self->order, \$pdf, {
554 media => $::form->{media},
555 format => $::form->{print_options}->{format},
556 formname => $::form->{print_options}->{formname},
557 language => $self->order->language,
558 printer_id => $::form->{print_options}->{printer_id},
559 groupitems => $::form->{print_options}->{groupitems}},
561 if (scalar @errors) {
562 $::form->error(t8('Generating the document failed: #1', $errors[0]));
565 my @warnings = store_pdf_to_webdav_and_filemanagement(
566 $self->order, $pdf, $::form->{attachment_filename}, $::form->{formname}
568 if (scalar @warnings) {
569 flash_later('warning', $_) for @warnings;
572 my $sfile = SL::SessionFile::Random->new(mode => "w");
573 $sfile->fh->print($pdf);
576 $::form->{tmpfile} = $sfile->file_name;
577 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be
578 # called in Form::send_email
581 $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a
582 # linked record to the mail
583 $::form->send_email(\%::myconfig, 'pdf');
585 $self->save_history('MAILED');
586 flash_later('info', t8('The email has been sent.'));
588 # internal notes unless no email journal
589 unless ($::instance_conf->get_email_journal) {
590 my $intnotes = $self->order->intnotes;
591 $intnotes .= "\n\n" if $self->order->intnotes;
592 $intnotes .= t8('[email]') . "\n";
593 $intnotes .= t8('Date') . ": " .
594 $::locale->format_date_object(
595 DateTime->now_local, precision => 'seconds'
597 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
598 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
599 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
600 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
601 $intnotes .= t8('Message') . ": " . SL::HTML::Util->strip($::form->{message});
603 $self->order->update_attributes(intnotes => $intnotes);
606 $self->redirect_to(@redirect_params);
609 sub action_workflow_new_record {
611 my $to_type = $::form->{to_type};
612 my $to_controller = get_object_name_from_type($to_type);
614 my %additional_params = ();
615 if ($::form->{only_selected_item_positions}) { # ids can be unset before save
616 my $item_positions = $::form->{selected_item_positions} || [];
617 my @from_item_ids = map { $self->order->items_sorted->[$_]->id } @$item_positions;
618 $additional_params{from_item_ids} = \@from_item_ids;
621 flash_later('info', $self->type_data->text('saved'));
624 controller => $to_controller,
625 action => 'add_from_record',
627 from_id => $self->order->id,
628 from_type => $self->order->type,
629 email_journal_id => $::form->{workflow_email_journal_id},
630 email_attachment_id => $::form->{workflow_email_attachment_id},
631 callback => $::form->{workflow_email_callback},
636 # save the order and redirect to the frontend subroutine for a new
638 sub action_workflow_invoice {
642 controller => 'do.pl',
643 action => 'invoice_from_delivery_order_controller',
644 from_id => $self->order->id,
645 email_journal_id => $::form->{workflow_email_journal_id},
646 email_attachment_id => $::form->{workflow_email_attachment_id},
647 callback => $::form->{workflow_email_callback},
651 # set form elements in respect to a changed customer or vendor
653 # This action is called on an change of the customer/vendor picker.
654 sub action_customer_vendor_changed {
658 SL::Model::Record->update_after_customer_vendor_change($self->order)
661 my $cv_method = $self->cv;
663 if ( $self->order->$cv_method->contacts
664 && scalar @{ $self->order->$cv_method->contacts } > 0) {
665 $self->js->show('#cp_row');
667 $self->js->hide('#cp_row');
670 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
671 $self->js->show('#shipto_selection');
673 $self->js->hide('#shipto_selection');
676 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
679 ->replaceWith('#order_cp_id', $self->build_contact_select)
680 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
681 ->replaceWith('#shipto_inputs ', $self->build_shipto_inputs)
682 ->replaceWith('#business_info_row', $self->build_business_info_row)
683 ->val( '#order_taxzone_id', $self->order->taxzone_id)
684 ->val( '#order_taxincluded', $self->order->taxincluded)
685 ->val( '#order_currency_id', $self->order->currency_id)
686 ->val( '#order_payment_id', $self->order->payment_id)
687 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
688 ->val( '#order_intnotes', $self->order->intnotes)
689 ->val( '#order_language_id', $self->order->$cv_method->language_id)
690 ->focus( '#order_' . $self->cv . '_id')
691 ->run('kivi.DeliveryOrder.update_exchangerate');
693 $self->js_redisplay_cvpartnumbers;
697 # called if a unit in an existing item row is changed
698 sub action_unit_changed {
701 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
702 my $item = $self->order->items_sorted->[$idx];
704 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
705 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
708 'kivi.DeliveryOrder.update_sellprice',
710 $item->sellprice_as_number
712 $self->js_redisplay_line_values;
716 # add an item row for a new item entered in the input row
717 sub action_add_item {
720 delete $::form->{add_item}->{create_part_type};
722 my $form_attr = $::form->{add_item};
724 return unless $form_attr->{parts_id};
726 my $item = new_item($self->order, $form_attr);
728 $self->order->add_items($item);
730 $self->get_item_cvpartnumber($item);
732 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
733 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
737 in_out => $self->type_data->properties("transfer"),
740 if ($::form->{insert_before_item_id}) {
743 '.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')',
748 ->append('#row_table_id', $row_as_html);
751 if ( $item->part->is_assortment ) {
752 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
753 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
755 parts_id => $assortment_item->parts_id,
756 qty => $assortment_item->qty *
757 $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
758 unit => $assortment_item->unit,
759 description => $assortment_item->part->description,
761 my $item = new_item($self->order, $attr);
763 # set discount to 100% if item isn't supposed to be charged, overwriting
764 # any customer discount
765 $item->discount(1) unless $assortment_item->charge;
767 $self->order->add_items( $item );
768 $self->get_item_cvpartnumber($item);
769 my $item_id = join('_',
771 Time::HiRes::gettimeofday(),
772 int rand 1000000000000
774 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
779 if ($::form->{insert_before_item_id}) {
782 '.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')',
787 ->append('#row_table_id', $row_as_html);
793 ->val('.add_item_input', '')
794 ->run('kivi.DeliveryOrder.init_row_handlers')
795 ->run('kivi.DeliveryOrder.renumber_positions')
796 ->focus('#add_item_parts_id_name');
798 $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
803 # add item rows for multiple items at once
804 sub action_add_multi_items {
807 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
808 return $self->js->render() unless scalar @form_attr;
811 foreach my $attr (@form_attr) {
812 my $item = new_item($self->order, $attr);
814 if ( $item->part->is_assortment ) {
815 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
817 parts_id => $assortment_item->parts_id,
818 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
819 unit => $assortment_item->unit,
820 description => $assortment_item->part->description,
822 my $item = new_item($self->order, $attr);
824 # set discount to 100% if item isn't supposed to be charged, overwriting
825 # any customer discount
826 $item->discount(1) unless $assortment_item->charge;
831 $self->order->add_items(@items);
833 foreach my $item (@items) {
834 $self->get_item_cvpartnumber($item);
835 my $item_id = join('_',
837 Time::HiRes::gettimeofday(),
838 int rand 1000000000000
840 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
844 in_out => $self->type_data->properties("transfer"),
847 if ($::form->{insert_before_item_id}) {
850 '.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')',
855 ->append('#row_table_id', $row_as_html);
860 ->run('kivi.Part.close_picker_dialogs')
861 ->run('kivi.DeliveryOrder.init_row_handlers')
862 ->run('kivi.DeliveryOrder.renumber_positions')
863 ->focus('#add_item_parts_id_name');
865 $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
870 sub action_update_exchangerate {
874 is_standard => $self->order->currency_id == $::instance_conf->get_currency_id,
875 currency_name => $self->order->currency->name,
878 $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
881 # redisplay item rows if they are sorted by an attribute
882 sub action_reorder_items {
886 partnumber => sub { $_[0]->part->partnumber },
887 description => sub { $_[0]->description },
888 qty => sub { $_[0]->qty },
889 sellprice => sub { $_[0]->sellprice },
890 discount => sub { $_[0]->discount },
891 cvpartnumber => sub { $_[0]->{cvpartnumber} },
894 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
896 my $method = $sort_keys{$::form->{order_by}};
898 map { { old_pos => $_->position, order_by => $method->($_) } }
899 @{ $self->order->items_sorted };
900 if ($::form->{sort_dir}) {
901 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
902 @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
904 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
907 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
908 @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
910 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
914 ->run('kivi.DeliveryOrder.redisplay_items', \@to_sort)
918 # save the order in a session variable and redirect to the part controller
919 sub action_create_part {
922 my $previousform = $::auth->save_form_in_session(non_scalars => 1);
924 my $callback = $self->url_for(
925 action => 'return_from_create_part',
926 type => $self->type, # type is needed for check_auth on return
927 previousform => $previousform,
931 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.')
934 my @redirect_params = (
935 controller => 'Part',
937 part_type => $::form->{add_item}->{create_part_type},
938 callback => $callback,
942 $self->redirect_to(@redirect_params);
945 sub action_return_from_create_part {
948 $self->{created_part} = SL::DB::Part->new(
949 id => delete $::form->{new_parts_id}
950 )->load if $::form->{new_parts_id};
952 $::auth->restore_form_from_session(delete $::form->{previousform});
954 $self->order($self->init_order);
955 $self->reinit_after_new_order();
957 if ($self->order->id) {
960 'delivery_order/form',
961 title => $self->get_title_for('edit'),
962 %{$self->{template_args}}
970 sub action_stock_in_out_dialog {
973 my $part = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
974 my $unit = SL::DB::Unit->load_cached($::form->{unit}) or die "need unit";
975 my $stock = $::form->{stock};
976 my $row = $::form->{row};
977 my $item_id = $::form->{item_id};
978 my $qty = _parse_number($::form->{qty_as_number});
979 my $row_ui_id = $::form->{row_ui_id};
980 my $next_button = $::form->{next_button} eq 'true';
982 my $inout = $self->type_data->properties("transfer");
984 my @contents = DO->get_item_availability(parts_id => $part->id);
985 my $stock_info = DO->unpack_stock_information(packed => $stock);
987 $self->merge_stock_data($stock_info, \@contents, $part, $unit);
989 my $delivered = $self->order->delivered;
990 $self->render("delivery_order/stock_dialog", { layout => 0 },
991 WHCONTENTS => \@contents,
992 STOCK_INFO => $stock_info,
993 WAREHOUSES => !$delivered ? SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]], with_objects=> ["bins",]) : [],
996 do_unit => $unit->name,
997 delivered => $self->order->delivered,
1001 row_ui_id => $row_ui_id,
1002 next_button => $next_button,
1006 sub action_add_stock_in_line_to_dialog {
1009 my $do_qty = _parse_number($::form->{do_qty});
1010 my $qty_sum = $::form->{qty_sum};
1011 my $row_count = $::form->{row_count};
1012 my $part = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
1014 my $row_as_html = $self->p->render('delivery_order/stock_dialog/_stock_in_new_row',
1015 WAREHOUSES => SL::DB::Manager::Warehouse->get_all(with_objects=> ["bins",]),
1017 pos => $row_count + 1,
1018 remaining_qty => $do_qty - $qty_sum,
1021 $self->js->append('#stock-in-out-table tbody', $row_as_html)->render();
1024 sub action_update_stock_information {
1027 my $stock_info = $::form->{stock_info};
1028 my $unit = $::form->{unit};
1029 my $yaml = SL::YAML::Dump($stock_info);
1030 my $stock_qty = $self->calculate_stock_in_out_from_stock_info($unit, $stock_info);
1033 stock_info => $yaml,
1034 stock_qty => $stock_qty,
1037 \ SL::JSON::to_json($response),
1038 { layout => 0, type => 'json', process => 0 }
1042 sub merge_stock_data {
1043 my ($self, $stock_info, $contents, $part, $unit) = @_;
1044 # TODO rewrite to mapping
1046 if (!$self->order->delivered) {
1047 for my $row (@$contents) {
1048 # row here is in parts units. stock is in item units
1049 $row->{available_qty} = _format_number(
1050 $part->unit_obj->convert_to($row->{qty}, $unit)
1053 for my $sinfo (@{ $stock_info }) {
1054 next if $row->{bin_id} != $sinfo->{bin_id} ||
1055 $row->{warehouse_id} != $sinfo->{warehouse_id} ||
1056 $row->{chargenumber} ne $sinfo->{chargenumber} ||
1057 $row->{bestbefore} ne $sinfo->{bestbefore};
1059 $row->{"stock_$_"} = $sinfo->{$_}
1060 for qw(qty unit error delivery_order_items_stock_id);
1065 for my $sinfo (@{ $stock_info }) {
1066 my $bin = SL::DB::Bin->load_cached($sinfo->{bin_id});
1067 $sinfo->{warehousedescription} = $bin->warehouse->description;
1068 $sinfo->{bindescription} = $bin->description;
1069 map { $sinfo->{"stock_$_"} = $sinfo->{$_} } qw(qty unit);
1074 # load the second row for one or more items
1076 # This action gets the html code for all items second rows by rendering a template for
1077 # the second row and sets the html code via client js.
1078 sub action_load_second_rows {
1081 foreach my $item_id (@{ $::form->{item_ids} }) {
1082 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1083 my $item = $self->order->items_sorted->[$idx];
1085 $self->js_load_second_row($item, $item_id, 0);
1088 # for lastcosts change-callback
1089 $self->js->run('kivi.DeliveryOrder.init_row_handlers') if $self->order->is_sales;
1091 $self->js->render();
1094 # update description, notes and sellprice from master data
1095 sub action_update_row_from_master_data {
1098 foreach my $item_id (@{ $::form->{item_ids} }) {
1099 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1100 my $item = $self->order->items_sorted->[$idx];
1101 my $texts = get_part_texts($item->part, $self->order->language_id);
1103 $item->description($texts->{description});
1104 $item->longdescription($texts->{longdescription});
1106 my ($price_src, undef) = SL::Model::Record->get_best_price_and_discount_source($self->order, $item, ignore_given => 1);
1107 $item->sellprice($price_src->price);
1108 $item->active_price_source($price_src);
1111 ->run('kivi.DeliveryOrder.update_sellprice', $item_id, $item->sellprice_as_number)
1112 ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
1113 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
1114 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
1116 if ($self->search_cvpartnumber) {
1117 $self->get_item_cvpartnumber($item);
1118 $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
1122 $self->js_redisplay_line_values;
1124 $self->js->render();
1127 sub action_transfer_stock {
1128 my ($self, $default_transfer) = @_;
1130 if ($self->order->delivered) {
1131 return $self->js->flash("error",
1132 t8('The parts for this order have already been transferred')
1136 my $inout = $self->type_data->properties('transfer');
1140 my $order = $self->order;
1142 # TODO move to type data
1143 my $trans_type = $inout eq 'in'
1144 ? SL::DB::Manager::TransferType->find_by(
1145 direction => "in", description => "stock")
1146 : SL::DB::Manager::TransferType->find_by(
1147 direction => "out", description => "shipped");
1150 my @transfer_requests;
1152 for my $item (@{ $order->items_sorted }) {
1153 for my $stock (@{ $item->delivery_order_stock_entries }) {
1154 my $transfer = SL::DB::Inventory->new_from($stock);
1155 $transfer->trans_type($trans_type);
1156 $transfer->oe_id($order->id);
1157 $transfer->qty($transfer->qty * -1) if $inout eq 'out';
1158 $transfer->qty($transfer->qty * 1) if $inout eq 'in';
1159 $transfer->comment(t8("Default transfer delivery order")) if $default_transfer;
1161 push @transfer_requests, $transfer if defined $transfer->qty && $transfer->qty != 0;
1165 if (!@transfer_requests) {
1166 return $self->js->flash("error", t8("No stock to transfer"))->render;
1169 if ($inout eq 'out') { # check stock for enough qty
1170 my @missing_qtys = SL::Helper::Inventory::check_stock_out_transfer_requests(
1171 transfer_requests => \@transfer_requests,
1172 default_transfer => $default_transfer,
1175 if (scalar @missing_qtys) {
1176 my $error = t8('The stock is to low: #1.',
1178 $_->{chargenumber} && $_->{bestbefore}
1180 "For #1, #2 #3 are missing of batch with chargenumber #4 and bestbefore date of #5 in bin #6",
1181 $_->{part}->displayable_name,
1182 $::form->format_amount(\%::myconfig, $_->{missing_qty}),
1185 DateTime->from_ymdhms($_->{bestbefore})->to_kivitendo,
1186 $_->{bin}->full_description,
1188 : $_->{chargenumber}
1190 "For #1, #2 #3 are missing of batch with chargenumber #4 in bin #5",
1191 $_->{part}->displayable_name,
1192 $::form->format_amount(\%::myconfig, $_->{missing_qty}),
1195 $_->{bin}->full_description,
1199 "For #1, #2 #3 are missing with a bestbefore date of #4 in bin #5",
1200 $_->{part}->displayable_name,
1201 $::form->format_amount(\%::myconfig, $_->{missing_qty}),
1203 DateTime->from_ymdhms($_->{bestbefore})->to_kivitendo,
1204 $_->{bin}->full_description,
1207 "For #1, #2 #3 are missing in bin #4",
1208 $_->{part}->displayable_name,
1209 $::form->format_amount(\%::myconfig, $_->{missing_qty}),
1211 $_->{bin}->full_description,
1217 return $self->js->flash("error", $error)->render;
1221 SL::DB->client->with_transaction(sub {
1223 $_->save for @transfer_requests;
1224 $self->order->update_attributes(delivered => 1);
1226 # update qty and stock info
1227 foreach my $item (@{$self->order->items}) {
1228 $self->order->prepare_stock_info($item);
1229 my $stock_info_yaml = $item->{stock_info};
1230 my $item_position = $item->position;
1231 my $stock_qty = $self->calculate_stock_in_out($item);
1232 my $unit = $item->unit;
1233 $self->js->text("[data-position=$item_position] .data-stock-qty", "$stock_qty $unit");
1234 my $selector = "[data-position=$item_position] .data-stock-info";
1235 $self->js->val($selector, $stock_info_yaml);
1239 ->flash("info", t8("Stock transfered"))
1240 ->run('kivi.ActionBar.setDisabled', '#save_action',
1241 t8('This record has already been delivered.'))
1242 ->run('kivi.ActionBar.setDisabled', '#save_and_close',
1243 t8('This record has already been delivered.'))
1244 ->run('kivi.ActionBar.setDisabled', '#transfer_out_action',
1245 t8('The parts for this order have already been transferred'))
1246 ->run('kivi.ActionBar.setDisabled', '#transfer_out_default_action',
1247 t8('The parts for this order have already been transferred'))
1248 ->run('kivi.ActionBar.setDisabled', '#transfer_in_action',
1249 t8('The parts for this order have already been transferred'))
1250 ->run('kivi.ActionBar.setDisabled', '#transfer_in_default_action',
1251 t8('The parts for this order have already been transferred'))
1252 ->run('kivi.ActionBar.setDisabled', '#delete_action',
1253 t8('The parts for this order have already been transferred'))
1254 ->run('kivi.ActionBar.setEnabled', '#undo_transfer_action',
1255 t8('The parts for this order have already been transferred'))
1256 ->html('#data-status-line', delivery_order_status_line($self->order))
1260 sub action_transfer_stock_default {
1262 my $delivery_order = $self->order;
1263 my @items = @{$delivery_order->items_sorted};
1265 # get default bin if set in config
1266 my ($default_warehouse_id, $default_bin_id);
1267 if ($::instance_conf->get_transfer_default_use_master_default_bin) {
1268 $default_warehouse_id = $::instance_conf->get_warehouse_id;
1269 $default_bin_id = $::instance_conf->get_bin_id;
1272 my @transfer_requests = ();
1274 my %units_by_name = map { $_->name => $_ } @{ SL::DB::Manager::Unit->get_all };
1275 foreach my $item (@items) {
1276 my $part = $item->part;
1277 my $base_unit_factor = $units_by_name{$part->unit}->factor || 1;
1278 my $item_unit_factor = $units_by_name{$item->unit}->factor || 1;
1279 my $qty = $item->qty * $item_unit_factor / $base_unit_factor;
1280 return $self->js->flash('error', t8('Cannot transfer negative entries.'))->render() if $qty < 0;
1281 $qty = 0 if (!$::instance_conf->get_transfer_default_services && $part->is_service);
1283 $parts_qty{$part->id} += $qty if $qty;
1284 push @transfer_requests, {
1285 'warehouse_id' => $part->warehouse_id || $default_warehouse_id,
1286 'bin_id' => $part->bin_id || $default_bin_id,
1287 'unit' => $item->unit,
1289 # added in check transfer_request out direction if possible
1290 'chargenumber' => undef, # $item->serialnumber, # Is not used in delivery order
1291 'bestbefore' => undef, # $item->bestbefore, # Is not used in delivery order
1295 # check transfer_requests are correctly
1296 my %parts_errors = (); # missing_bin, missing_qty, multiple_options
1297 my $grouped_qty_query = qq|
1298 SELECT SUM(qty) as qty, chargenumber, bestbefore
1300 WHERE parts_id = ? AND bin_id = ?
1301 GROUP BY chargenumber, bestbefore
1303 my $dbh = $self->order->dbh;
1304 my $in_out_direction = $delivery_order->type_data->properties('transfer');
1305 for my $idx (0 .. scalar @transfer_requests - 1) {
1306 my $transfer_request = $transfer_requests[$idx];
1307 next unless $transfer_request->{qty}; # empty request
1308 my $item = $items[$idx];
1309 my $part_id = $item->parts_id;
1310 my $bin_id = $transfer_request->{bin_id};
1311 $parts_errors{$part_id}{missing_bin} = 1 unless $bin_id;
1312 next unless $bin_id;
1313 if ($in_out_direction eq 'out') {
1314 my @grouped_qty = selectall_hashref_query(
1315 $::form, $dbh, $grouped_qty_query, $part_id, $bin_id);
1317 if (1 < scalar grep {$_->{qty} != 0} @grouped_qty) {
1318 $parts_errors{$part_id}{multiple_options} = 1;
1320 my $max_qty = sum0(map {$_->{qty}} @grouped_qty);
1321 if ($max_qty < $parts_qty{$part_id}) {
1322 $parts_errors{$part_id}{missing_qty} = $parts_qty{$part_id} - $max_qty;
1323 $parts_errors{$part_id}{bin_id} = $bin_id;
1326 next if $parts_errors{$part_id};
1327 # find correct chargenumber and bestbefore
1328 my $stock_info = first {$_->{qty} >= $transfer_request->{qty}} @grouped_qty;
1329 $transfer_request->{chargenumber} = $stock_info->{chargenumber};
1330 $transfer_request->{bestbefore} = $stock_info->{bestbefore};
1334 # auslagern soll immer gehen, auch wenn nicht genügend auf lager ist.
1335 # der lagerplatz ist hier extra konfigurierbar, bspw. Lager-Korrektur mit
1336 # Lagerplatz Lagerplatz-Korrektur
1337 my $default_warehouse_id_ignore_onhand = $::instance_conf->get_warehouse_id_ignore_onhand;
1338 my $default_bin_id_ignore_onhand = $::instance_conf->get_bin_id_ignore_onhand;
1339 if ($::instance_conf->get_transfer_default_ignore_onhand && $default_bin_id_ignore_onhand) {
1340 foreach my $part_id (keys %parts_errors) {
1341 # entsprechende defaults holen
1342 # falls chargenumber, bestbefore oder anzahl nicht stimmt, auf automatischen
1343 # lagerplatz wegbuchen!
1344 for my $idx (0 .. scalar @transfer_requests - 1) {
1345 my $transfer_request = $transfer_requests[$idx];
1346 next unless $transfer_request->{qty}; # empty request
1348 if ($items[$idx]->parts_id eq $part_id){
1349 $transfer_request->{bin_id} = $default_bin_id_ignore_onhand;
1350 $transfer_request->{warehouse_id} = $default_warehouse_id_ignore_onhand;
1353 delete %parts_errors{$part_id};
1358 if (scalar keys %parts_errors) {
1359 my @multiple_options = ();
1360 foreach my $part_id (keys %parts_errors) {
1361 my $part = SL::DB::Part->new(id => $part_id)->load();
1362 if ($parts_errors{$part_id}{missing_bin}){
1363 $self->js->error(t8('No standard bin set for #1.', $part->displayable_name));
1365 if ($parts_errors{$part_id}{missing_qty}) {
1366 my $bin = SL::DB::Manager::Bin->find_by(
1367 id => $parts_errors{$part_id}{bin_id}
1370 t8('There are #1 of "#2" missing from the bin #3 for transfer.',
1371 $parts_errors{$part_id}{missing_qty}, $part->displayable_name, $bin->full_description));
1373 if ($parts_errors{$part_id}{multiple_options}){
1374 push @multiple_options, $part;
1377 if (scalar @multiple_options) {
1378 $self->js->error(t8(
1379 "There are parts with multiple chargenumbers or bestbefore dates set. This can't be decided automatically. Pleas transfer this delivery order manually. Can't decided for #1.",
1380 join ", ", map {$_->displayable_name} @multiple_options)
1383 return $self->js->render();
1386 # assign each delivery_order_item it's stock
1387 for my $idx (0 .. scalar @transfer_requests - 1) {
1388 my %transfer_request = %{$transfer_requests[$idx]};
1389 next unless $transfer_request{qty}; # empty request
1391 my $item = $items[$idx];
1392 my @stocks = (SL::DB::DeliveryOrderItemsStock->new(%transfer_request));
1393 $item->delivery_order_stock_entries(@stocks);
1396 my $default_transfer = 1;
1397 $self->action_transfer_stock($default_transfer);
1400 sub action_undo_transfers {
1403 SL::DB->client->with_transaction(sub {
1404 foreach my $item (@{$self->order->orderitems}) {
1405 foreach my $inv_item (@{ $item->delivery_order_stock_entries}) {
1406 $inv_item->inventory->delete;
1410 $self->order->update_attributes(delivered => 0);
1411 $self->order->update_attributes(closed => 0);
1414 flash_later('info', t8("Transfer undone"));
1415 my @redirect_params = (
1417 type => $self->type,
1418 id => $self->order->id,
1421 $self->redirect_to(@redirect_params);
1424 sub js_load_second_row {
1425 my ($self, $item, $item_id, $do_parse) = @_;
1428 # Parse values from form (they are formated while rendering (template)).
1429 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1430 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
1431 foreach my $var (@{ $item->cvars_by_config }) {
1432 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1434 $item->parse_custom_variable_values;
1437 my $row_as_html = $self->p->render('delivery_order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
1440 ->html('#second_row_' . $item_id, $row_as_html)
1441 ->data('#second_row_' . $item_id, 'loaded', 1);
1444 sub js_redisplay_line_values {
1447 my $is_sales = $self->order->is_sales;
1449 # sales orders with margins
1454 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1455 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
1456 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
1457 ]} @{ $self->order->items_sorted };
1461 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1462 ]} @{ $self->order->items_sorted };
1466 ->run('kivi.DeliveryOrder.redisplay_line_values', $is_sales, \@data);
1469 sub js_redisplay_cvpartnumbers {
1472 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1474 my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
1477 ->run('kivi.DeliveryOrder.redisplay_cvpartnumbers', \@data);
1480 sub js_reset_order_and_item_ids_after_save {
1484 ->val('#id', $self->order->id)
1485 ->val('#converted_from_record_type_ref', '')
1486 ->val('#converted_from_record_id', '')
1487 ->val('#order_' . $self->nr_key(), $self->order->number);
1490 foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
1491 next if !$self->order->items_sorted->[$idx]->id;
1492 next if $form_item_id !~ m{^new};
1495 '[name="orderitem_ids[+]"][value="' . $form_item_id . '"]',
1496 $self->order->items_sorted->[$idx]->id)
1497 ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
1498 ->attr('#item_' . $form_item_id, "id",
1499 'item_' . $self->order->items_sorted->[$idx]->id);
1503 $self->js->val('[name="converted_from_record_item_type_refs[+]"]', '');
1504 $self->js->val('[name="converted_from_record_item_ids[+]"]', '');
1514 my $type = $self->order->record_type;
1515 if (none { $type eq $_ } @{$self->valid_types}) {
1516 die "Not a valid type for delivery order";
1525 return $self->type_data->properties("customervendor");
1528 sub init_search_cvpartnumber {
1531 my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1532 my $search_cvpartnumber;
1533 $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
1534 $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel() if $self->cv eq 'vendor';
1536 return $search_cvpartnumber;
1539 sub init_show_update_button {
1542 !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1553 sub init_part_picker_classification_ids {
1556 return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(
1557 where => $self->type_data->part_classification_query
1564 $::auth->assert($self->type_data->rights('view') || 'DOES_NOT_EXIST');
1567 sub check_auth_for_edit {
1570 $::auth->assert($self->type_data->rights('edit') || 'DOES_NOT_EXIST');
1573 # build the selection box for contacts
1575 # Needed, if customer/vendor changed.
1576 sub build_contact_select {
1579 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1580 value_key => 'cp_id',
1581 title_key => 'full_name_dep',
1582 default => $self->order->cp_id,
1584 style => 'width: 300px',
1588 # build the selection box for shiptos
1590 # Needed, if customer/vendor changed.
1591 sub build_shipto_select {
1594 select_tag('order.shipto_id',
1596 displayable_id => t8("No/individual shipping address"),
1599 $self->order->{$self->cv}->shipto ],
1600 value_key => 'shipto_id',
1601 title_key => 'displayable_id',
1602 default => $self->order->shipto_id,
1604 style => 'width: 300px',
1608 # build the inputs for the cusom shipto dialog
1610 # Needed, if customer/vendor changed.
1611 sub build_shipto_inputs {
1614 my $content = $self->p->render('common/_ship_to_dialog',
1615 vc_obj => $self->order->customervendor,
1616 cs_obj => $self->order->custom_shipto,
1617 cvars => $self->order->custom_shipto->cvars_by_config,
1618 id_selector => '#order_shipto_id');
1620 div_tag($content, id => 'shipto_inputs');
1623 # render the info line for business
1625 # Needed, if customer/vendor changed.
1626 sub build_business_info_row
1628 $_[0]->p->render('delivery_order/tabs/_business_info_row', SELF => $_[0]);
1635 return if !$::form->{id};
1637 $self->order(SL::DB::DeliveryOrder->new(id => $::form->{id})->load);
1639 $self->reinit_after_new_order();
1641 return $self->order;
1644 # load or create a new order object
1646 # And assign changes from the form to this object.
1647 # If the order is loaded from db, check if items are deleted in the form,
1648 # remove them form the object and collect them for removing from db on saving.
1649 # Then create/update items from form (via make_item) and add them.
1653 # add_items adds items to an order with no items for saving, but they cannot
1654 # be retrieved via items until the order is saved. Adding empty items to new
1655 # order here solves this problem.
1657 if ($::form->{id}) {
1658 $order = SL::DB::DeliveryOrder->new(
1667 $order = SL::DB::DeliveryOrder->new(
1669 currency_id => $::instance_conf->get_currency_id(),
1670 record_type => $::form->{type}
1672 $order = SL::Model::Record->update_after_new($order);
1675 my $cv_id_method = $order->type_data->properties('customervendor'). '_id';
1676 if (!$::form->{id} && $::form->{$cv_id_method}) {
1677 $order->$cv_id_method($::form->{$cv_id_method});
1678 $order = SL::Model::Record->update_after_customer_vendor_change($order);
1681 # don't assign hashes as objects
1682 my $form_orderitems = delete $::form->{order}->{orderitems};
1684 $order->assign_attributes(%{$::form->{order}});
1686 # restore form values
1687 $::form->{order}->{orderitems} = $form_orderitems;
1689 $self->setup_custom_shipto_from_form($order, $::form);
1691 # remove deleted items
1692 $self->item_ids_to_delete([]);
1693 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1694 my $item = $order->orderitems->[$idx];
1695 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1696 splice @{$order->orderitems}, $idx, 1;
1697 push @{$self->item_ids_to_delete}, $item->id;
1703 foreach my $form_attr (@{$form_orderitems}) {
1704 my $item = make_item($order, $form_attr);
1705 $item->position($pos);
1710 $order->add_items(grep {!$_->id} @items);
1715 # create or update items from form
1717 # Make item objects from form values. For items already existing read from db.
1718 # Create a new item else. And assign attributes.
1720 my ($record, $attr) = @_;
1723 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1725 my $is_new = !$item;
1727 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1728 # they cannot be retrieved via custom_variables until the order/orderitem is
1729 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1730 $item ||= SL::DB::DeliveryOrderItem->new(custom_variables => []);
1733 if (my $stock_info = delete $attr->{stock_info}) {
1734 my %existing = map { $_->id => $_ } $item->delivery_order_stock_entries;
1737 for my $line (@{ DO->unpack_stock_information(packed => $stock_info) }) {
1738 # lookup existing or make new
1739 my $obj = delete $existing{$line->{delivery_order_items_stock_id}}
1740 // SL::DB::DeliveryOrderItemsStock->new;
1743 $obj->$_($line->{$_}) for qw(bin_id warehouse_id chargenumber qty unit);
1744 $obj->bestbefore_as_date($line->{bestfbefore})
1745 if $line->{bestbefore} && $::instance_conf->get_show_bestbefore;
1746 push @save, $obj if $obj->qty;
1749 $item->delivery_order_stock_entries(@save);
1752 $item->assign_attributes(%$attr);
1755 my $texts = get_part_texts($item->part, $record->language_id);
1756 $item->longdescription($texts->{longdescription}) if !defined $attr->{longdescription};
1757 $item->project_id($record->globalproject_id) if !defined $attr->{project_id};
1758 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1766 # This is used to add one item
1768 my ($record, $attr) = @_;
1770 my $item = SL::DB::DeliveryOrderItem->new;
1772 # Remove attributes where the user left or set the inputs empty.
1773 # So these attributes will be undefined and we can distinguish them
1774 # from zero later on.
1775 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1776 delete $attr->{$_} if $attr->{$_} eq '';
1779 $item->assign_attributes(%$attr);
1781 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1782 $item->qty(1.0) if !$item->qty;
1783 $item->unit($part->unit) if !$item->unit;
1785 my ($price_src, $discount_src) = SL::Model::Record->get_best_price_and_discount_source($record, $item, ignore_given => 0);
1788 $new_attr{part} = $part;
1789 $new_attr{description} = $part->description if ! $item->description;
1790 $new_attr{qty} = 1.0 if ! $item->qty;
1791 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1792 $new_attr{sellprice} = $price_src->price;
1793 $new_attr{discount} = $discount_src->discount;
1794 $new_attr{active_price_source} = $price_src;
1795 $new_attr{active_discount_source} = $discount_src;
1796 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1797 $new_attr{project_id} = $record->globalproject_id;
1798 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1800 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1801 # they cannot be retrieved via custom_variables until the order/orderitem is
1802 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1803 $new_attr{custom_variables} = [];
1805 my $texts = get_part_texts(
1806 $part, $record->language_id,
1807 description => $new_attr{description},
1808 longdescription => $new_attr{longdescription}
1811 $item->assign_attributes(%new_attr, %{ $texts });
1816 # setup custom shipto from form
1818 # The dialog returns form variables starting with 'shipto' and cvars starting
1819 # with 'shiptocvar_'.
1820 # Mark it to be deleted if a shipto from master data is selected
1821 # (i.e. order has a shipto).
1822 # Else, update or create a new custom shipto. If the fields are empty, it
1823 # will not be saved on save.
1824 sub setup_custom_shipto_from_form {
1825 my ($self, $order, $form) = @_;
1827 if ($order->shipto) {
1828 $self->is_custom_shipto_to_delete(1);
1831 $order->custom_shipto
1832 || $order->custom_shipto(
1833 SL::DB::Shipto->new(module => 'DO', custom_variables => [])
1836 my $shipto_cvars = {
1837 map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}}
1838 grep { m{^shiptocvar_} }
1841 my $shipto_attrs = {
1842 map { $_ => delete $form->{$_}}
1847 $custom_shipto->assign_attributes(%$shipto_attrs);
1848 $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
1852 # get data for saving, printing, ..., that is not changed in the form
1854 # Only cvars for now.
1855 sub get_unalterable_data {
1858 foreach my $item (@{ $self->order->items }) {
1859 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1860 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1861 foreach my $var (@{ $item->cvars_by_config }) {
1862 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1864 $item->parse_custom_variable_values;
1870 # And delete items that are deleted in the form.
1874 set_record_link_conversions($self->order,
1875 delete $::form->{RECORD_TYPE_REF()}
1876 => delete $::form->{RECORD_ID()},
1877 delete $::form->{RECORD_ITEM_TYPE_REF()}
1878 => delete $::form->{RECORD_ITEM_ID()},
1881 my $items_to_delete = scalar @{ $self->item_ids_to_delete || [] }
1882 ? SL::DB::Manager::DeliveryOrderItem->get_all(where => [id => $self->item_ids_to_delete])
1885 SL::Model::Record->save($self->order,
1886 with_validity_token => {
1887 scope => SL::DB::ValidityToken::SCOPE_DELIVERY_ORDER_SAVE(),
1888 token => $::form->{form_validity_token}
1890 delete_custom_shipto => $self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty),
1891 items_to_delete => $items_to_delete,
1894 if ($::form->{email_journal_id}) {
1895 my $email_journal = SL::DB::EmailJournal->new(
1896 id => delete $::form->{email_journal_id}
1898 $email_journal->link_to_record_with_attachment(
1900 delete $::form->{email_attachment_id}
1904 delete $::form->{form_validity_token};
1907 sub reinit_after_new_order {
1911 $::form->{type} = $self->order->type;
1912 $self->type($self->init_type);
1913 $self->type_data($self->init_type_data);
1914 $self->cv($self->init_cv);
1917 $self->setup_custom_shipto_from_form($self->order, $::form);
1919 foreach my $item (@{$self->order->items_sorted}) {
1920 # set item ids to new fake id, to identify them as new items
1921 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1923 # trigger rendering values for second row as hidden, because they
1924 # are loaded only on demand. So we need to keep the values from the
1926 $item->{render_second_row} = 1;
1929 $self->order->prepare_stock_info($_) for $self->order->items;
1930 $self->get_unalterable_data();
1937 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1938 $self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
1939 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1940 $self->{all_languages} = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
1941 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1944 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1947 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1949 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1950 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1951 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1952 $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
1954 my $print_form = Form->new('');
1955 $print_form->{type} = $self->type;
1956 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1957 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1958 form => $print_form,
1959 options => {dialog_name_prefix => 'print_options.',
1963 no_opendocument => 0,
1967 foreach my $item (@{$self->order->orderitems}) {
1968 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1969 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1970 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1973 if ($self->order->${\ $self->type_data->properties("nr_key") } && $::instance_conf->get_webdav) {
1974 my $webdav = SL::Webdav->new(
1975 type => $self->type,
1976 number => $self->order->number,
1978 my @all_objects = $webdav->get_all_objects;
1979 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1981 link => File::Spec->catfile($_->full_filedescriptor),
1985 $self->{template_args}{in_out} = $self->type_data->properties("transfer");
1986 $self->{template_args}{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
1988 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1990 $::request->{layout}->use_javascript("${_}.js") for qw(
1991 kivi.SalesPurchase kivi.DeliveryOrder kivi.File calculate_qty kivi.Validator
1992 follow_up show_history
1994 $self->setup_edit_action_bar;
1997 sub setup_edit_action_bar {
1998 my ($self, %params) = @_;
2000 my $deletion_allowed = $self->type_data->show_menu("delete");
2001 my $may_edit_create = $::auth->assert(
2002 $self->type_data->rights('edit') || 'DOES_NOT_EXIST', 1
2005 my $confirmation_on_workflow = $self->order->delivered ? undef
2006 : ( $self->order->is_sales && $::instance_conf->get_sales_delivery_order_check_stocked) ? t8('This record has not been stocked out. Proceed?')
2007 : (!$self->order->is_sales && $::instance_conf->get_purchase_delivery_order_check_stocked) ? t8('This record has not been stocked in. Proceed?')
2010 for my $bar ($::request->layout->get('actionbar')) {
2015 id => 'save_action',
2016 call => [ 'kivi.DeliveryOrder.save', {
2018 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2019 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2021 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2022 : $self->order->delivered ? t8('This record has already been delivered.')
2026 t8('Save and Close'),
2027 id => 'save_and_close',
2028 call => [ 'kivi.DeliveryOrder.save', {
2030 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2031 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2033 { name => 'back_to_caller', value => 1 },
2036 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2037 : $self->order->delivered ? t8('This record has already been delivered.')
2041 t8('Mark as closed'),
2042 id => 'close_order',
2043 call => [ 'kivi.DeliveryOrder.close_order' ],
2044 confirm => t8('This will remove the delivery order from showing as open even if contents are not delivered. Proceed?'),
2045 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2046 : !$self->order->id ? t8('This object has not been saved yet.')
2047 : $self->order->closed ? t8('This record has already been closed.')
2052 call => [ 'kivi.DeliveryOrder.save', {
2053 action => 'save_as_new',
2054 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2056 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2057 : $self->type eq 'supplier_delivery_order' ? t8('Need a workflow for Supplier Delivery Order')
2058 : $self->type eq 'rma_delivery_order' ? t8('Need a workflow for RMA Delivery Order.')
2059 : !$self->order->id ? t8('This object has not been saved yet.')
2062 ], # end of combobox "Save"
2069 t8('Create Invoice'),
2070 call => [ 'kivi.DeliveryOrder.save', {
2071 action => 'workflow_invoice',
2072 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2074 only_if => $self->type_data->show_menu("workflow_invoice"),
2075 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2076 : !$self->order->id ? t8('This object has not been saved yet.')
2078 confirm => $confirmation_on_workflow,
2081 t8('Create Reclamation'),
2082 call => [ 'kivi.DeliveryOrder.save', {
2083 action => 'workflow_new_record',
2084 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2086 { name => 'to_type',
2087 value => $self->order->is_sales ? SALES_RECLAMATION_TYPE()
2088 : PURCHASE_RECLAMATION_TYPE() },
2091 only_if => $self->type_data->show_menu('workflow_reclamation'),
2092 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2093 : !$self->order->id ? t8('This object has not been saved yet.')
2095 confirm => $confirmation_on_workflow,
2098 ], # end of combobox "Workflow"
2105 t8('Save and preview PDF'),
2106 call => [ 'kivi.DeliveryOrder.save', {
2107 action => 'preview_pdf',
2108 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2109 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2111 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2114 t8('Save and print'),
2115 call => [ 'kivi.DeliveryOrder.show_print_options', {
2116 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2117 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate },
2119 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2122 t8('Save and E-mail'),
2123 id => 'save_and_email_action',
2124 call => [ 'kivi.DeliveryOrder.save', {
2125 action => 'save_and_show_email_dialog',
2126 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2127 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2129 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2130 : !$self->order->id ? t8('This object has not been saved yet.')
2134 t8('Download attachments of all parts'),
2135 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
2136 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2137 : !$self->order->id ? t8('This object has not been saved yet.')
2139 only_if => $::instance_conf->get_doc_storage,
2141 ], # end of combobox "Export"
2145 id => 'delete_action',
2146 call => [ 'kivi.DeliveryOrder.delete_order' ],
2147 confirm => $::locale->text('Do you really want to delete this object?'),
2148 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2149 : !$self->order->id ? t8('This object has not been saved yet.')
2150 : $self->order->delivered ? t8('The parts for this order have already been transferred')
2152 only_if => $self->type_data->show_menu("delete"),
2158 id => 'transfer_out_action',
2159 call => [ 'kivi.DeliveryOrder.save', {
2160 action => 'transfer_stock',
2162 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2163 : !$self->order->id ? t8('This object has not been saved yet.')
2164 : $self->order->delivered ? t8('The parts for this order have already been transferred')
2166 only_if => $self->type_data->properties('transfer') eq 'out',
2167 confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
2170 t8('Transfer out via default'),
2171 id => 'transfer_out_default_action',
2172 call => [ 'kivi.DeliveryOrder.save', {
2173 action => 'transfer_stock_default',
2175 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2176 : !$self->order->id ? t8('This object has not been saved yet.')
2177 : $self->order->delivered ? t8('The parts for this order have already been transferred')
2179 only_if => $self->type_data->properties('transfer') eq 'out',
2180 confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
2184 id => 'transfer_in_action',
2185 call => [ 'kivi.DeliveryOrder.save', {
2186 action => 'transfer_stock',
2188 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2189 : !$self->order->id ? t8('This object has not been saved yet.')
2190 : $self->order->delivered ? t8('The parts for this order have already been transferred')
2192 only_if => $self->type_data->properties('transfer') eq 'in',
2193 confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
2196 t8('Transfer in via default'),
2197 id => 'transfer_in_default_action',
2198 call => [ 'kivi.DeliveryOrder.save', {
2199 action => 'transfer_stock_default',
2201 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2202 : !$self->order->id ? t8('This object has not been saved yet.')
2203 : $self->order->delivered ? t8('The parts for this order have already been transferred')
2205 only_if => $self->type_data->properties('transfer') eq 'in',
2206 confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
2209 t8('Undo Transfer'),
2210 id => 'undo_transfer_action',
2211 call => [ 'kivi.DeliveryOrder.save', {
2212 action => 'undo_transfers',
2214 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2215 : !$self->order->id ? t8('This object has not been saved yet.')
2217 disabled => !$self->order->delivered,
2218 confirm => t8('Do you really want undo transfers the stock and set this order to undelivered?'),
2228 call => [ 'kivi.DeliveryOrder.follow_up_window' ],
2229 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2230 only_if => $::auth->assert('productivity', 1),
2234 call => [ 'set_history_window', $self->order->id, 'id' ],
2235 disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
2237 ], # end of combobox "more"
2243 my ($order, $pdf_ref, $params) = @_;
2247 my $print_form = Form->new('');
2248 $print_form->{type} = $order->type;
2249 $print_form->{formname} = $params->{formname} || $order->type;
2250 $print_form->{format} = $params->{format} || 'pdf';
2251 $print_form->{media} = $params->{media} || 'file';
2252 $print_form->{groupitems} = $params->{groupitems};
2253 $print_form->{printer_id} = $params->{printer_id};
2254 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
2256 $order->language($params->{language});
2257 $order->flatten_to_form($print_form, format_amounts => 1);
2261 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
2262 $template_ext = 'odt';
2263 $template_type = 'OpenDocument';
2266 # search for the template
2267 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
2268 name => $print_form->{formname},
2269 extension => $template_ext,
2270 email => $print_form->{media} eq 'email',
2271 language => $params->{language},
2272 printer_id => $print_form->{printer_id},
2275 if (!defined $template_file) {
2276 push @errors, $::locale->text(
2277 'Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.',
2278 join ', ', map { "'$_'"} @template_files
2282 return @errors if scalar @errors;
2284 $print_form->throw_on_error(sub {
2286 $print_form->prepare_for_printing;
2288 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
2289 format => $print_form->{format},
2290 template_type => $template_type,
2291 template => $template_file,
2292 variables => $print_form,
2293 variable_content_types => {
2294 longdescription => 'html',
2295 partnotes => 'html',
2300 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
2306 sub get_files_for_email_dialog {
2309 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
2311 return %files if !$::instance_conf->get_doc_storage;
2313 if ($self->order->id) {
2314 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
2315 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
2316 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
2317 $files{project_files} = [ SL::File->get_all( object_id => $self->order->globalproject_id, object_type => 'project', file_type => 'attachment') ];
2321 uniq_by { $_->{id} }
2323 +{ id => $_->part->id,
2324 partnumber => $_->part->partnumber }
2325 } @{$self->order->items_sorted};
2327 foreach my $part (@parts) {
2328 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
2329 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
2332 foreach my $key (keys %files) {
2333 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
2340 my ($self, $action) = @_;
2342 return '' if none { lc($action)} qw(add edit);
2343 return $self->type_data->text($action);
2346 sub get_item_cvpartnumber {
2347 my ($self, $item) = @_;
2349 return if !$self->search_cvpartnumber;
2350 return if !$self->order->customervendor;
2352 if ($self->cv eq 'vendor') {
2354 grep { $_->make eq $self->order->customervendor->id }
2355 @{$item->part->makemodels};
2356 $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
2357 } elsif ($self->cv eq 'customer') {
2359 grep { $_->customer_id eq $self->order->customervendor->id }
2360 @{$item->part->customerprices};
2361 $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
2365 sub get_part_texts {
2366 my ($part_or_id, $language_or_id, %defaults) = @_;
2368 my $part = ref($part_or_id) ? $part_or_id : SL::DB::Part->load_cached($part_or_id);
2369 my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
2371 description => $defaults{description} // $part->description,
2372 longdescription => $defaults{longdescription} // $part->notes,
2375 return $texts unless $language_id;
2377 my $translation = SL::DB::Manager::Translation->get_first(
2379 parts_id => $part->id,
2380 language_id => $language_id,
2383 $texts->{description} = $translation->translation if $translation && $translation->translation;
2384 $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
2390 return $_[0]->type_data->properties("nr_key");
2394 my ($self, $addition) = @_;
2396 my $number_type = $self->nr_key;
2397 my $snumbers = $number_type . '_' . $self->order->$number_type;
2399 SL::DB::History->new(
2400 trans_id => $self->order->id,
2401 employee_id => SL::DB::Manager::Employee->current->id,
2402 what_done => $self->order->type,
2403 snumbers => $snumbers,
2404 addition => $addition,
2408 sub store_pdf_to_webdav_and_filemanagement {
2409 my($order, $content, $filename, $variant) = @_;
2413 # copy file to webdav folder
2414 if ($order->number && $::instance_conf->get_webdav_documents) {
2415 my $webdav = SL::Webdav->new(
2416 type => $order->type,
2417 number => $order->number,
2419 my $webdav_file = SL::Webdav::File->new(
2421 filename => $filename,
2424 $webdav_file->store(data => \$content);
2427 push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
2430 if ($order->id && $::instance_conf->get_doc_storage) {
2432 SL::File->save(object_id => $order->id,
2433 object_type => $order->type,
2434 mime_type => 'application/pdf',
2435 source => 'created',
2436 file_type => 'document',
2437 file_name => $filename,
2438 file_contents => $content,
2439 print_variant => $variant);
2442 push @errors, t8('Storing PDF in storage backend failed: #1', $@);
2449 sub calculate_stock_in_out_from_stock_info {
2450 my ($self, $unit, $stock_info) = @_;
2452 return "" if !$unit;
2454 my %units_by_name = map { $_->name => $_ } @{ SL::DB::Manager::Unit->get_all };
2456 my $sum = sum0 map {
2457 $units_by_name{$_->{unit}}->convert_to($_->{qty}, $units_by_name{$unit})
2460 my $content = _format_number($sum, 2) . ' ' . $unit;
2465 sub calculate_stock_in_out {
2466 my ($self, $item, $stock_info) = @_;
2468 return "" if !$item->part || !$item->part->unit || !$item->unit;
2470 my $sum = sum0 map {
2471 $_->unit_obj->convert_to($_->qty, $item->unit_obj)
2472 } $item->delivery_order_stock_entries;
2474 my $content = _format_number($sum, 2);
2479 sub init_type_data {
2481 SL::DB::Helper::TypeDataProxy->new('SL::DB::DeliveryOrder', $self->order->record_type);
2484 sub init_valid_types {
2485 $_[0]->type_data->valid_types;
2496 SL::Controller::Order - controller for orders
2500 This is a new form to enter orders, completely rewritten with the use
2501 of controller and java script techniques.
2503 The aim is to provide the user a better experience and a faster workflow. Also
2504 the code should be more readable, more reliable and better to maintain.
2512 One input row, so that input happens every time at the same place.
2516 Use of pickers where possible.
2520 Possibility to enter more than one item at once.
2524 Item list in a scrollable area, so that the workflow buttons stay at
2529 Reordering item rows with drag and drop is possible. Sorting item rows is
2530 possible (by partnumber, description, qty, sellprice and discount for now).
2534 No C<update> is necessary. All entries and calculations are managed
2535 with ajax-calls and the page only reloads on C<save>.
2539 User can see changes immediately, because of the use of java script
2550 =item * C<SL/Controller/Order.pm>
2554 =item * C<template/webpages/delivery_order/form.html>
2558 =item * C<template/webpages/delivery_order/tabs/basic_data.html>
2560 Main tab for basic_data.
2562 This is the only tab here for now. "linked records" and "webdav" tabs are
2563 reused from generic code.
2567 =item * C<template/webpages/delivery_order/tabs/_business_info_row.html>
2569 For displaying information on business type
2571 =item * C<template/webpages/delivery_order/tabs/_item_input.html>
2573 The input line for items
2575 =item * C<template/webpages/delivery_order/tabs/_row.html>
2577 One row for already entered items
2579 =item * C<template/webpages/delivery_order/tabs/_tax_row.html>
2581 Displaying tax information
2585 =item * C<js/kivi.DeliveryOrder.js>
2587 java script functions
2597 =item * price sources: little symbols showing better price / better discount
2599 =item * select units in input row?
2601 =item * check for direct delivery (workflow sales order -> purchase order)
2603 =item * access rights
2605 =item * display weights
2609 =item * optional client/user behaviour
2611 (transactions has to be set - department has to be set -
2612 force project if enabled in client config - transport cost reminder)
2616 =head1 KNOWN BUGS AND CAVEATS
2622 Customer discount is not displayed as a valid discount in price source popup
2623 (this might be a bug in price sources)
2625 (I cannot reproduce this (Bernd))
2629 No indication that <shift>-up/down expands/collapses second row.
2633 Inline creation of parts is not currently supported
2637 Table header is not sticky in the scrolling area.
2641 Sorting does not include C<position>, neither does reordering.
2643 This behavior was implemented intentionally. But we can discuss, which behavior
2644 should be implemented.
2648 =head1 To discuss / Nice to have
2654 How to expand/collapse second row. Now it can be done clicking the icon or
2659 Possibility to select PriceSources in input row?
2663 This controller uses a (changed) copy of the template for the PriceSource
2664 dialog. Maybe there could be used one code source.
2668 Rounding-differences between this controller (PriceTaxCalculator) and the old
2669 form. This is not only a problem here, but also in all parts using the PTC.
2670 There exists a ticket and a patch. This patch should be testet.
2674 An indicator, if the actual inputs are saved (like in an
2675 editor or on text processing application).
2679 A warning when leaving the page without saving unchanged inputs.
2686 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>