1 package SL::Controller::Order;
4 use parent qw(SL::Controller::Base);
6 use SL::Helper::Flash qw(flash flash_later);
8 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
9 use SL::Locale::String qw(t8);
10 use SL::SessionFile::Random;
16 use SL::Util qw(trim);
18 use SL::DB::AdditionalBillingAddress;
22 use SL::DB::OrderItem;
26 use SL::DB::PartClassification;
27 use SL::DB::PartsGroup;
31 use SL::DB::Reclamation;
32 use SL::DB::RecordLink;
34 use SL::DB::Translation;
35 use SL::DB::EmailJournal;
36 use SL::DB::ValidityToken;
37 use SL::DB::Helper::RecordLink qw(set_record_link_conversions RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
38 use SL::DB::Helper::TypeDataProxy;
39 use SL::DB::Helper::Record qw(get_object_name_from_type get_class_from_type);
40 use SL::Model::Record;
41 use SL::DB::Order::TypeData qw(:types);
42 use SL::DB::DeliveryOrder::TypeData qw(:types);
43 use SL::DB::Reclamation::TypeData qw(:types);
45 use SL::Helper::CreatePDF qw(:all);
46 use SL::Helper::PrintOptions;
47 use SL::Helper::ShippedQty;
48 use SL::Helper::UserPreferences::DisplayPreferences;
49 use SL::Helper::UserPreferences::PositionsScrollbar;
50 use SL::Helper::UserPreferences::UpdatePositions;
51 use SL::Helper::UserPreferences::ItemInputPosition;
53 use SL::Controller::Helper::GetModels;
55 use List::Util qw(first sum0);
56 use List::UtilsBy qw(sort_by uniq_by);
57 use List::MoreUtils qw(uniq any none pairwise first_index);
58 use English qw(-no_match_vars);
63 use Rose::Object::MakeMethods::Generic
65 scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
66 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors
67 search_cvpartnumber show_update_button
68 part_picker_classification_ids
69 is_final_version type_data) ],
74 __PACKAGE__->run_before('check_auth',
75 except => [ qw(close_quotations) ]);
77 __PACKAGE__->run_before('check_auth_for_edit',
78 except => [ qw(edit price_popup load_second_rows close_quotations) ]);
79 __PACKAGE__->run_before('get_basket_info_from_from',
80 except => [ qw(close_quotations) ]);
92 if (!$::form->{form_validity_token}) {
93 $::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_ORDER_SAVE())->token;
98 title => $self->type_data->text('add'),
99 %{$self->{template_args}}
103 sub action_add_from_record {
105 my $from_type = $::form->{from_type};
106 my $from_id = $::form->{from_id};
108 die "No 'from_type' was given." unless ($from_type);
109 die "No 'from_id' was given." unless ($from_id);
112 if (defined($::form->{from_item_ids})) {
113 my %use_item = map { $_ => 1 } @{$::form->{from_item_ids}};
114 $flags{item_filter} = sub {
116 return %use_item{$item->{RECORD_ITEM_ID()}};
120 my $record = SL::Model::Record->get_record($from_type, $from_id);
121 my $order = SL::Model::Record->new_from_workflow($record, $self->type, %flags);
122 $self->order($order);
124 $self->reinit_after_new_order();
129 sub action_add_from_purchase_basket {
132 my $basket_item_ids = $::form->{basket_item_ids} || [];
133 my $vendor_item_ids = $::form->{vendor_item_ids} || [];
134 my $vendor_id = $::form->{vendor_id};
137 unless (scalar @{ $basket_item_ids} || scalar @{ $vendor_item_ids}) {
138 $self->js->flash('error', t8('There are no items selected'));
139 return $self->js->render();
142 my $order = SL::DB::Order->create_from_purchase_basket(
143 $basket_item_ids, $vendor_item_ids, $vendor_id
146 $self->order($order);
148 $self->reinit_after_new_order();
153 sub action_add_from_email_journal {
155 die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
160 sub action_edit_with_email_journal_workflow {
162 die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
163 $::form->{workflow_email_journal_id} = delete $::form->{email_journal_id};
164 $::form->{workflow_email_attachment_id} = delete $::form->{email_attachment_id};
165 $::form->{workflow_email_callback} = delete $::form->{callback};
167 $self->action_edit();
170 # edit an existing order
173 die "No 'id' was given." unless $::form->{id};
177 if ($self->order->is_sales && $::lx_office_conf{imap_client}->{enabled}) {
178 my $imap_client = SL::IMAPClient->new(%{$::lx_office_conf{imap_client}});
180 $imap_client->update_email_files_for_record(record => $self->order);
187 title => $self->type_data->text('edit'),
188 %{$self->{template_args}}
192 # edit a collective order (consisting of one or more existing orders)
193 sub action_edit_collective {
197 my @multi_ids = map {
198 $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
199 } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
201 # fall back to add if no ids are given
202 if (scalar @multi_ids == 0) {
207 # fall back to save as new if only one id is given
208 if (scalar @multi_ids == 1) {
209 $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
210 $self->action_save_as_new();
214 # make new order from given orders
215 my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
216 my $target_type = SALES_ORDER_TYPE();
217 my $order = SL::Model::Record->new_from_workflow_multi(\@multi_orders, $target_type, sort_sources_by => 'transdate');
218 $self->order($order);
219 $self->reinit_after_new_order();
228 SL::Model::Record->delete($self->order);
229 my $text = $self->type eq SALES_ORDER_INTAKE_TYPE() ? $::locale->text('The order intake has been deleted')
230 : $self->type eq SALES_ORDER_TYPE() ? $::locale->text('The order confirmation has been deleted')
231 : $self->type eq PURCHASE_ORDER_TYPE() ? $::locale->text('The order has been deleted')
232 : $self->type eq PURCHASE_ORDER_CONFIRMATION_TYPE() ? $::locale->text('The order confirmation has been deleted')
233 : $self->type eq SALES_QUOTATION_TYPE() ? $::locale->text('The quotation has been deleted')
234 : $self->type eq REQUEST_QUOTATION_TYPE() ? $::locale->text('The rfq has been deleted')
235 : $self->type eq PURCHASE_QUOTATION_INTAKE_TYPE() ? $::locale->text('The quotation intake has been deleted')
237 flash_later('info', $text);
239 my @redirect_params = (
244 $self->redirect_to(@redirect_params);
253 flash_later('info', $self->type_data->text('saved'));
256 if ($::form->{back_to_caller}) {
257 @redirect_params = $::form->{callback} ? ($::form->{callback})
258 : (controller => 'LoginScreen', action => 'user_login');
264 id => $self->order->id,
265 callback => $::form->{callback},
269 $self->redirect_to(@redirect_params);
272 # create new version and set version number
273 sub action_add_subversion {
276 SL::DB->client->with_transaction(
278 SL::Model::Record->increment_subversion($self->order);
284 $self->redirect_to(action => 'edit',
286 id => $self->order->id,
290 # save the order as new document and open it for edit
291 sub action_save_as_new {
294 my $order = $self->order;
297 $self->js->flash('error', t8('This object has not been saved yet.'));
298 return $self->js->render();
301 my $saved_order = SL::DB::Order->new(id => $order->id)->load;
303 # Create new record from current one
304 my $new_order = SL::Model::Record->clone_for_save_as_new($saved_order, $order);
305 $self->order($new_order);
307 # Warn on obsolete items
308 my @obsolete_positions = map { $_->position } grep { $_->part->obsolete } @{ $self->order->items_sorted };
309 flash_later('warning', t8('This record contains obsolete items at position #1', join ', ', @obsolete_positions)) if @obsolete_positions;
311 # Warn on order locked items if they are not wanted for this record type
312 if ($self->type_data->no_order_locked_parts) {
313 my @order_locked_positions = map { $_->position } grep { $_->part->order_locked } @{ $self->order->items_sorted };
314 flash_later('warning', t8('This record contains not orderable items at position #1', join ', ', @order_locked_positions)) if @order_locked_positions;
317 if (!$::form->{form_validity_token}) {
318 $::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_ORDER_SAVE())->token;
322 $self->action_save();
327 # This is called if "print" is pressed in the print dialog.
328 # If PDF creation was requested and succeeded, the pdf is offered for download
329 # via send_file (which uses ajax in this case).
335 $self->js_reset_order_and_item_ids_after_save;
337 my $redirect_url = $self->url_for(
340 id => $self->order->id,
343 my $format = $::form->{print_options}->{format};
344 my $media = $::form->{print_options}->{media};
345 my $formname = $::form->{print_options}->{formname};
346 my $copies = $::form->{print_options}->{copies};
347 my $groupitems = $::form->{print_options}->{groupitems};
348 my $printer_id = $::form->{print_options}->{printer_id};
350 # only PDF, OpenDocument & HTML for now
351 if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
352 flash_later('error', t8('Format \'#1\' is not supported yet/anymore.', $format));
353 return $self->js->redirect_to($redirect_url)->render;
356 # only screen or printer by now
357 if (none { $media eq $_ } qw(screen printer)) {
358 flash_later('error', t8('Media \'#1\' is not supported yet/anymore.', $media));
359 return $self->js->redirect_to($redirect_url)->render;
362 # create a form for generate_attachment_filename
363 my $form = Form->new;
364 $form->{$self->nr_key()} = $self->order->number;
365 $form->{type} = $self->type;
366 $form->{format} = $format;
367 $form->{formname} = $formname;
368 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
369 my $doc_filename = $form->generate_attachment_filename();
372 my @errors = $self->generate_doc(\$doc, { media => $media,
374 formname => $formname,
375 language => $self->order->language,
376 printer_id => $printer_id,
377 groupitems => $groupitems });
378 if (scalar @errors) {
379 flash_later('error', t8('Generating the document failed: #1', $errors[0]));
380 return $self->js->redirect_to($redirect_url)->render;
383 if ($media eq 'screen') {
385 flash_later('info', t8('The document has been created.'));
388 type => SL::MIME->mime_type_from_ext($doc_filename),
389 name => $doc_filename,
393 } elsif ($media eq 'printer') {
395 my $printer_id = $::form->{print_options}->{printer_id};
396 SL::DB::Printer->new(id => $printer_id)->load->print_document(
401 flash_later('info', t8('The document has been printed.'));
404 my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
405 if (scalar @warnings) {
406 flash_later('warning', $_) for @warnings;
409 $self->save_history('PRINTED');
411 $self->js->redirect_to($redirect_url)->render;
414 sub action_preview_pdf {
419 $self->js_reset_order_and_item_ids_after_save;
421 my $redirect_url = $self->url_for(
424 id => $self->order->id,
428 my $media = 'screen';
429 my $formname = $self->type;
432 # create a form for generate_attachment_filename
433 my $form = Form->new;
434 $form->{$self->nr_key()} = $self->order->number;
435 $form->{type} = $self->type;
436 $form->{format} = $format;
437 $form->{formname} = $formname;
438 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
439 my $pdf_filename = $form->generate_attachment_filename();
442 my @errors = $self->generate_doc(\$pdf, { media => $media,
444 formname => $formname,
445 language => $self->order->language,
447 if (scalar @errors) {
448 flash_later('error', t8('Conversion to PDF failed: #1', $errors[0]));
449 return $self->js->redirect_to($redirect_url)->render;
452 $self->save_history('PREVIEWED');
454 flash_later('info', t8('The PDF has been previewed'));
459 type => SL::MIME->mime_type_from_ext($pdf_filename),
460 name => $pdf_filename,
464 $self->js->redirect_to($redirect_url)->render;
467 # open the email dialog
468 sub action_save_and_show_email_dialog {
471 if (!$self->is_final_version) {
473 $self->js_reset_order_and_item_ids_after_save;
476 my $cv = $self->order->customervendor
477 or return $self->js->flash('error',
478 $self->type_data->properties('is_customer') ?
479 t8('Cannot send E-mail without customer given')
480 : t8('Cannot send E-mail without vendor given')
483 my $form = Form->new;
484 $form->{$self->nr_key()} = $self->order->number;
485 $form->{cusordnumber} = $self->order->cusordnumber;
486 $form->{formname} = $self->type;
487 $form->{type} = $self->type;
488 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
489 $form->{language_id} = $self->order->language->id if $self->order->language;
490 $form->{format} = 'pdf';
491 $form->{cp_id} = $self->order->contact->cp_id if $self->order->contact;
492 $form->{transaction_description} = $self->order->transaction_description;
496 ($self->order->contact ? $self->order->contact->cp_email : undef)
498 $email_form->{cc} = $cv->cc;
499 $email_form->{bcc} = join ', ', grep $_, $cv->bcc;
500 # Todo: get addresses from shipto, if any
501 $email_form->{subject} = $form->generate_email_subject();
502 $email_form->{attachment_filename} = $form->generate_attachment_filename();
503 $email_form->{message} = $form->generate_email_body();
504 $email_form->{js_send_function} = 'kivi.Order.send_email()';
506 my %files = $self->get_files_for_email_dialog();
508 my @employees_with_email = grep {
509 my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
510 $user && !!trim($user->get_config_value('email'));
511 } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
513 my $dialog_html = $self->render(
514 'common/_send_email_dialog', { output => 0 },
515 email_form => $email_form,
516 show_bcc => $::auth->assert('email_bcc', 'may fail'),
518 is_customer => $self->type_data->properties('is_customer'),
519 ALL_EMPLOYEES => \@employees_with_email,
520 ALL_PARTNER_EMAIL_ADDRESSES => $cv->get_all_email_addresses(),
521 is_final_version => $self->is_final_version,
525 ->run('kivi.Order.show_email_dialog', $dialog_html)
531 sub action_send_email {
534 if (!$self->is_final_version) {
539 $self->js->run('kivi.Order.close_email_dialog');
544 my @redirect_params = (
547 id => $self->order->id,
550 # Set the error handler to reload the document and display errors later,
551 # because the document is already saved and saving can have some side effects
552 # such as generating a document number, project number or record links,
553 # which will be up to date when the document is reloaded.
554 # Hint: Do not use "die" here and try to catch exceptions in subroutine
555 # calls. You should use "$::form->error" which respects the error handler.
556 local $::form->{__ERROR_HANDLER} = sub {
557 flash_later('error', $_[0]);
558 $self->redirect_to(@redirect_params);
559 $::dispatcher->end_request;
562 # move $::form->{email_form} to $::form
563 my $email_form = delete $::form->{email_form};
565 if ($email_form->{additional_to}) {
566 $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
567 delete $email_form->{additional_to};
570 my %field_names = (to => 'email');
571 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
573 # for Form::cleanup which may be called in Form::send_email
574 $::form->{cwd} = getcwd();
575 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
577 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
578 $::form->{media} = 'email';
580 $::form->{attachment_policy} //= '';
582 # Is an old file version available?
584 if ($::form->{attachment_policy} eq 'old_file') {
585 $attfile = SL::File->get_all(
586 object_id => $self->order->id,
587 object_type => $self->type,
588 print_variant => $::form->{formname},
592 if ($self->is_final_version && $::form->{attachment_policy} eq 'old_file' && !$attfile) {
593 $::form->error(t8('Re-sending a final version was requested, but the latest version of the document could not be found'));
596 if ( !$self->is_final_version
597 && $::form->{attachment_policy} ne 'no_file'
598 && !($::form->{attachment_policy} eq 'old_file' && $attfile)
601 my @errors = $self->generate_doc(\$doc, {
602 media => $::form->{media},
603 format => $::form->{print_options}->{format},
604 formname => $::form->{print_options}->{formname},
605 language => $self->order->language,
606 printer_id => $::form->{print_options}->{printer_id},
607 groupitems => $::form->{print_options}->{groupitems},
609 if (scalar @errors) {
610 $::form->error(t8('Generating the document failed: #1', $errors[0]));
613 my @warnings = $self->store_doc_to_webdav_and_filemanagement(
614 $doc, $::form->{attachment_filename}, $::form->{formname}
616 if (scalar @warnings) {
617 flash_later('warning', $_) for @warnings;
620 my $sfile = SL::SessionFile::Random->new(mode => "w");
621 $sfile->fh->print($doc);
624 $::form->{tmpfile} = $sfile->file_name;
625 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be
626 # called in Form::send_email
629 $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a
630 # linked record to the mail
631 $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
633 flash_later('info', t8('The email has been sent.'));
634 $self->save_history('MAILED');
636 # internal notes unless no email journal
637 unless ($::instance_conf->get_email_journal) {
638 my $intnotes = $self->order->intnotes;
639 $intnotes .= "\n\n" if $self->order->intnotes;
640 $intnotes .= t8('[email]') . "\n";
641 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(
643 precision => 'seconds') . "\n";
644 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
645 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
646 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
647 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
648 $intnotes .= t8('Message') . ": " . SL::HTML::Util->strip($::form->{message});
650 $self->order->update_attributes(intnotes => $intnotes);
653 if ($::instance_conf->get_lock_oe_subversions && !$self->is_final_version) {
655 if ($::instance_conf->get_doc_storage && $::form->{attachment_policy} ne 'no_file') {
656 # self is generated on the fly. form is a file from the dms
657 # TODO: for the case Filesystem and Webdav we want the real file from the filesystem
658 # for the nyi case DMS/CMIS we need a gloid or whatever the system offers (elo_id for ELO)
659 # DMS kivi version should have a record_link to email_journal
660 # the record link has to refer to the correct version -> helper table file <-> file_version
661 $file_id = $self->{file_id} || $::form->{file_id};
662 $::form->error("No file id") unless $file_id;
665 # email is sent -> set this version to final and link to journal and file
666 my $current_version = SL::DB::Manager::OrderVersion->get_all(where => [oe_id => $self->order->id, final_version => 0]);
667 $::form->error("Invalid version state") unless scalar @{ $current_version } == 1;
668 $current_version->[0]->update_attributes(file_id => $file_id,
669 email_journal_id => $::form->{email_journal_id},
673 $self->redirect_to(@redirect_params);
676 # open the periodic invoices config dialog
678 # If there are values in the form (i.e. dialog was opened before),
679 # then use this values. Create new ones, else.
680 sub action_show_periodic_invoices_config_dialog {
683 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
684 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
685 $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm',
686 order_value_periodicity => 'p', # = same as periodicity
687 start_date_as_date => $::form->{transdate_as_date} || $::form->current_date,
688 extend_automatically_by => 12,
690 email_subject => GenericTranslations->get(
691 language_id => $::form->{language_id},
692 translation_type =>"preset_text_periodic_invoices_email_subject"),
693 email_body => GenericTranslations->get(
694 language_id => $::form->{language_id},
695 translation_type => "salutation_general")
696 . GenericTranslations->get(
697 language_id => $::form->{language_id},
698 translation_type => "salutation_punctuation_mark") . "\n\n"
699 . GenericTranslations->get(
700 language_id => $::form->{language_id},
701 translation_type =>"preset_text_periodic_invoices_email_body"),
703 # for older configs, replace email preset text if not yet set.
704 $config->email_subject(GenericTranslations->get(
705 language_id => $::form->{language_id},
706 translation_type =>"preset_text_periodic_invoices_email_subject")
707 ) unless $config->email_subject;
709 $config->email_body(GenericTranslations->get(
710 language_id => $::form->{language_id},
711 translation_type => "salutation_general")
712 . GenericTranslations->get(
713 language_id => $::form->{language_id},
714 translation_type => "salutation_punctuation_mark") . "\n\n"
715 . GenericTranslations->get(
716 language_id => $::form->{language_id},
717 translation_type =>"preset_text_periodic_invoices_email_body")
718 ) unless $config->email_body;
720 $config->periodicity('m') if none { $_ eq $config->periodicity } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
721 $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
723 $::form->get_lists(printers => "ALL_PRINTERS",
724 charts => { key => 'ALL_CHARTS',
725 transdate => 'current_date' });
727 $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
729 if ($::form->{customer_id}) {
730 $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
731 my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
732 $::form->{postal_invoice} = $customer_object->postal_invoice;
733 $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
734 $config->send_email(0) if $::form->{postal_invoice};
737 $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
739 popup_js_close_function => 'kivi.Order.close_periodic_invoices_config_dialog()',
740 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
745 # assign the values of the periodic invoices config dialog
746 # as yaml in the hidden tag and set the status.
747 sub action_assign_periodic_invoices_config {
750 $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
752 my $config = { active => $::form->{active} ? 1 : 0,
753 terminated => $::form->{terminated} ? 1 : 0,
754 direct_debit => $::form->{direct_debit} ? 1 : 0,
755 periodicity => (any { $_ eq $::form->{periodicity} } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES) ? $::form->{periodicity} : 'm',
756 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
757 start_date_as_date => $::form->{start_date_as_date},
758 end_date_as_date => $::form->{end_date_as_date},
759 first_billing_date_as_date => $::form->{first_billing_date_as_date},
760 print => $::form->{print} ? 1 : 0,
761 printer_id => $::form->{print} ? $::form->{printer_id} * 1 : undef,
762 copies => $::form->{copies} * 1 ? $::form->{copies} : 1,
763 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
764 ar_chart_id => $::form->{ar_chart_id} * 1,
765 send_email => $::form->{send_email} ? 1 : 0,
766 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
767 email_recipient_address => $::form->{email_recipient_address},
768 email_sender => $::form->{email_sender},
769 email_subject => $::form->{email_subject},
770 email_body => $::form->{email_body},
773 my $periodic_invoices_config = SL::YAML::Dump($config);
775 my $status = $self->get_periodic_invoices_status($config);
778 ->remove('#order_periodic_invoices_config')
779 ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
780 ->run('kivi.Order.close_periodic_invoices_config_dialog')
781 ->html('#periodic_invoices_status', $status)
782 ->flash('info', t8('The periodic invoices config has been assigned.'))
786 sub action_get_has_active_periodic_invoices {
789 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
790 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
792 my $has_active_periodic_invoices =
793 $self->type eq SALES_ORDER_TYPE()
796 && (!$config->end_date || ($config->end_date > DateTime->today_local))
797 && $config->get_previous_billed_period_start_date;
799 $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
802 sub action_save_and_new_record {
804 my $to_type = $::form->{to_type};
805 my $to_controller = get_object_name_from_type($to_type);
808 flash_later('info', $self->type_data->text('saved'));
810 my %additional_params = ();
811 if ($::form->{only_selected_item_positions}) { # ids can be unset before save
812 my $item_positions = $::form->{selected_item_positions} || [];
813 my @from_item_ids = map { $self->order->items_sorted->[$_]->id } @$item_positions;
814 $additional_params{from_item_ids} = \@from_item_ids;
818 controller => $to_controller,
819 action => 'add_from_record',
821 from_id => $self->order->id,
822 from_type => $self->order->type,
823 email_journal_id => $::form->{workflow_email_journal_id},
824 email_attachment_id => $::form->{workflow_email_attachment_id},
825 callback => $::form->{workflow_email_callback},
830 # save the order and redirect to the frontend subroutine for a new
832 sub action_save_and_invoice {
835 $self->save_and_redirect_to(
836 controller => 'oe.pl',
837 action => 'oe_invoice_from_order',
838 email_journal_id => $::form->{workflow_email_journal_id},
839 email_attachment_id => $::form->{workflow_email_attachment_id},
840 callback => $::form->{workflow_email_callback},
844 sub action_save_and_invoice_for_advance_payment {
847 $self->save_and_redirect_to(
848 controller => 'oe.pl',
849 action => 'oe_invoice_from_order',
850 new_invoice_type => 'invoice_for_advance_payment',
851 email_journal_id => $::form->{workflow_email_journal_id},
852 email_attachment_id => $::form->{workflow_email_attachment_id},
853 callback => $::form->{workflow_email_callback},
857 sub action_save_and_final_invoice {
860 $self->save_and_redirect_to(
861 controller => 'oe.pl',
862 action => 'oe_invoice_from_order',
863 new_invoice_type => 'final_invoice',
864 email_journal_id => $::form->{workflow_email_journal_id},
865 email_attachment_id => $::form->{workflow_email_attachment_id},
866 callback => $::form->{workflow_email_callback},
870 # workflows to all types of this controller
871 sub action_save_and_order_workflow {
874 $self->save_and_redirect_to(
875 action => 'order_workflow',
877 to_type => $::form->{to_type},
878 use_shipto => $::form->{use_shipto},
879 email_journal_id => $::form->{workflow_email_journal_id},
880 email_attachment_id => $::form->{workflow_email_attachment_id},
881 callback => $::form->{workflow_email_callback},
885 # workflow from purchase order to ap transaction
886 sub action_save_and_ap_transaction {
889 $self->save_and_redirect_to(
890 controller => 'ap.pl',
891 action => 'add_from_purchase_order',
892 email_journal_id => $::form->{workflow_email_journal_id},
893 email_attachment_id => $::form->{workflow_email_attachment_id},
894 callback => $::form->{workflow_email_callback},
898 sub action_order_workflow {
903 my $destination_type = $::form->{to_type} ? $::form->{to_type} : '';
905 my $from_side = $self->order->is_sales ? 'sales' : 'purchase';
906 my $to_side = (any { $destination_type eq $_ } (SALES_ORDER_INTAKE_TYPE(), SALES_ORDER_TYPE(), SALES_QUOTATION_TYPE())) ? 'sales' : 'purchase';
908 # check for direct delivery
909 # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
911 if ( $from_side eq 'sales' && $to_side eq 'purchase'
912 && $::form->{use_shipto} && $self->order->shipto) {
913 $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
916 my $no_linked_records = (any { $destination_type eq $_ } (SALES_QUOTATION_TYPE(), REQUEST_QUOTATION_TYPE()))
917 && $from_side eq $to_side;
919 $self->order(SL::Model::Record->new_from_workflow($self->order, $destination_type, no_linked_records => $no_linked_records));
921 delete $::form->{id};
923 if (!$no_linked_records) {
924 $self->{converted_from_oe_id} = $self->order->{ RECORD_ID() };
925 $_ ->{converted_from_orderitems_id} = $_ ->{ RECORD_ITEM_ID() } for @{ $self->order->items_sorted };
928 if ($from_side eq 'sales' && $to_side eq 'purchase') {
929 if ($::form->{use_shipto}) {
930 $self->order->custom_shipto($custom_shipto) if $custom_shipto;
932 # remove any custom shipto if not wanted
933 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
937 $self->reinit_after_new_order();
942 # set form elements in respect to a changed customer or vendor
944 # This action is called on an change of the customer/vendor picker.
945 sub action_customer_vendor_changed {
948 $self->order(SL::Model::Record->update_after_customer_vendor_change($self->order));
952 my $cv_method = $self->cv;
954 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
955 $self->js->show('#cp_row');
957 $self->js->hide('#cp_row');
960 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
961 $self->js->show('#shipto_selection');
963 $self->js->hide('#shipto_selection');
966 if ($cv_method eq 'customer') {
967 my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
968 $self->js->$show_hide('#billing_address_row');
971 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
974 ->replaceWith('#order_cp_id', $self->build_contact_select)
975 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
976 ->replaceWith('#shipto_inputs ', $self->build_shipto_inputs)
977 ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
978 ->replaceWith('#business_info_row', $self->build_business_info_row)
979 ->val( '#order_taxzone_id', $self->order->taxzone_id)
980 ->val( '#order_taxincluded', $self->order->taxincluded)
981 ->val( '#order_currency_id', $self->order->currency_id)
982 ->val( '#order_payment_id', $self->order->payment_id)
983 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
984 ->val( '#order_intnotes', $self->order->intnotes)
985 ->val( '#order_language_id', $self->order->$cv_method->language_id)
986 ->focus( '#order_' . $self->cv . '_id')
987 ->run('kivi.Order.update_exchangerate');
989 $self->js_redisplay_amounts_and_taxes;
990 $self->js_redisplay_cvpartnumbers;
994 # called if a unit in an existing item row is changed
995 sub action_unit_changed {
998 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
999 my $item = $self->order->items_sorted->[$idx];
1001 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
1002 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
1007 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
1008 $self->js_redisplay_line_values;
1009 $self->js_redisplay_amounts_and_taxes;
1010 $self->js->render();
1013 # update item input row when a part ist picked
1014 sub action_update_item_input_row {
1017 delete $::form->{add_item}->{$_} for qw(create_part_type sellprice_as_number discount_as_percent);
1019 my $form_attr = $::form->{add_item};
1021 return unless $form_attr->{parts_id};
1023 my $record = $self->order;
1024 my $item = SL::DB::OrderItem->new(%$form_attr);
1025 $item->qty(1) if !$item->qty;
1026 $item->unit($item->part->unit);
1028 my ($price_src, $discount_src) = SL::Model::Record->get_best_price_and_discount_source($record, $item, ignore_given => 0);
1030 my $texts = get_part_texts($item->part, $record->language_id);
1033 ->val ('#add_item_unit', $item->unit)
1034 ->val ('#add_item_description', $texts->{description})
1035 ->val ('#add_item_sellprice_as_number', '')
1036 ->attr ('#add_item_sellprice_as_number', 'placeholder', $price_src->price_as_number)
1037 ->attr ('#add_item_sellprice_as_number', 'title', $price_src->source_description)
1038 ->val ('#add_item_discount_as_percent', '')
1039 ->attr ('#add_item_discount_as_percent', 'placeholder', $discount_src->discount_as_percent)
1040 ->attr ('#add_item_discount_as_percent', 'title', $discount_src->source_description)
1044 # add an item row for a new item entered in the input row
1045 sub action_add_item {
1048 delete $::form->{add_item}->{create_part_type};
1050 my $form_attr = $::form->{add_item};
1052 return unless $form_attr->{parts_id};
1054 my $item = new_item($self->order, $form_attr);
1056 $self->order->add_items($item);
1060 $self->get_item_cvpartnumber($item);
1062 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1063 my $row_as_html = $self->p->render('order/tabs/_row',
1069 if ($::form->{insert_before_item_id}) {
1071 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
1074 ->before('#row_table_footer', $row_as_html);
1077 if ( $item->part->is_assortment ) {
1078 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
1079 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
1080 my $attr = { parts_id => $assortment_item->parts_id,
1081 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
1082 unit => $assortment_item->unit,
1083 description => $assortment_item->part->description,
1085 my $item = new_item($self->order, $attr);
1087 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
1088 $item->discount(1) unless $assortment_item->charge;
1090 $self->order->add_items( $item );
1092 $self->get_item_cvpartnumber($item);
1093 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1094 my $row_as_html = $self->p->render('order/tabs/_row',
1099 if ($::form->{insert_before_item_id}) {
1101 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
1104 ->before('#row_table_footer', $row_as_html);
1110 ->val('.add_item_input', '')
1111 ->attr('.add_item_input', 'placeholder', '')
1112 ->attr('.add_item_input', 'title', '')
1113 ->attr('#add_item_qty_as_number', 'placeholder', '1')
1114 ->run('kivi.Order.init_row_handlers')
1115 ->run('kivi.Order.renumber_positions')
1116 ->focus('#add_item_parts_id_name');
1118 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
1120 # alternate scroll behaviour if item input below positions and unlimited scroll height
1121 $self->js->run('kivi.Order.scroll_page_after_row_insert', $item_id)
1122 if 0 == SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height
1123 && SL::Helper::UserPreferences::ItemInputPosition->new()->get_order_item_input_position
1124 // $::instance_conf->get_order_item_input_position;
1126 $self->js_redisplay_amounts_and_taxes;
1127 $self->js->render();
1130 # add item rows for multiple items at once
1131 sub action_add_multi_items {
1134 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
1135 return $self->js->render() unless scalar @form_attr;
1138 foreach my $attr (@form_attr) {
1139 my $item = new_item($self->order, $attr);
1141 if ( $item->part->is_assortment ) {
1142 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
1143 my $attr = { parts_id => $assortment_item->parts_id,
1144 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
1145 unit => $assortment_item->unit,
1146 description => $assortment_item->part->description,
1148 my $item = new_item($self->order, $attr);
1150 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
1151 $item->discount(1) unless $assortment_item->charge;
1156 $self->order->add_items(@items);
1160 foreach my $item (@items) {
1161 $self->get_item_cvpartnumber($item);
1162 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1163 my $row_as_html = $self->p->render('order/tabs/_row',
1169 if ($::form->{insert_before_item_id}) {
1171 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
1174 ->append('#row_table_id', $row_as_html);
1179 ->run('kivi.Part.close_picker_dialogs')
1180 ->run('kivi.Order.init_row_handlers')
1181 ->run('kivi.Order.renumber_positions')
1182 ->focus('#add_item_parts_id_name');
1184 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
1186 $self->js_redisplay_amounts_and_taxes;
1187 $self->js->render();
1190 # recalculate all linetotals, amounts and taxes and redisplay them
1191 sub action_recalc_amounts_and_taxes {
1196 $self->js_redisplay_line_values;
1197 $self->js_redisplay_amounts_and_taxes;
1198 $self->js->render();
1201 sub action_update_exchangerate {
1205 is_standard => $self->order->currency_id == $::instance_conf->get_currency_id,
1206 currency_name => $self->order->currency->name,
1207 exchangerate => $self->order->daily_exchangerate_as_null_number,
1210 $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
1213 # redisplay item rows if they are sorted by an attribute
1214 sub action_reorder_items {
1218 partnumber => sub { $_[0]->part->partnumber },
1219 description => sub { $_[0]->description },
1220 qty => sub { $_[0]->qty },
1221 sellprice => sub { $_[0]->sellprice },
1222 discount => sub { $_[0]->discount },
1223 cvpartnumber => sub { $_[0]->{cvpartnumber} },
1226 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1228 my $method = $sort_keys{$::form->{order_by}};
1229 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
1230 if ($::form->{sort_dir}) {
1231 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
1232 @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
1234 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
1237 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
1238 @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
1240 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
1244 ->run('kivi.Order.redisplay_items', \@to_sort)
1248 # show the popup to choose a price/discount source
1249 sub action_price_popup {
1252 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
1253 my $item = $self->order->items_sorted->[$idx];
1255 $self->render_price_dialog($item);
1258 # save the order in a session variable and redirect to the part controller
1259 sub action_create_part {
1262 my $previousform = $::auth->save_form_in_session(non_scalars => 1);
1264 my $callback = $self->url_for(
1265 action => 'return_from_create_part',
1266 type => $self->type, # type is needed for check_auth on return
1267 previousform => $previousform,
1270 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.'));
1272 my @redirect_params = (
1273 controller => 'Part',
1275 part_type => $::form->{add_item}->{create_part_type},
1276 callback => $callback,
1280 $self->redirect_to(@redirect_params);
1283 sub action_return_from_create_part {
1286 $self->{created_part} = SL::DB::Part->new(
1287 id => delete $::form->{new_parts_id}
1288 )->load if $::form->{new_parts_id};
1290 $::auth->restore_form_from_session(delete $::form->{previousform});
1292 $self->order($self->init_order);
1293 $self->reinit_after_new_order();
1295 if ($self->order->id) {
1296 $self->pre_render();
1299 title => $self->type_data->text('edit'),
1300 %{$self->{template_args}}
1307 # load the second row for one or more items
1309 # This action gets the html code for all items second rows by rendering a template for
1310 # the second row and sets the html code via client js.
1311 sub action_load_second_rows {
1314 $self->recalc() if $self->order->is_sales; # for margin calculation
1316 foreach my $item_id (@{ $::form->{item_ids} }) {
1317 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1318 my $item = $self->order->items_sorted->[$idx];
1320 $self->js_load_second_row($item, $item_id, 0);
1323 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
1325 $self->js->render();
1328 # update description, notes and sellprice from master data
1329 sub action_update_row_from_master_data {
1332 foreach my $item_id (@{ $::form->{item_ids} }) {
1333 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1334 my $item = $self->order->items_sorted->[$idx];
1335 my $texts = get_part_texts($item->part, $self->order->language_id);
1337 $item->description($texts->{description});
1338 $item->longdescription($texts->{longdescription});
1340 my ($price_src, $discount_src) = SL::Model::Record->get_best_price_and_discount_source($self->order, $item, ignore_given => 1);
1341 $item->sellprice($price_src->price);
1342 $item->active_price_source($price_src);
1343 $item->discount($discount_src->discount);
1344 $item->active_discount_source($discount_src);
1346 my $price_editable = $self->order->is_sales ? $::auth->assert('sales_edit_prices', 1) : $::auth->assert('purchase_edit_prices', 1);
1349 ->run('kivi.Order.set_price_and_source_text', $item_id, $price_src ->source, $price_src ->source_description, $item->sellprice_as_number, $price_editable)
1350 ->run('kivi.Order.set_discount_and_source_text', $item_id, $discount_src->source, $discount_src->source_description, $item->discount_as_percent, $price_editable)
1351 ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
1352 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
1353 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
1355 if ($self->search_cvpartnumber) {
1356 $self->get_item_cvpartnumber($item);
1357 $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
1362 $self->js_redisplay_line_values;
1363 $self->js_redisplay_amounts_and_taxes;
1365 $self->js->render();
1368 sub action_save_phone_note {
1371 my $phone_note = $self->parse_phone_note;
1372 my $is_new = !$phone_note->id;
1375 $self->order(SL::DB::Order->new(id => $self->order->id)->load);
1377 my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
1380 ->replaceWith('#phone-notes', $tab_as_html)
1381 ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
1382 ->flash('info', $is_new ? t8('Phone note has been created.') : t8('Phone note has been updated.'))
1387 sub action_delete_phone_note {
1390 my $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
1392 return $self->js->flash('error', t8('Phone note not found for this order.'))->render if !$phone_note;
1394 $phone_note->delete;
1395 $self->order(SL::DB::Order->new(id => $self->order->id)->load);
1397 my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
1400 ->replaceWith('#phone-notes', $tab_as_html)
1401 ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
1402 ->flash('info', t8('Phone note has been deleted.'))
1407 sub action_close_quotations {
1410 my @redirect_params = $::form->{callback} ? ($::form->{callback})
1411 : (controller => 'LoginScreen', action => 'user_login');
1413 if (!$::form->{ids} || !@{$::form->{ids}}) {
1414 flash_later('info', t8('Nothing selected!'));
1415 $self->redirect_to(@redirect_params);
1416 $::dispatcher->end_request;
1419 my $sales_quotations = SL::DB::Manager::Order->get_all(where => [id => $::form->{ids},
1420 or => [closed => 0, closed => undef],
1421 record_type => SALES_QUOTATION_TYPE()]);
1423 my $request_quotations = SL::DB::Manager::Order->get_all(where => [id => $::form->{ids},
1424 or => [closed => 0, closed => undef],
1425 record_type => REQUEST_QUOTATION_TYPE()]);
1427 $::auth->assert('sales_quotation_edit') if scalar @$sales_quotations;
1428 $::auth->assert('request_quotation_edit') if scalar @$request_quotations;
1430 my $employee_id = SL::DB::Manager::Employee->current->id;
1431 SL::DB->client->with_transaction(sub {
1432 SL::DB::Manager::Order->update_all(set => {closed => 1},
1433 where => [id => $::form->{ids}]);
1435 foreach my $quotation (@$sales_quotations, @$request_quotations) {
1436 SL::DB::History->new(
1437 trans_id => $quotation->id,
1438 employee_id => $employee_id,
1439 what_done => $quotation->type,
1440 snumbers => 'quonumber_' . $quotation->number,
1441 addition => 'SAVED',
1447 $::form->error(t8('Closing the selected quotations failed: #1', SL::DB->client->error));
1450 flash_later('info', t8('The selected quotations where closed.'));
1451 $self->redirect_to(@redirect_params);
1454 sub action_show_conversion_to_purchase_delivery_order_item_selection {
1457 my $items = $self->order->items_sorted;
1460 my @part_ids = uniq map { $_->{parts_id} } @$items;
1461 my %parts_by_id = map { ($_->id => $_) } @{ SL::DB::Manager::Part->get_all(where => [ id => \@part_ids ]) };
1462 my %make_models_by_id = map { ($_->parts_id => $_->model) } @{
1463 SL::DB::Manager::MakeModel->get_all(
1465 parts_id => \@part_ids,
1466 make => $::form->{order}->{vendor_id},
1470 foreach my $item (@$items) {
1471 $item->{partnumber} = $parts_by_id{ $item->{parts_id} }->partnumber;
1472 $item->{vendor_partnumber} = $make_models_by_id{ $item->{parts_id} };
1477 'order/tabs/_purchase_delivery_order_item_selection',
1483 sub js_load_second_row {
1484 my ($self, $item, $item_id, $do_parse) = @_;
1487 # Parse values from form (they are formated while rendering (template)).
1488 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1489 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
1490 foreach my $var (@{ $item->cvars_by_config }) {
1491 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1493 $item->parse_custom_variable_values;
1496 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
1499 ->html('#second_row_' . $item_id, $row_as_html)
1500 ->data('#second_row_' . $item_id, 'loaded', 1);
1503 sub js_redisplay_line_values {
1506 my $is_sales = $self->order->is_sales;
1508 # sales orders with margins
1513 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1514 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
1515 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
1516 ]} @{ $self->order->items_sorted };
1520 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1521 ]} @{ $self->order->items_sorted };
1525 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
1528 sub js_redisplay_amounts_and_taxes {
1531 if (scalar @{ $self->{taxes} }) {
1532 $self->js->show('#taxincluded_row_id');
1534 $self->js->hide('#taxincluded_row_id');
1537 if ($self->order->taxincluded) {
1538 $self->js->hide('#subtotal_row_id');
1540 $self->js->show('#subtotal_row_id');
1543 if ($self->order->is_sales) {
1544 my $is_neg = $self->order->marge_total < 0;
1546 ->html('#marge_total_id', $::form->format_amount(\%::myconfig, $self->order->marge_total, 2))
1547 ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
1548 ->action_if( $is_neg, 'addClass', '#marge_total_id', 'plus0')
1549 ->action_if( $is_neg, 'addClass', '#marge_percent_id', 'plus0')
1550 ->action_if( $is_neg, 'addClass', '#marge_percent_sign_id', 'plus0')
1551 ->action_if(!$is_neg, 'removeClass', '#marge_total_id', 'plus0')
1552 ->action_if(!$is_neg, 'removeClass', '#marge_percent_id', 'plus0')
1553 ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
1557 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
1558 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
1559 ->remove('.tax_row')
1560 ->insertBefore($self->build_tax_rows, '#amount_row_id');
1563 sub js_redisplay_cvpartnumbers {
1566 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1568 my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
1571 ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
1574 sub js_reset_order_and_item_ids_after_save {
1578 ->val('#id', $self->order->id)
1579 ->val('#converted_from_record_type_ref', '')
1580 ->val('#converted_from_record_id', '')
1581 ->val('#order_' . $self->nr_key(), $self->order->number);
1584 foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
1585 next if !$self->order->items_sorted->[$idx]->id;
1586 next if $form_item_id !~ m{^new};
1588 ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
1589 ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
1590 ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
1594 $self->js->val('[name="converted_from_record_item_type_refs[+]"]', '');
1595 $self->js->val('[name="converted_from_record_item_ids[+]"]', '');
1596 $self->js->val('[name="basket_item_ids[+]"]', '');
1603 sub init_valid_types {
1604 $_[0]->type_data->valid_types;
1610 my $type = $self->order->record_type;
1611 if (none { $type eq $_ } @{$self->valid_types}) {
1612 die "Not a valid type for order";
1621 return $self->type_data->properties('customervendor');
1624 sub init_search_cvpartnumber {
1627 my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1628 my $search_cvpartnumber;
1629 $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
1630 $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel() if $self->cv eq 'vendor';
1632 return $search_cvpartnumber;
1635 sub init_show_update_button {
1638 !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1649 sub init_all_price_factors {
1650 SL::DB::Manager::PriceFactor->get_all;
1653 sub init_part_picker_classification_ids {
1656 return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(
1657 where => $self->type_data->part_classification_query()) } ];
1660 sub init_is_final_version {
1661 # VALID States for current Sales Version
1662 # 1. save create version without email_id -> open
1663 # 2. send email set email_id for version 1 -> final
1664 # 3. save and subversion new version without email_id -> open
1665 # 4. send email set email_id for current subversion -> final
1666 # for all versions > 1 set postfix -2 .. -n for recordnumber
1667 return $::instance_conf->get_lock_oe_subversions ? # conf enabled
1668 $_[0]->order->id ? # is saved
1669 $_[0]->order->is_final_version : # is final
1670 undef : # is not final
1671 undef; # conf disabled
1676 $::auth->assert($self->type_data->rights('view'));
1679 sub check_auth_for_edit {
1681 $::auth->assert($self->type_data->rights('edit'));
1684 # build the selection box for contacts
1686 # Needed, if customer/vendor changed.
1687 sub build_contact_select {
1690 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1691 value_key => 'cp_id',
1692 title_key => 'full_name_dep',
1693 default => $self->order->cp_id,
1695 style => 'width: 300px',
1699 # build the selection box for the additional billing address
1701 # Needed, if customer/vendor changed.
1702 sub build_billing_address_select {
1705 return '' if $self->cv ne 'customer';
1707 select_tag('order.billing_address_id',
1708 [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
1710 title_key => 'displayable_id',
1711 default => $self->order->billing_address_id,
1713 style => 'width: 300px',
1717 # build the selection box for shiptos
1719 # Needed, if customer/vendor changed.
1720 sub build_shipto_select {
1723 select_tag('order.shipto_id',
1724 [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
1725 value_key => 'shipto_id',
1726 title_key => 'displayable_id',
1727 default => $self->order->shipto_id,
1729 style => 'width: 300px',
1733 # build the inputs for the cusom shipto dialog
1735 # Needed, if customer/vendor changed.
1736 sub build_shipto_inputs {
1739 my $content = $self->p->render('common/_ship_to_dialog',
1740 vc_obj => $self->order->customervendor,
1741 cs_obj => $self->order->custom_shipto,
1742 cvars => $self->order->custom_shipto->cvars_by_config,
1743 id_selector => '#order_shipto_id');
1745 div_tag($content, id => 'shipto_inputs');
1748 # render the info line for business
1750 # Needed, if customer/vendor changed.
1751 sub build_business_info_row
1753 $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
1756 # build the rows for displaying taxes
1758 # Called if amounts where recalculated and redisplayed.
1759 sub build_tax_rows {
1763 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
1764 $rows_as_html .= $self->p->render(
1765 'order/tabs/_tax_row',
1768 TAXINCLUDED => $self->order->taxincluded,
1769 QUOTATION => $self->order->quotation
1772 return $rows_as_html;
1776 sub render_price_dialog {
1777 my ($self, $record_item) = @_;
1779 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1783 'kivi.io.price_chooser_dialog',
1784 t8('Available Prices'),
1785 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1790 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1791 # $self->js->show('#dialog_flash_error');
1800 return if !$::form->{id};
1802 $self->order(SL::DB::Order->new(id => $::form->{id})->load);
1804 $self->reinit_after_new_order();
1806 return $self->order;
1809 # load or create a new order object
1811 # And assign changes from the form to this object.
1812 # If the order is loaded from db, check if items are deleted in the form,
1813 # remove them form the object and collect them for removing from db on saving.
1814 # Then create/update items from form (via make_item) and add them.
1818 # add_items adds items to an order with no items for saving, but they cannot
1819 # be retrieved via items until the order is saved. Adding empty items to new
1820 # order here solves this problem.
1822 if ($::form->{id}) {
1823 $order = SL::DB::Order->new(
1832 $order = SL::DB::Order->new(
1834 record_type => $::form->{type},
1835 currency_id => $::instance_conf->get_currency_id(),
1837 $order = SL::Model::Record->update_after_new($order)
1840 my $cv_id_method = $order->type_data->properties('customervendor'). '_id';
1841 if (!$::form->{id} && $::form->{$cv_id_method}) {
1842 $order->$cv_id_method($::form->{$cv_id_method});
1843 $order = SL::Model::Record->update_after_customer_vendor_change($order);
1846 # don't assign hashes as objects
1847 my $form_orderitems = delete $::form->{order}->{orderitems};
1848 my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1850 $order->assign_attributes(%{$::form->{order}});
1852 # restore form values
1853 $::form->{order}->{orderitems} = $form_orderitems;
1854 $::form->{order}->{periodic_invoices_config} = $form_periodic_invoices_config;
1856 $self->setup_custom_shipto_from_form($order, $::form);
1859 my $periodic_invoices_config_attrs = $form_periodic_invoices_config ?
1860 SL::YAML::Load($form_periodic_invoices_config)
1863 my $periodic_invoices_config =
1864 $order->periodic_invoices_config
1865 || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
1866 $periodic_invoices_config->assign_attributes(
1867 %$periodic_invoices_config_attrs
1871 # remove deleted items
1872 $self->item_ids_to_delete([]);
1873 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1874 my $item = $order->orderitems->[$idx];
1875 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1876 splice @{$order->orderitems}, $idx, 1;
1877 push @{$self->item_ids_to_delete}, $item->id;
1883 foreach my $form_attr (@{$form_orderitems}) {
1884 my $item = make_item($order, $form_attr);
1885 $item->position($pos);
1889 $order->add_items(grep {!$_->id} @items);
1894 # create or update items from form
1896 # Make item objects from form values. For items already existing read from db.
1897 # Create a new item else. And assign attributes.
1899 my ($record, $attr) = @_;
1902 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1904 my $is_new = !$item;
1906 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1907 # they cannot be retrieved via custom_variables until the order/orderitem is
1908 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1909 $item ||= SL::DB::OrderItem->new(custom_variables => []);
1911 $item->assign_attributes(%$attr);
1914 my $texts = get_part_texts($item->part, $record->language_id);
1915 $item->longdescription($texts->{longdescription}) if !defined $attr->{longdescription};
1916 $item->project_id($record->globalproject_id) if !defined $attr->{project_id};
1917 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1925 # This is used to add one item
1927 my ($record, $attr) = @_;
1929 my $item = SL::DB::OrderItem->new;
1931 # Remove attributes where the user left or set the inputs empty.
1932 # So these attributes will be undefined and we can distinguish them
1933 # from zero later on.
1934 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1935 delete $attr->{$_} if $attr->{$_} eq '';
1938 $item->assign_attributes(%$attr);
1939 $item->qty(1.0) if !$item->qty;
1940 $item->unit($item->part->unit) if !$item->unit;
1942 my ($price_src, $discount_src) = SL::Model::Record->get_best_price_and_discount_source($record, $item, ignore_given => 0);
1944 my $texts = get_part_texts($item->part, $record->language_id);
1947 $new_attr{description} = $texts->{description} if ! $item->description;
1948 $new_attr{qty} = 1.0 if ! $item->qty;
1949 $new_attr{price_factor_id} = $item->part->price_factor_id if ! $item->price_factor_id;
1950 $new_attr{sellprice} = $price_src->price;
1951 $new_attr{discount} = $discount_src->discount;
1952 $new_attr{active_price_source} = $price_src;
1953 $new_attr{active_discount_source} = $discount_src;
1954 $new_attr{longdescription} = $texts->{longdescription} if ! defined $attr->{longdescription};
1955 $new_attr{project_id} = $record->globalproject_id;
1956 $new_attr{lastcost} = $record->is_sales ? $item->part->lastcost : 0;
1958 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1959 # they cannot be retrieved via custom_variables until the order/orderitem is
1960 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1961 $new_attr{custom_variables} = [];
1963 $item->assign_attributes(%new_attr);
1968 sub get_basket_info_from_from {
1971 my $order = $self->order;
1972 my $basket_item_ids = $::form->{basket_item_ids};
1973 if (scalar @{ $basket_item_ids || [] }) {
1974 for my $idx (0 .. $#{ $order->items_sorted }) {
1975 my $order_item = $order->items_sorted->[$idx];
1976 $order_item->{basket_item_id} = $basket_item_ids->[$idx];
1981 # setup custom shipto from form
1983 # The dialog returns form variables starting with 'shipto' and cvars starting
1984 # with 'shiptocvar_'.
1985 # Mark it to be deleted if a shipto from master data is selected
1986 # (i.e. order has a shipto).
1987 # Else, update or create a new custom shipto. If the fields are empty, it
1988 # will not be saved on save.
1989 sub setup_custom_shipto_from_form {
1990 my ($self, $order, $form) = @_;
1992 if ($order->shipto) {
1993 $self->is_custom_shipto_to_delete(1);
1996 $order->custom_shipto
1997 || $order->custom_shipto(
1998 SL::DB::Shipto->new(module => 'OE', custom_variables => [])
2001 my $shipto_cvars = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
2002 my $shipto_attrs = {map { $_ => delete $form->{$_}} grep { m{^shipto} } keys %$form};
2004 $custom_shipto->assign_attributes(%$shipto_attrs);
2005 $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
2009 # recalculate prices and taxes
2011 # Using the PriceTaxCalculator. Store linetotals in the item objects.
2015 my %pat = $self->order->calculate_prices_and_taxes();
2017 $self->{taxes} = [];
2018 foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
2019 my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
2021 push(@{ $self->{taxes} }, { amount => $pat{taxes_by_tax_id}->{$tax_id},
2022 netamount => $netamount,
2023 tax => SL::DB::Tax->new(id => $tax_id)->load });
2025 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
2028 # get data for saving, printing, ..., that is not changed in the form
2030 # Only cvars for now.
2031 sub get_unalterable_data {
2034 foreach my $item (@{ $self->order->items }) {
2035 # autovivify all cvars that are not in the form (cvars_by_config can do it).
2036 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
2037 foreach my $var (@{ $item->cvars_by_config }) {
2038 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
2040 $item->parse_custom_variable_values;
2044 # parse new or updated phone note
2046 # And put them into the order object.
2047 sub parse_phone_note {
2050 if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
2051 die t8('Phone note needs a subject and a body.');
2055 if ($::form->{phone_note}->{id}) {
2056 $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
2057 die t8('Phone note not found for this order.') if !$phone_note;
2060 $phone_note = SL::DB::Note->new() if !$phone_note;
2061 my $is_new = !$phone_note->id;
2063 $phone_note->assign_attributes(%{ $::form->{phone_note} },
2064 trans_id => $self->order->id,
2065 trans_module => 'oe',
2066 employee => SL::DB::Manager::Employee->current);
2068 $self->order->add_phone_notes($phone_note) if $is_new;
2072 sub check_if_periodic_invoices_contact_matches_customer {
2075 return if !$self->order->is_type(SL::DB::Order::SALES_ORDER_TYPE());
2077 my $cfg = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $self->order->id);
2078 return if !$cfg || !$cfg->email_recipient_contact_id;
2080 my $contact = SL::DB::Manager::Contact->find_by(cp_id => $cfg->email_recipient_contact_id);
2081 return if !$contact;
2083 if ($contact->cp_cv_id != $self->order->customer_id) {
2084 $cfg->update_attributes(email_recipient_contact_id => undef);
2090 # And delete items that are deleted in the form.
2094 my $is_new = !$self->order->id;
2096 $self->parse_phone_note if $::form->{phone_note}->{subject} || $::form->{phone_note}->{body};
2098 # Test for order locked items if they are not wanted for this record type.
2099 if ($self->type_data->no_order_locked_parts) {
2100 my @order_locked_positions = map { $_->position } grep { $_->part->order_locked } @{ $self->order->items_sorted };
2101 die t8('This record contains not orderable items at position #1', join ', ', @order_locked_positions) if @order_locked_positions;
2104 # create first version if none exists
2105 $self->order->add_order_version(SL::DB::OrderVersion->new(version => 1)) if !$self->order->order_version;
2107 set_record_link_conversions($self->order,
2108 delete $::form->{RECORD_TYPE_REF()}
2109 => delete $::form->{RECORD_ID()},
2110 delete $::form->{RECORD_ITEM_TYPE_REF()}
2111 => delete $::form->{RECORD_ITEM_ID()},
2114 my @converted_from_oe_ids;
2115 if ($self->order->{RECORD_TYPE_REF()} eq 'SL::DB::Order'
2116 && $self->order->{RECORD_ID()}) {
2117 @converted_from_oe_ids = split ' ', $self->order->{RECORD_ID()};
2120 # check for purchase basket items
2121 my %basket_item_id_to_orderitem =
2122 map { $_->{basket_item_id} => $_ }
2123 grep { $_->{basket_item_id} ne '' }
2124 $self->order->orderitems;
2125 my @basket_item_ids = keys %basket_item_id_to_orderitem;
2126 if (scalar @basket_item_ids) {
2127 my $basket_items = SL::DB::Manager::PurchaseBasketItem->get_all(
2128 where => [ id => \@basket_item_ids ]);
2129 if (scalar @$basket_items != scalar @basket_item_ids) {
2130 my %basket_item_exists = map { $_->id => 1 } @$basket_items;
2131 my @missing_for_positions =
2132 map { $_->position }
2133 map { $basket_item_id_to_orderitem{$_} }
2134 grep { !$basket_item_exists{$_} }
2136 return [t8('Purchase basket item not existing any more for position(s): #1.',
2137 join(',', @missing_for_positions))];
2141 my $objects_to_close = scalar @converted_from_oe_ids
2142 ? SL::DB::Manager::Order->get_all(where => [
2143 id => \@converted_from_oe_ids,
2144 or => [ record_type => SALES_QUOTATION_TYPE(),
2145 record_type => REQUEST_QUOTATION_TYPE(),
2146 (record_type => PURCHASE_QUOTATION_INTAKE_TYPE()) x $self->order->is_type(PURCHASE_ORDER_TYPE()),
2147 (record_type => PURCHASE_ORDER_TYPE()) x $self->order->is_type(PURCHASE_ORDER_CONFIRMATION_TYPE()) ]
2151 my $items_to_delete = scalar @{ $self->item_ids_to_delete || [] }
2152 ? SL::DB::Manager::OrderItem->get_all(where => [id => $self->item_ids_to_delete])
2155 SL::Model::Record->save($self->order,
2156 with_validity_token => { scope => SL::DB::ValidityToken::SCOPE_ORDER_SAVE(), token => $::form->{form_validity_token} },
2157 delete_custom_shipto => $self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty),
2158 items_to_delete => $items_to_delete,
2159 objects_to_close => $objects_to_close,
2160 link_requirement_specs_linking_to_created_from_objects => \@converted_from_oe_ids,
2161 set_project_in_linked_requirement_specs => 1,
2164 if ($::form->{email_journal_id}) {
2165 my $email_journal = SL::DB::EmailJournal->new(
2166 id => delete $::form->{email_journal_id}
2168 $email_journal->link_to_record_with_attachment(
2170 delete $::form->{email_attachment_id}
2174 if ($is_new && $self->order->is_sales && $::lx_office_conf{imap_client}->{enabled}) {
2175 my $imap_client = SL::IMAPClient->new(%{$::lx_office_conf{imap_client}});
2177 $imap_client->create_folder_for_record(record => $self->order);
2181 $self->check_if_periodic_invoices_contact_matches_customer;
2183 delete $::form->{form_validity_token};
2186 sub reinit_after_new_order {
2190 $::form->{type} = $self->order->type;
2191 $self->type($self->init_type);
2192 $self->type_data($self->init_type_data);
2193 $self->cv($self->init_cv);
2196 $self->setup_custom_shipto_from_form($self->order, $::form);
2198 foreach my $item (@{$self->order->items_sorted}) {
2199 # set item ids to new fake id, to identify them as new items
2200 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
2202 # trigger rendering values for second row as hidden, because they
2203 # are loaded only on demand. So we need to keep the values from the
2205 $item->{render_second_row} = 1;
2208 # Warn on order locked items if they are not wanted for this record type
2209 if ($self->type_data->no_order_locked_parts) {
2210 my @order_locked_positions =
2211 map { $_->position }
2212 grep { $_->part->order_locked }
2213 @{ $self->order->items_sorted };
2214 flash('warning', t8(
2215 'This record contains not orderable items at position #1',
2216 join ', ', @order_locked_positions)
2217 ) if @order_locked_positions;
2220 $self->get_unalterable_data();
2227 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
2228 $self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
2229 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
2230 $self->{all_languages} = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
2231 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
2234 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
2237 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
2239 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_valid($self->order->delivery_term_id);
2240 $self->{all_statuses} = SL::DB::Manager::OrderStatus->get_all_sorted(where => [ or => [ id => $self->order->order_status_id,
2241 obsolete => 0, ] ] );
2242 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
2243 $self->{periodic_invoices_status} = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
2244 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
2245 $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
2247 my $print_form = Form->new('');
2248 $print_form->{type} = $self->type;
2249 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
2250 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
2251 form => $print_form,
2252 options => {dialog_name_prefix => 'print_options.',
2256 no_opendocument => 0,
2260 foreach my $item (@{$self->order->orderitems}) {
2261 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
2262 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
2263 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
2266 if (any { $self->type eq $_ } (SALES_ORDER_INTAKE_TYPE(), SALES_ORDER_TYPE(), PURCHASE_ORDER_TYPE(), PURCHASE_ORDER_CONFIRMATION_TYPE())) {
2267 # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
2268 # Do not use write_to_objects to prevent order->delivered to be set, because this should be
2269 # the value from db, which can be set manually or is set when linked delivery orders are saved.
2270 SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
2273 if ($self->order->number && $::instance_conf->get_webdav) {
2274 my $webdav = SL::Webdav->new(
2275 type => $self->type,
2276 number => $self->order->number,
2278 my @all_objects = $webdav->get_all_objects;
2279 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
2281 link => File::Spec->catfile($_->full_filedescriptor),
2285 if ( (any { $self->type eq $_ } (SALES_QUOTATION_TYPE(), SALES_ORDER_INTAKE_TYPE(), SALES_ORDER_TYPE()))
2286 && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
2287 $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
2289 $self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
2290 $self->{template_args}->{order_item_input_position} = SL::Helper::UserPreferences::ItemInputPosition->new()->get_order_item_input_position
2291 // $::instance_conf->get_order_item_input_position;
2293 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
2295 $self->{template_args}->{num_phone_notes} = scalar @{ $self->order->phone_notes || [] };
2297 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File
2298 edit_periodic_invoices_config calculate_qty follow_up show_history);
2299 $self->setup_edit_action_bar;
2302 sub setup_edit_action_bar {
2303 my ($self, %params) = @_;
2308 push @valid, "kivi.Order.check_duplicate_parts" if $::instance_conf->get_order_warn_duplicate_parts;
2309 push @valid, "kivi.Order.check_valid_reqdate" if $::instance_conf->get_order_warn_no_deliverydate;
2310 my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
2311 my @req_cusordnumber = qw(kivi.Order.check_cusordnumber_presence) x(( any {$self->type eq $_} (SALES_ORDER_INTAKE_TYPE(), SALES_ORDER_TYPE()) ) && $::instance_conf->get_order_warn_no_cusordnumber);
2313 my $has_invoice_for_advance_payment;
2314 if ($self->order->id && $self->type eq SALES_ORDER_TYPE()) {
2315 my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
2316 $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
2319 my $has_final_invoice;
2320 if ($self->order->id && $self->type eq SALES_ORDER_TYPE()) {
2321 my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
2322 $has_final_invoice = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
2325 my $may_edit_create = $::auth->assert($self->type_data->rights('edit'), 'may fail');
2327 my $is_final_version = $self->is_final_version;
2329 for my $bar ($::request->layout->get('actionbar')) {
2334 call => [ 'kivi.Order.save', {
2336 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2337 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2339 checks => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
2340 @req_trans_cost_art, @req_cusordnumber,
2342 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2343 : $is_final_version ? t8('This record is the final version. Please create a new sub-version') : undef,
2346 t8('Save and Close'),
2347 call => [ 'kivi.Order.save', {
2349 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2350 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2352 { name => 'back_to_caller', value => 1 },
2355 checks => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
2356 @req_trans_cost_art, @req_cusordnumber,
2358 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2359 : $is_final_version ? t8('This record is the final version. Please create a new sub-version') : undef,
2362 t8('Create Sub-Version'),
2363 call => [ 'kivi.Order.save', { action => 'add_subversion' } ],
2364 only_if => $::instance_conf->get_lock_oe_subversions,
2365 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2366 : !$is_final_version ? t8('This sub-version is not yet finalized')
2371 call => [ 'kivi.Order.save', {
2372 action => 'save_as_new',
2373 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2375 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2376 @req_trans_cost_art, @req_cusordnumber,
2378 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2379 : !$self->order->id ? t8('This object has not been saved yet.')
2382 ], # end of combobox "Save"
2389 t8('Save and Quotation'),
2390 call => [ 'kivi.submit_ajax_form', $self->url_for(action => "save_and_order_workflow", to_type => SALES_QUOTATION_TYPE()), '#order_form' ],
2391 checks => [ @valid, @req_trans_cost_art, @req_cusordnumber ],
2392 only_if => $self->type_data->show_menu('save_and_quotation'),
2393 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2397 call => [ 'kivi.Order.purchase_check_for_direct_delivery', { to_type => REQUEST_QUOTATION_TYPE() } ],
2398 checks => [ @valid ],
2399 only_if => $self->type_data->show_menu('save_and_rfq'),
2400 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2403 t8('Save and Purchase Quotation Intake'),
2404 call => [ 'kivi.submit_ajax_form', $self->url_for(action => "save_and_order_workflow", to_type => PURCHASE_QUOTATION_INTAKE_TYPE()), '#order_form' ],
2405 only_if => $self->type_data->show_menu('save_and_purchase_quotation_intake'),
2406 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2409 t8('Save and Sales Order Intake'),
2410 call => [ 'kivi.submit_ajax_form', $self->url_for(action => "save_and_order_workflow", to_type => SALES_ORDER_INTAKE_TYPE()), '#order_form' ],
2411 only_if => $self->type_data->show_menu('save_and_sales_order_intake'),
2412 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2415 t8('Save and Sales Order Confirmation'),
2416 call => [ 'kivi.submit_ajax_form', $self->url_for(action => "save_and_order_workflow", to_type => SALES_ORDER_TYPE()), '#order_form' ],
2417 checks => [ @valid, @req_trans_cost_art ],
2418 only_if => $self->type_data->show_menu('save_and_sales_order'),
2419 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2422 t8('Save and Purchase Order'),
2423 call => [ 'kivi.Order.purchase_check_for_direct_delivery', { to_type => PURCHASE_ORDER_TYPE() } ],
2424 checks => [ @valid, @req_trans_cost_art, @req_cusordnumber ],
2425 only_if => $self->type_data->show_menu('save_and_purchase_order'),
2426 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2429 t8('Save and Purchase Order Confirmation'),
2430 call => [ 'kivi.Order.purchase_check_for_direct_delivery', { to_type => PURCHASE_ORDER_CONFIRMATION_TYPE() } ],
2431 checks => [ @valid, @req_trans_cost_art, @req_cusordnumber ],
2432 only_if => $self->type_data->show_menu('save_and_purchase_order_confirmation'),
2433 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2436 t8('Save and Sales Delivery Order'),
2437 call => [ 'kivi.Order.save', {
2438 action => 'save_and_new_record',
2439 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2440 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2442 { name => 'to_type', value => SALES_DELIVERY_ORDER_TYPE() },
2445 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2446 @req_trans_cost_art, @req_cusordnumber,
2448 only_if => $self->type_data->show_menu('save_and_sales_delivery_order'),
2449 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2452 t8('Save and Purchase Delivery Order'),
2453 call => [ 'kivi.Order.save', {
2454 action => 'save_and_new_record',
2455 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2456 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2458 { name => 'to_type', value => PURCHASE_DELIVERY_ORDER_TYPE() },
2461 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2462 @req_trans_cost_art, @req_cusordnumber,
2464 only_if => $self->type_data->show_menu('save_and_purchase_delivery_order'),
2465 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2468 t8('Save and Purchase Delivery Order with item selection'),
2470 'kivi.Order.show_purchase_delivery_order_select_items', {
2471 action => 'save_and_new_record',
2472 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2473 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2475 { name => 'to_type', value => PURCHASE_DELIVERY_ORDER_TYPE() },
2478 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2479 only_if => $self->type_data->show_menu('save_and_purchase_delivery_order'),
2480 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2483 t8('Save and Supplier Delivery Order'),
2484 call => [ 'kivi.Order.save', {
2485 action => 'save_and_new_record',
2486 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2487 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2489 { name => 'to_type', value => SUPPLIER_DELIVERY_ORDER_TYPE() },
2492 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2493 @req_trans_cost_art, @req_cusordnumber,
2495 only_if => $self->type_data->show_menu('save_and_supplier_delivery_order'),
2496 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2499 t8('Save and Reclamation'),
2500 call => [ 'kivi.Order.save', {
2501 action => 'save_and_new_record',
2502 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2503 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2505 { name => 'to_type',
2506 value => $self->order->is_sales ? SALES_RECLAMATION_TYPE()
2507 : PURCHASE_RECLAMATION_TYPE() },
2510 only_if => $self->type_data->show_menu('save_and_reclamation'),
2511 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2514 t8('Save and Invoice'),
2515 call => [ 'kivi.Order.save', {
2516 action => 'save_and_invoice',
2517 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2518 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2520 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2521 @req_trans_cost_art, @req_cusordnumber,
2523 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2524 only_if => $self->type_data->show_menu('save_and_invoice'),
2527 ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
2528 call => [ 'kivi.Order.save', {
2529 action => 'save_and_invoice_for_advance_payment',
2530 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2532 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2533 @req_trans_cost_art, @req_cusordnumber,
2535 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2536 : $has_final_invoice ? t8('This order has already a final invoice.')
2538 only_if => $self->type_data->show_menu('save_and_invoice_for_advance_payment'),
2541 t8('Save and Final Invoice'),
2542 call => [ 'kivi.Order.save', {
2543 action => 'save_and_final_invoice',
2544 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2546 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2547 @req_trans_cost_art, @req_cusordnumber,
2549 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2550 : $has_final_invoice ? t8('This order has already a final invoice.')
2552 only_if => $self->type_data->show_menu('save_and_final_invoice') && $has_invoice_for_advance_payment,
2555 t8('Save and AP Transaction'),
2556 call => [ 'kivi.Order.save', {
2557 action => 'save_and_ap_transaction',
2558 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2560 only_if => $self->type_data->show_menu('save_and_ap_transaction'),
2561 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2564 ], # end of combobox "Workflow"
2571 t8('Save and preview PDF'),
2572 call => [ 'kivi.Order.save', {
2573 action => 'preview_pdf',
2574 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2575 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2577 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2578 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2579 : $is_final_version ? t8('This record is the final version. Please create a new sub-version') : undef,
2580 only_if => $self->type_data->show_menu('save_and_print'),
2583 t8('Save and print'),
2584 call => [ 'kivi.Order.show_print_options', { warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2585 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate },
2587 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2588 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2589 : $is_final_version ? t8('This record is the final version. Please create a new sub-version') : undef,
2590 only_if => $self->type_data->show_menu('save_and_print'),
2593 ($is_final_version ? t8('E-mail') : t8('Save and E-mail')),
2594 id => 'save_and_email_action',
2595 call => [ 'kivi.Order.save', {
2596 action => 'save_and_show_email_dialog',
2597 warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2598 warn_on_reqdate => $::instance_conf->get_order_warn_no_deliverydate,
2600 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2601 : !$self->order->id ? t8('This object has not been saved yet.')
2603 only_if => $self->type_data->show_menu('save_and_email'),
2606 t8('Download attachments of all parts'),
2607 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
2608 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2609 only_if => $::instance_conf->get_doc_storage,
2611 ], # end of combobox "Export"
2615 call => [ 'kivi.Order.delete_order' ],
2616 confirm => $::locale->text('Do you really want to delete this object?'),
2617 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2618 : !$self->order->id ? t8('This object has not been saved yet.')
2620 only_if => $self->type_data->show_menu('delete'),
2629 call => [ 'set_history_window', $self->order->id, 'id' ],
2630 disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
2634 call => [ 'kivi.Order.follow_up_window' ],
2635 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2636 only_if => $::auth->assert('productivity', 1),
2638 ], # end of combobox "more"
2644 my ($self, $doc_ref, $params) = @_;
2646 my $order = $self->order;
2649 my $print_form = Form->new('');
2650 $print_form->{type} = $order->type;
2651 $print_form->{formname} = $params->{formname} || $order->type;
2652 $print_form->{format} = $params->{format} || 'pdf';
2653 $print_form->{media} = $params->{media} || 'file';
2654 $print_form->{groupitems} = $params->{groupitems};
2655 $print_form->{printer_id} = $params->{printer_id};
2656 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
2658 $order->language($params->{language});
2659 $order->flatten_to_form($print_form, format_amounts => 1);
2663 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
2664 $template_ext = 'odt';
2665 $template_type = 'OpenDocument';
2666 } elsif ($print_form->{format} =~ m{html}i) {
2667 $template_ext = 'html';
2668 $template_type = 'HTML';
2671 # search for the template
2672 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
2673 name => $print_form->{formname},
2674 extension => $template_ext,
2675 email => $print_form->{media} eq 'email',
2676 language => $params->{language},
2677 printer_id => $print_form->{printer_id},
2680 if (!defined $template_file) {
2681 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);
2684 return @errors if scalar @errors;
2686 $print_form->throw_on_error(sub {
2688 $print_form->prepare_for_printing;
2690 $$doc_ref = SL::Helper::CreatePDF->create_pdf(
2691 format => $print_form->{format},
2692 template_type => $template_type,
2693 template => $template_file,
2694 variables => $print_form,
2695 variable_content_types => {
2696 longdescription => 'html',
2697 partnotes => 'html',
2699 $::form->get_variable_content_types_for_cvars,
2703 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
2709 sub get_files_for_email_dialog {
2712 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
2714 return %files if !$::instance_conf->get_doc_storage;
2716 if ($self->order->id) {
2717 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
2718 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
2719 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
2720 $files{project_files} = [ SL::File->get_all( object_id => $self->order->globalproject_id, object_type => 'project', file_type => 'attachment') ];
2724 uniq_by { $_->{id} }
2726 +{ id => $_->part->id,
2727 partnumber => $_->part->partnumber }
2728 } @{$self->order->items_sorted};
2730 foreach my $part (@parts) {
2731 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
2732 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
2735 foreach my $key (keys %files) {
2736 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
2742 sub make_periodic_invoices_config_from_yaml {
2743 my ($yaml_config) = @_;
2745 return if !$yaml_config;
2746 my $attr = SL::YAML::Load($yaml_config);
2747 return if 'HASH' ne ref $attr;
2748 return SL::DB::PeriodicInvoicesConfig->new(%$attr);
2752 sub get_periodic_invoices_status {
2753 my ($self, $config) = @_;
2755 return if $self->type ne SALES_ORDER_TYPE();
2756 return t8('not configured') if !$config;
2758 my $active = ('HASH' eq ref $config) ? $config->{active}
2759 : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
2760 : die "Cannot get status of periodic invoices config";
2762 return $active ? t8('active') : t8('inactive');
2765 sub get_item_cvpartnumber {
2766 my ($self, $item) = @_;
2768 return if !$self->search_cvpartnumber;
2769 return if !$self->order->customervendor;
2771 if ($self->cv eq 'vendor') {
2772 my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
2773 $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
2774 } elsif ($self->cv eq 'customer') {
2775 my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
2776 $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
2780 sub get_part_texts {
2781 my ($part_or_id, $language_or_id, %defaults) = @_;
2783 my $part = ref($part_or_id) ? $part_or_id : SL::DB::Part->load_cached($part_or_id);
2784 my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
2786 description => $defaults{description} // $part->description,
2787 longdescription => $defaults{longdescription} // $part->notes,
2790 return $texts unless $language_id;
2792 my $translation = SL::DB::Manager::Translation->get_first(
2794 parts_id => $part->id,
2795 language_id => $language_id,
2798 $texts->{description} = $translation->translation if $translation && $translation->translation;
2799 $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
2807 return $self->type_data->properties('nr_key');
2810 sub save_and_redirect_to {
2811 my ($self, %params) = @_;
2815 flash_later('info', $self->type_data->text('saved'));
2817 $self->redirect_to(%params, id => $self->order->id);
2821 my ($self, $addition) = @_;
2823 my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
2824 my $snumbers = $number_type . '_' . $self->order->$number_type;
2826 SL::DB::History->new(
2827 trans_id => $self->order->id,
2828 employee_id => SL::DB::Manager::Employee->current->id,
2829 what_done => $self->order->type,
2830 snumbers => $snumbers,
2831 addition => $addition,
2835 sub store_doc_to_webdav_and_filemanagement {
2836 my ($self, $content, $filename, $variant) = @_;
2838 my $order = $self->order;
2841 # copy file to webdav folder
2842 if ($order->number && $::instance_conf->get_webdav_documents) {
2843 my $webdav = SL::Webdav->new(
2844 type => $order->type,
2845 number => $order->number,
2847 my $webdav_file = SL::Webdav::File->new(
2849 filename => $filename,
2852 $webdav_file->store(data => \$content);
2855 push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
2859 if ($order->id && $::instance_conf->get_doc_storage) {
2861 $file_obj = SL::File->save(object_id => $order->id,
2862 object_type => $order->type,
2863 mime_type => SL::MIME->mime_type_from_ext($filename),
2864 source => 'created',
2865 file_type => 'document',
2866 file_name => $filename,
2867 file_contents => $content,
2868 print_variant => $variant);
2870 $self->{file_id} = $file_obj->id;
2873 push @errors, t8('Storing the document in the storage backend failed: #1', $@);
2880 sub init_type_data {
2882 SL::DB::Helper::TypeDataProxy->new('SL::DB::Order', $self->order->record_type);
2893 SL::Controller::Order - controller for orders
2897 This is a new form to enter orders, completely rewritten with the use
2898 of controller and java script techniques.
2900 The aim is to provide the user a better experience and a faster workflow. Also
2901 the code should be more readable, more reliable and better to maintain.
2909 One input row, so that input happens every time at the same place.
2913 Use of pickers where possible.
2917 Possibility to enter more than one item at once.
2921 Item list in a scrollable area, so that the workflow buttons stay at
2926 Reordering item rows with drag and drop is possible. Sorting item rows is
2927 possible (by partnumber, description, qty, sellprice and discount for now).
2931 No C<update> is necessary. All entries and calculations are managed
2932 with ajax-calls and the page only reloads on C<save>.
2936 User can see changes immediately, because of the use of java script
2947 =item * C<SL/Controller/Order.pm>
2951 =item * C<template/webpages/order/form.html>
2955 =item * C<template/webpages/order/tabs/basic_data.html>
2957 Main tab for basic_data.
2959 This is the only tab here for now. "linked records" and "webdav" tabs are
2960 reused from generic code.
2964 =item * C<template/webpages/order/tabs/_business_info_row.html>
2966 For displaying information on business type
2968 =item * C<template/webpages/order/tabs/_item_input.html>
2970 The input line for items
2972 =item * C<template/webpages/order/tabs/_row.html>
2974 One row for already entered items
2976 =item * C<template/webpages/order/tabs/_tax_row.html>
2978 Displaying tax information
2980 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
2982 Dialog for selecting price and discount sources
2986 =item * C<js/kivi.Order.js>
2988 java script functions
2998 =item * price sources: little symbols showing better price / better discount
3000 =item * select units in input row?
3002 =item * check for direct delivery (workflow sales order -> purchase order)
3004 =item * access rights
3006 =item * display weights
3010 =item * optional client/user behaviour
3012 (transactions has to be set - department has to be set -
3013 force project if enabled in client config)
3017 =head1 KNOWN BUGS AND CAVEATS
3023 No indication that <shift>-up/down expands/collapses second row.
3027 Table header is not sticky in the scrolling area.
3031 Sorting does not include C<position>, neither does reordering.
3033 This behavior was implemented intentionally. But we can discuss, which behavior
3034 should be implemented.
3038 =head1 To discuss / Nice to have
3044 How to expand/collapse second row. Now it can be done clicking the icon or
3049 This controller uses a (changed) copy of the template for the PriceSource
3050 dialog. Maybe there could be used one code source.
3054 Rounding-differences between this controller (PriceTaxCalculator) and the old
3055 form. This is not only a problem here, but also in all parts using the PTC.
3056 There exists a ticket and a patch. This patch should be testet.
3060 An indicator, if the actual inputs are saved (like in an
3061 editor or on text processing application).
3065 A warning when leaving the page without saving unchanged inputs.
3072 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>