1 package SL::Controller::Order;
4 use parent qw(SL::Controller::Base);
6 use SL::Helper::Flash qw(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;
15 use SL::Util qw(trim);
17 use SL::DB::AdditionalBillingAddress;
24 use SL::DB::PartClassification;
25 use SL::DB::PartsGroup;
28 use SL::DB::RecordLink;
29 use SL::DB::RequirementSpec;
31 use SL::DB::Translation;
33 use SL::Helper::CreatePDF qw(:all);
34 use SL::Helper::PrintOptions;
35 use SL::Helper::ShippedQty;
36 use SL::Helper::UserPreferences::PositionsScrollbar;
37 use SL::Helper::UserPreferences::UpdatePositions;
39 use SL::Controller::Helper::GetModels;
41 use List::Util qw(first sum0);
42 use List::UtilsBy qw(sort_by uniq_by);
43 use List::MoreUtils qw(any none pairwise first_index);
44 use English qw(-no_match_vars);
49 use Rose::Object::MakeMethods::Generic
51 scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
52 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
57 __PACKAGE__->run_before('check_auth');
59 __PACKAGE__->run_before('recalc',
60 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
63 __PACKAGE__->run_before('get_unalterable_data',
64 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
75 $self->order->transdate(DateTime->now_local());
76 my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval :
77 $self->type eq sales_order_type() ? $::instance_conf->get_delivery_date_interval : 1;
79 if ( ($self->type eq sales_order_type() && $::instance_conf->get_deliverydate_on)
80 || ($self->type eq sales_quotation_type() && $::instance_conf->get_reqdate_on)
81 && (!$self->order->reqdate)) {
82 $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
89 title => $self->get_title_for('add'),
90 %{$self->{template_args}}
94 # edit an existing order
102 # this is to edit an order from an unsaved order object
104 # set item ids to new fake id, to identify them as new items
105 foreach my $item (@{$self->order->items_sorted}) {
106 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
108 # trigger rendering values for second row as hidden, because they
109 # are loaded only on demand. So we need to keep the values from
111 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
118 title => $self->get_title_for('edit'),
119 %{$self->{template_args}}
123 # edit a collective order (consisting of one or more existing orders)
124 sub action_edit_collective {
128 my @multi_ids = map {
129 $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
130 } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
132 # fall back to add if no ids are given
133 if (scalar @multi_ids == 0) {
138 # fall back to save as new if only one id is given
139 if (scalar @multi_ids == 1) {
140 $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
141 $self->action_save_as_new();
145 # make new order from given orders
146 my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
147 $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
148 $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
150 $self->action_edit();
157 my $errors = $self->delete();
159 if (scalar @{ $errors }) {
160 $self->js->flash('error', $_) foreach @{ $errors };
161 return $self->js->render();
164 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been deleted')
165 : $self->type eq purchase_order_type() ? $::locale->text('The order has been deleted')
166 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been deleted')
167 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
169 flash_later('info', $text);
171 my @redirect_params = (
176 $self->redirect_to(@redirect_params);
183 my $errors = $self->save();
185 if (scalar @{ $errors }) {
186 $self->js->flash('error', $_) foreach @{ $errors };
187 return $self->js->render();
190 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
191 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
192 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
193 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
195 flash_later('info', $text);
197 my @redirect_params = (
200 id => $self->order->id,
203 $self->redirect_to(@redirect_params);
206 # save the order as new document an open it for edit
207 sub action_save_as_new {
210 my $order = $self->order;
213 $self->js->flash('error', t8('This object has not been saved yet.'));
214 return $self->js->render();
217 # load order from db to check if values changed
218 my $saved_order = SL::DB::Order->new(id => $order->id)->load;
221 # Lets assign a new number if the user hasn't changed the previous one.
222 # If it has been changed manually then use it as-is.
223 $new_attrs{number} = (trim($order->number) eq $saved_order->number)
225 : trim($order->number);
227 # Clear transdate unless changed
228 $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
229 ? DateTime->today_local
232 # Set new reqdate unless changed if it is enabled in client config
233 if ($order->reqdate == $saved_order->reqdate) {
234 my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval :
235 $self->type eq sales_order_type() ? $::instance_conf->get_delivery_date_interval : 1;
237 if ( ($self->type eq sales_order_type() && !$::instance_conf->get_deliverydate_on)
238 || ($self->type eq sales_quotation_type() && !$::instance_conf->get_reqdate_on)) {
239 $new_attrs{reqdate} = '';
241 $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
244 $new_attrs{reqdate} = $order->reqdate;
248 $new_attrs{employee} = SL::DB::Manager::Employee->current;
250 # Create new record from current one
251 $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
253 # no linked records on save as new
254 delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
257 $self->action_save();
262 # This is called if "print" is pressed in the print dialog.
263 # If PDF creation was requested and succeeded, the pdf is offered for download
264 # via send_file (which uses ajax in this case).
268 my $errors = $self->save();
270 if (scalar @{ $errors }) {
271 $self->js->flash('error', $_) foreach @{ $errors };
272 return $self->js->render();
275 $self->js_reset_order_and_item_ids_after_save;
277 my $format = $::form->{print_options}->{format};
278 my $media = $::form->{print_options}->{media};
279 my $formname = $::form->{print_options}->{formname};
280 my $copies = $::form->{print_options}->{copies};
281 my $groupitems = $::form->{print_options}->{groupitems};
282 my $printer_id = $::form->{print_options}->{printer_id};
284 # only PDF, OpenDocument & HTML for now
285 if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
286 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
289 # only screen or printer by now
290 if (none { $media eq $_ } qw(screen printer)) {
291 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
294 # create a form for generate_attachment_filename
295 my $form = Form->new;
296 $form->{$self->nr_key()} = $self->order->number;
297 $form->{type} = $self->type;
298 $form->{format} = $format;
299 $form->{formname} = $formname;
300 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
301 my $doc_filename = $form->generate_attachment_filename();
304 my @errors = $self->generate_doc(\$doc, { media => $media,
306 formname => $formname,
307 language => $self->order->language,
308 printer_id => $printer_id,
309 groupitems => $groupitems });
310 if (scalar @errors) {
311 return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
314 if ($media eq 'screen') {
316 $self->js->flash('info', t8('The document has been created.'));
319 type => SL::MIME->mime_type_from_ext($doc_filename),
320 name => $doc_filename,
324 } elsif ($media eq 'printer') {
326 my $printer_id = $::form->{print_options}->{printer_id};
327 SL::DB::Printer->new(id => $printer_id)->load->print_document(
332 $self->js->flash('info', t8('The document has been printed.'));
335 my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
336 if (scalar @warnings) {
337 $self->js->flash('warning', $_) for @warnings;
340 $self->save_history('PRINTED');
343 ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
346 sub action_preview_pdf {
349 my $errors = $self->save();
350 if (scalar @{ $errors }) {
351 $self->js->flash('error', $_) foreach @{ $errors };
352 return $self->js->render();
355 $self->js_reset_order_and_item_ids_after_save;
358 my $media = 'screen';
359 my $formname = $self->type;
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 $pdf_filename = $form->generate_attachment_filename();
372 my @errors = $self->generate_doc(\$pdf, { media => $media,
374 formname => $formname,
375 language => $self->order->language,
377 if (scalar @errors) {
378 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
380 $self->save_history('PREVIEWED');
381 $self->js->flash('info', t8('The PDF has been previewed'));
385 type => SL::MIME->mime_type_from_ext($pdf_filename),
386 name => $pdf_filename,
391 # open the email dialog
392 sub action_save_and_show_email_dialog {
395 my $errors = $self->save();
397 if (scalar @{ $errors }) {
398 $self->js->flash('error', $_) foreach @{ $errors };
399 return $self->js->render();
402 my $cv_method = $self->cv;
404 if (!$self->order->$cv_method) {
405 return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
410 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
411 $email_form->{to} ||= $self->order->$cv_method->email;
412 $email_form->{cc} = $self->order->$cv_method->cc;
413 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
414 # Todo: get addresses from shipto, if any
416 my $form = Form->new;
417 $form->{$self->nr_key()} = $self->order->number;
418 $form->{cusordnumber} = $self->order->cusordnumber;
419 $form->{formname} = $self->type;
420 $form->{type} = $self->type;
421 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
422 $form->{language_id} = $self->order->language->id if $self->order->language;
423 $form->{format} = 'pdf';
424 $form->{cp_id} = $self->order->contact->cp_id if $self->order->contact;
426 $email_form->{subject} = $form->generate_email_subject();
427 $email_form->{attachment_filename} = $form->generate_attachment_filename();
428 $email_form->{message} = $form->generate_email_body();
429 $email_form->{js_send_function} = 'kivi.Order.send_email()';
431 my %files = $self->get_files_for_email_dialog();
433 my @employees_with_email = grep {
434 my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
435 $user && !!trim($user->get_config_value('email'));
436 } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
438 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
439 email_form => $email_form,
440 show_bcc => $::auth->assert('email_bcc', 'may fail'),
442 is_customer => $self->cv eq 'customer',
443 ALL_EMPLOYEES => \@employees_with_email,
447 ->run('kivi.Order.show_email_dialog', $dialog_html)
454 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
455 sub action_send_email {
458 my $errors = $self->save();
460 if (scalar @{ $errors }) {
461 $self->js->run('kivi.Order.close_email_dialog');
462 $self->js->flash('error', $_) foreach @{ $errors };
463 return $self->js->render();
466 $self->js_reset_order_and_item_ids_after_save;
468 my $email_form = delete $::form->{email_form};
469 my %field_names = (to => 'email');
471 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
473 # for Form::cleanup which may be called in Form::send_email
474 $::form->{cwd} = getcwd();
475 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
477 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
478 $::form->{media} = 'email';
480 $::form->{attachment_policy} //= '';
482 # Is an old file version available?
484 if ($::form->{attachment_policy} eq 'old_file') {
485 $attfile = SL::File->get_all(object_id => $self->order->id,
486 object_type => $self->type,
487 file_type => 'document',
488 print_variant => $::form->{formname});
491 if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
493 my @errors = $self->generate_doc(\$doc, {media => $::form->{media},
494 format => $::form->{print_options}->{format},
495 formname => $::form->{print_options}->{formname},
496 language => $self->order->language,
497 printer_id => $::form->{print_options}->{printer_id},
498 groupitems => $::form->{print_options}->{groupitems}});
499 if (scalar @errors) {
500 return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
503 my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename}, $::form->{formname});
504 if (scalar @warnings) {
505 flash_later('warning', $_) for @warnings;
508 my $sfile = SL::SessionFile::Random->new(mode => "w");
509 $sfile->fh->print($doc);
512 $::form->{tmpfile} = $sfile->file_name;
513 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
516 $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
517 $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
519 # internal notes unless no email journal
520 unless ($::instance_conf->get_email_journal) {
521 my $intnotes = $self->order->intnotes;
522 $intnotes .= "\n\n" if $self->order->intnotes;
523 $intnotes .= t8('[email]') . "\n";
524 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
525 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
526 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
527 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
528 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
529 $intnotes .= t8('Message') . ": " . SL::HTML::Util->strip($::form->{message});
531 $self->order->update_attributes(intnotes => $intnotes);
534 $self->save_history('MAILED');
536 flash_later('info', t8('The email has been sent.'));
538 my @redirect_params = (
541 id => $self->order->id,
544 $self->redirect_to(@redirect_params);
547 # open the periodic invoices config dialog
549 # If there are values in the form (i.e. dialog was opened before),
550 # then use this values. Create new ones, else.
551 sub action_show_periodic_invoices_config_dialog {
554 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
555 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
556 $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm',
557 order_value_periodicity => 'p', # = same as periodicity
558 start_date_as_date => $::form->{transdate_as_date} || $::form->current_date,
559 extend_automatically_by => 12,
561 email_subject => GenericTranslations->get(
562 language_id => $::form->{language_id},
563 translation_type =>"preset_text_periodic_invoices_email_subject"),
564 email_body => GenericTranslations->get(
565 language_id => $::form->{language_id},
566 translation_type => "salutation_general")
567 . GenericTranslations->get(
568 language_id => $::form->{language_id},
569 translation_type => "salutation_punctuation_mark") . "\n\n"
570 . GenericTranslations->get(
571 language_id => $::form->{language_id},
572 translation_type =>"preset_text_periodic_invoices_email_body"),
574 # for older configs, replace email preset text if not yet set.
575 $config->email_subject(GenericTranslations->get(
576 language_id => $::form->{language_id},
577 translation_type =>"preset_text_periodic_invoices_email_subject")
578 ) unless $config->email_subject;
580 $config->email_body(GenericTranslations->get(
581 language_id => $::form->{language_id},
582 translation_type => "salutation_general")
583 . GenericTranslations->get(
584 language_id => $::form->{language_id},
585 translation_type => "salutation_punctuation_mark") . "\n\n"
586 . GenericTranslations->get(
587 language_id => $::form->{language_id},
588 translation_type =>"preset_text_periodic_invoices_email_body")
589 ) unless $config->email_body;
591 $config->periodicity('m') if none { $_ eq $config->periodicity } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
592 $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
594 $::form->get_lists(printers => "ALL_PRINTERS",
595 charts => { key => 'ALL_CHARTS',
596 transdate => 'current_date' });
598 $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
600 if ($::form->{customer_id}) {
601 $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
602 my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
603 $::form->{postal_invoice} = $customer_object->postal_invoice;
604 $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
605 $config->send_email(0) if $::form->{postal_invoice};
608 $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
610 popup_js_close_function => 'kivi.Order.close_periodic_invoices_config_dialog()',
611 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
616 # assign the values of the periodic invoices config dialog
617 # as yaml in the hidden tag and set the status.
618 sub action_assign_periodic_invoices_config {
621 $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
623 my $config = { active => $::form->{active} ? 1 : 0,
624 terminated => $::form->{terminated} ? 1 : 0,
625 direct_debit => $::form->{direct_debit} ? 1 : 0,
626 periodicity => (any { $_ eq $::form->{periodicity} } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES) ? $::form->{periodicity} : 'm',
627 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
628 start_date_as_date => $::form->{start_date_as_date},
629 end_date_as_date => $::form->{end_date_as_date},
630 first_billing_date_as_date => $::form->{first_billing_date_as_date},
631 print => $::form->{print} ? 1 : 0,
632 printer_id => $::form->{print} ? $::form->{printer_id} * 1 : undef,
633 copies => $::form->{copies} * 1 ? $::form->{copies} : 1,
634 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
635 ar_chart_id => $::form->{ar_chart_id} * 1,
636 send_email => $::form->{send_email} ? 1 : 0,
637 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
638 email_recipient_address => $::form->{email_recipient_address},
639 email_sender => $::form->{email_sender},
640 email_subject => $::form->{email_subject},
641 email_body => $::form->{email_body},
644 my $periodic_invoices_config = SL::YAML::Dump($config);
646 my $status = $self->get_periodic_invoices_status($config);
649 ->remove('#order_periodic_invoices_config')
650 ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
651 ->run('kivi.Order.close_periodic_invoices_config_dialog')
652 ->html('#periodic_invoices_status', $status)
653 ->flash('info', t8('The periodic invoices config has been assigned.'))
657 sub action_get_has_active_periodic_invoices {
660 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
661 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
663 my $has_active_periodic_invoices =
664 $self->type eq sales_order_type()
667 && (!$config->end_date || ($config->end_date > DateTime->today_local))
668 && $config->get_previous_billed_period_start_date;
670 $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
673 # save the order and redirect to the frontend subroutine for a new
675 sub action_save_and_delivery_order {
678 $self->save_and_redirect_to(
679 controller => 'oe.pl',
680 action => 'oe_delivery_order_from_order',
684 sub action_save_and_supplier_delivery_order {
687 $self->save_and_redirect_to(
688 controller => 'controller.pl',
689 action => 'DeliveryOrder/add_from_order',
690 type => 'supplier_delivery_order',
694 # save the order and redirect to the frontend subroutine for a new
696 sub action_save_and_invoice {
699 $self->save_and_redirect_to(
700 controller => 'oe.pl',
701 action => 'oe_invoice_from_order',
705 sub action_save_and_invoice_for_advance_payment {
708 $self->save_and_redirect_to(
709 controller => 'oe.pl',
710 action => 'oe_invoice_from_order',
711 new_invoice_type => 'invoice_for_advance_payment',
715 sub action_save_and_final_invoice {
718 $self->save_and_redirect_to(
719 controller => 'oe.pl',
720 action => 'oe_invoice_from_order',
721 new_invoice_type => 'final_invoice',
725 # workflow from sales order to sales quotation
726 sub action_sales_quotation {
727 $_[0]->workflow_sales_or_request_for_quotation();
730 # workflow from sales order to sales quotation
731 sub action_request_for_quotation {
732 $_[0]->workflow_sales_or_request_for_quotation();
735 # workflow from sales quotation to sales order
736 sub action_sales_order {
737 $_[0]->workflow_sales_or_purchase_order();
740 # workflow from rfq to purchase order
741 sub action_purchase_order {
742 $_[0]->workflow_sales_or_purchase_order();
745 # workflow from purchase order to ap transaction
746 sub action_save_and_ap_transaction {
749 $self->save_and_redirect_to(
750 controller => 'ap.pl',
751 action => 'add_from_purchase_order',
755 # set form elements in respect to a changed customer or vendor
757 # This action is called on an change of the customer/vendor picker.
758 sub action_customer_vendor_changed {
761 setup_order_from_cv($self->order);
764 my $cv_method = $self->cv;
766 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
767 $self->js->show('#cp_row');
769 $self->js->hide('#cp_row');
772 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
773 $self->js->show('#shipto_selection');
775 $self->js->hide('#shipto_selection');
778 if ($cv_method eq 'customer') {
779 my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
780 $self->js->$show_hide('#billing_address_row');
783 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
786 ->replaceWith('#order_cp_id', $self->build_contact_select)
787 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
788 ->replaceWith('#shipto_inputs ', $self->build_shipto_inputs)
789 ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
790 ->replaceWith('#business_info_row', $self->build_business_info_row)
791 ->val( '#order_taxzone_id', $self->order->taxzone_id)
792 ->val( '#order_taxincluded', $self->order->taxincluded)
793 ->val( '#order_currency_id', $self->order->currency_id)
794 ->val( '#order_payment_id', $self->order->payment_id)
795 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
796 ->val( '#order_intnotes', $self->order->intnotes)
797 ->val( '#order_language_id', $self->order->$cv_method->language_id)
798 ->focus( '#order_' . $self->cv . '_id')
799 ->run('kivi.Order.update_exchangerate');
801 $self->js_redisplay_amounts_and_taxes;
802 $self->js_redisplay_cvpartnumbers;
806 # open the dialog for customer/vendor details
807 sub action_show_customer_vendor_details_dialog {
810 my $is_customer = 'customer' eq $::form->{vc};
813 $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
815 $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
818 my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
819 $details{discount_as_percent} = $cv->discount_as_percent;
820 $details{creditlimt} = $cv->creditlimit_as_number;
821 $details{business} = $cv->business->description if $cv->business;
822 $details{language} = $cv->language_obj->description if $cv->language_obj;
823 $details{delivery_terms} = $cv->delivery_term->description if $cv->delivery_term;
824 $details{payment_terms} = $cv->payment->description if $cv->payment;
825 $details{pricegroup} = $cv->pricegroup->pricegroup if $is_customer && $cv->pricegroup;
828 foreach my $entry (@{ $cv->additional_billing_addresses }) {
829 push @{ $details{ADDITIONAL_BILLING_ADDRESSES} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
832 foreach my $entry (@{ $cv->shipto }) {
833 push @{ $details{SHIPTO} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
835 foreach my $entry (@{ $cv->contacts }) {
836 push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
839 $_[0]->render('common/show_vc_details', { layout => 0 },
840 is_customer => $is_customer,
845 # called if a unit in an existing item row is changed
846 sub action_unit_changed {
849 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
850 my $item = $self->order->items_sorted->[$idx];
852 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
853 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
858 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
859 $self->js_redisplay_line_values;
860 $self->js_redisplay_amounts_and_taxes;
864 # add an item row for a new item entered in the input row
865 sub action_add_item {
868 delete $::form->{add_item}->{create_part_type};
870 my $form_attr = $::form->{add_item};
872 return unless $form_attr->{parts_id};
874 my $item = new_item($self->order, $form_attr);
876 $self->order->add_items($item);
880 $self->get_item_cvpartnumber($item);
882 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
883 my $row_as_html = $self->p->render('order/tabs/_row',
889 if ($::form->{insert_before_item_id}) {
891 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
894 ->append('#row_table_id', $row_as_html);
897 if ( $item->part->is_assortment ) {
898 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
899 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
900 my $attr = { parts_id => $assortment_item->parts_id,
901 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
902 unit => $assortment_item->unit,
903 description => $assortment_item->part->description,
905 my $item = new_item($self->order, $attr);
907 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
908 $item->discount(1) unless $assortment_item->charge;
910 $self->order->add_items( $item );
912 $self->get_item_cvpartnumber($item);
913 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
914 my $row_as_html = $self->p->render('order/tabs/_row',
919 if ($::form->{insert_before_item_id}) {
921 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
924 ->append('#row_table_id', $row_as_html);
930 ->val('.add_item_input', '')
931 ->run('kivi.Order.init_row_handlers')
932 ->run('kivi.Order.renumber_positions')
933 ->focus('#add_item_parts_id_name');
935 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
937 $self->js_redisplay_amounts_and_taxes;
941 # add item rows for multiple items at once
942 sub action_add_multi_items {
945 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
946 return $self->js->render() unless scalar @form_attr;
949 foreach my $attr (@form_attr) {
950 my $item = new_item($self->order, $attr);
952 if ( $item->part->is_assortment ) {
953 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
954 my $attr = { parts_id => $assortment_item->parts_id,
955 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
956 unit => $assortment_item->unit,
957 description => $assortment_item->part->description,
959 my $item = new_item($self->order, $attr);
961 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
962 $item->discount(1) unless $assortment_item->charge;
967 $self->order->add_items(@items);
971 foreach my $item (@items) {
972 $self->get_item_cvpartnumber($item);
973 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
974 my $row_as_html = $self->p->render('order/tabs/_row',
980 if ($::form->{insert_before_item_id}) {
982 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
985 ->append('#row_table_id', $row_as_html);
990 ->run('kivi.Part.close_picker_dialogs')
991 ->run('kivi.Order.init_row_handlers')
992 ->run('kivi.Order.renumber_positions')
993 ->focus('#add_item_parts_id_name');
995 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
997 $self->js_redisplay_amounts_and_taxes;
1001 # recalculate all linetotals, amounts and taxes and redisplay them
1002 sub action_recalc_amounts_and_taxes {
1007 $self->js_redisplay_line_values;
1008 $self->js_redisplay_amounts_and_taxes;
1009 $self->js->render();
1012 sub action_update_exchangerate {
1016 is_standard => $self->order->currency_id == $::instance_conf->get_currency_id,
1017 currency_name => $self->order->currency->name,
1018 exchangerate => $self->order->daily_exchangerate_as_null_number,
1021 $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
1024 # redisplay item rows if they are sorted by an attribute
1025 sub action_reorder_items {
1029 partnumber => sub { $_[0]->part->partnumber },
1030 description => sub { $_[0]->description },
1031 qty => sub { $_[0]->qty },
1032 sellprice => sub { $_[0]->sellprice },
1033 discount => sub { $_[0]->discount },
1034 cvpartnumber => sub { $_[0]->{cvpartnumber} },
1037 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1039 my $method = $sort_keys{$::form->{order_by}};
1040 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
1041 if ($::form->{sort_dir}) {
1042 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
1043 @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
1045 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
1048 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
1049 @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
1051 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
1055 ->run('kivi.Order.redisplay_items', \@to_sort)
1059 # show the popup to choose a price/discount source
1060 sub action_price_popup {
1063 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
1064 my $item = $self->order->items_sorted->[$idx];
1066 $self->render_price_dialog($item);
1069 # save the order in a session variable and redirect to the part controller
1070 sub action_create_part {
1073 my $previousform = $::auth->save_form_in_session(non_scalars => 1);
1075 my $callback = $self->url_for(
1076 action => 'return_from_create_part',
1077 type => $self->type, # type is needed for check_auth on return
1078 previousform => $previousform,
1081 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.'));
1083 my @redirect_params = (
1084 controller => 'Part',
1086 part_type => $::form->{add_item}->{create_part_type},
1087 callback => $callback,
1091 $self->redirect_to(@redirect_params);
1094 sub action_return_from_create_part {
1097 $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
1099 $::auth->restore_form_from_session(delete $::form->{previousform});
1101 # set item ids to new fake id, to identify them as new items
1102 foreach my $item (@{$self->order->items_sorted}) {
1103 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1107 $self->get_unalterable_data();
1108 $self->pre_render();
1110 # trigger rendering values for second row/longdescription as hidden,
1111 # because they are loaded only on demand. So we need to keep the values
1113 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1114 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
1118 title => $self->get_title_for('edit'),
1119 %{$self->{template_args}}
1124 # load the second row for one or more items
1126 # This action gets the html code for all items second rows by rendering a template for
1127 # the second row and sets the html code via client js.
1128 sub action_load_second_rows {
1131 $self->recalc() if $self->order->is_sales; # for margin calculation
1133 foreach my $item_id (@{ $::form->{item_ids} }) {
1134 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1135 my $item = $self->order->items_sorted->[$idx];
1137 $self->js_load_second_row($item, $item_id, 0);
1140 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
1142 $self->js->render();
1145 # update description, notes and sellprice from master data
1146 sub action_update_row_from_master_data {
1149 foreach my $item_id (@{ $::form->{item_ids} }) {
1150 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1151 my $item = $self->order->items_sorted->[$idx];
1152 my $texts = get_part_texts($item->part, $self->order->language_id);
1154 $item->description($texts->{description});
1155 $item->longdescription($texts->{longdescription});
1157 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1160 if ($item->part->is_assortment) {
1161 # add assortment items with price 0, as the components carry the price
1162 $price_src = $price_source->price_from_source("");
1163 $price_src->price(0);
1165 $price_src = $price_source->best_price
1166 ? $price_source->best_price
1167 : $price_source->price_from_source("");
1168 $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
1169 $price_src->price(0) if !$price_source->best_price;
1173 $item->sellprice($price_src->price);
1174 $item->active_price_source($price_src);
1177 ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
1178 ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
1179 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
1180 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
1182 if ($self->search_cvpartnumber) {
1183 $self->get_item_cvpartnumber($item);
1184 $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
1189 $self->js_redisplay_line_values;
1190 $self->js_redisplay_amounts_and_taxes;
1192 $self->js->render();
1195 sub js_load_second_row {
1196 my ($self, $item, $item_id, $do_parse) = @_;
1199 # Parse values from form (they are formated while rendering (template)).
1200 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1201 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
1202 foreach my $var (@{ $item->cvars_by_config }) {
1203 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1205 $item->parse_custom_variable_values;
1208 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
1211 ->html('#second_row_' . $item_id, $row_as_html)
1212 ->data('#second_row_' . $item_id, 'loaded', 1);
1215 sub js_redisplay_line_values {
1218 my $is_sales = $self->order->is_sales;
1220 # sales orders with margins
1225 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1226 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
1227 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
1228 ]} @{ $self->order->items_sorted };
1232 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1233 ]} @{ $self->order->items_sorted };
1237 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
1240 sub js_redisplay_amounts_and_taxes {
1243 if (scalar @{ $self->{taxes} }) {
1244 $self->js->show('#taxincluded_row_id');
1246 $self->js->hide('#taxincluded_row_id');
1249 if ($self->order->taxincluded) {
1250 $self->js->hide('#subtotal_row_id');
1252 $self->js->show('#subtotal_row_id');
1255 if ($self->order->is_sales) {
1256 my $is_neg = $self->order->marge_total < 0;
1258 ->html('#marge_total_id', $::form->format_amount(\%::myconfig, $self->order->marge_total, 2))
1259 ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
1260 ->action_if( $is_neg, 'addClass', '#marge_total_id', 'plus0')
1261 ->action_if( $is_neg, 'addClass', '#marge_percent_id', 'plus0')
1262 ->action_if( $is_neg, 'addClass', '#marge_percent_sign_id', 'plus0')
1263 ->action_if(!$is_neg, 'removeClass', '#marge_total_id', 'plus0')
1264 ->action_if(!$is_neg, 'removeClass', '#marge_percent_id', 'plus0')
1265 ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
1269 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
1270 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
1271 ->remove('.tax_row')
1272 ->insertBefore($self->build_tax_rows, '#amount_row_id');
1275 sub js_redisplay_cvpartnumbers {
1278 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1280 my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
1283 ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
1286 sub js_reset_order_and_item_ids_after_save {
1290 ->val('#id', $self->order->id)
1291 ->val('#converted_from_oe_id', '')
1292 ->val('#order_' . $self->nr_key(), $self->order->number);
1295 foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
1296 next if !$self->order->items_sorted->[$idx]->id;
1297 next if $form_item_id !~ m{^new};
1299 ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
1300 ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
1301 ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
1305 $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
1312 sub init_valid_types {
1313 [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
1319 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
1320 die "Not a valid type for order";
1323 $self->type($::form->{type});
1329 my $cv = (any { $self->type eq $_ } (sales_order_type(), sales_quotation_type())) ? 'customer'
1330 : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
1331 : die "Not a valid type for order";
1336 sub init_search_cvpartnumber {
1339 my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1340 my $search_cvpartnumber;
1341 $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
1342 $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel() if $self->cv eq 'vendor';
1344 return $search_cvpartnumber;
1347 sub init_show_update_button {
1350 !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1361 sub init_all_price_factors {
1362 SL::DB::Manager::PriceFactor->get_all;
1365 sub init_part_picker_classification_ids {
1367 my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
1369 return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
1375 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
1377 my $right = $right_for->{ $self->type };
1378 $right ||= 'DOES_NOT_EXIST';
1380 $::auth->assert($right);
1383 # build the selection box for contacts
1385 # Needed, if customer/vendor changed.
1386 sub build_contact_select {
1389 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1390 value_key => 'cp_id',
1391 title_key => 'full_name_dep',
1392 default => $self->order->cp_id,
1394 style => 'width: 300px',
1398 # build the selection box for the additional billing address
1400 # Needed, if customer/vendor changed.
1401 sub build_billing_address_select {
1404 return '' if $self->cv ne 'customer';
1406 select_tag('order.billing_address_id',
1407 [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
1409 title_key => 'displayable_id',
1410 default => $self->order->billing_address_id,
1412 style => 'width: 300px',
1416 # build the selection box for shiptos
1418 # Needed, if customer/vendor changed.
1419 sub build_shipto_select {
1422 select_tag('order.shipto_id',
1423 [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
1424 value_key => 'shipto_id',
1425 title_key => 'displayable_id',
1426 default => $self->order->shipto_id,
1428 style => 'width: 300px',
1432 # build the inputs for the cusom shipto dialog
1434 # Needed, if customer/vendor changed.
1435 sub build_shipto_inputs {
1438 my $content = $self->p->render('common/_ship_to_dialog',
1439 vc_obj => $self->order->customervendor,
1440 cs_obj => $self->order->custom_shipto,
1441 cvars => $self->order->custom_shipto->cvars_by_config,
1442 id_selector => '#order_shipto_id');
1444 div_tag($content, id => 'shipto_inputs');
1447 # render the info line for business
1449 # Needed, if customer/vendor changed.
1450 sub build_business_info_row
1452 $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
1455 # build the rows for displaying taxes
1457 # Called if amounts where recalculated and redisplayed.
1458 sub build_tax_rows {
1462 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
1463 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
1465 return $rows_as_html;
1469 sub render_price_dialog {
1470 my ($self, $record_item) = @_;
1472 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1476 'kivi.io.price_chooser_dialog',
1477 t8('Available Prices'),
1478 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1483 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1484 # $self->js->show('#dialog_flash_error');
1493 return if !$::form->{id};
1495 $self->order(SL::DB::Order->new(id => $::form->{id})->load);
1497 # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
1498 # You need a custom shipto object to call cvars_by_config to get the cvars.
1499 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
1501 return $self->order;
1504 # load or create a new order object
1506 # And assign changes from the form to this object.
1507 # If the order is loaded from db, check if items are deleted in the form,
1508 # remove them form the object and collect them for removing from db on saving.
1509 # Then create/update items from form (via make_item) and add them.
1513 # add_items adds items to an order with no items for saving, but they cannot
1514 # be retrieved via items until the order is saved. Adding empty items to new
1515 # order here solves this problem.
1517 $order = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
1518 $order ||= SL::DB::Order->new(orderitems => [],
1519 quotation => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
1520 currency_id => $::instance_conf->get_currency_id(),);
1522 my $cv_id_method = $self->cv . '_id';
1523 if (!$::form->{id} && $::form->{$cv_id_method}) {
1524 $order->$cv_id_method($::form->{$cv_id_method});
1525 setup_order_from_cv($order);
1528 my $form_orderitems = delete $::form->{order}->{orderitems};
1529 my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1531 $order->assign_attributes(%{$::form->{order}});
1533 $self->setup_custom_shipto_from_form($order, $::form);
1535 if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
1536 my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
1537 $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
1540 # remove deleted items
1541 $self->item_ids_to_delete([]);
1542 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1543 my $item = $order->orderitems->[$idx];
1544 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1545 splice @{$order->orderitems}, $idx, 1;
1546 push @{$self->item_ids_to_delete}, $item->id;
1552 foreach my $form_attr (@{$form_orderitems}) {
1553 my $item = make_item($order, $form_attr);
1554 $item->position($pos);
1558 $order->add_items(grep {!$_->id} @items);
1563 # create or update items from form
1565 # Make item objects from form values. For items already existing read from db.
1566 # Create a new item else. And assign attributes.
1568 my ($record, $attr) = @_;
1571 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1573 my $is_new = !$item;
1575 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1576 # they cannot be retrieved via custom_variables until the order/orderitem is
1577 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1578 $item ||= SL::DB::OrderItem->new(custom_variables => []);
1580 $item->assign_attributes(%$attr);
1583 my $texts = get_part_texts($item->part, $record->language_id);
1584 $item->longdescription($texts->{longdescription}) if !defined $attr->{longdescription};
1585 $item->project_id($record->globalproject_id) if !defined $attr->{project_id};
1586 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1594 # This is used to add one item
1596 my ($record, $attr) = @_;
1598 my $item = SL::DB::OrderItem->new;
1600 # Remove attributes where the user left or set the inputs empty.
1601 # So these attributes will be undefined and we can distinguish them
1602 # from zero later on.
1603 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1604 delete $attr->{$_} if $attr->{$_} eq '';
1607 $item->assign_attributes(%$attr);
1609 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1610 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1612 $item->unit($part->unit) if !$item->unit;
1615 if ( $part->is_assortment ) {
1616 # add assortment items with price 0, as the components carry the price
1617 $price_src = $price_source->price_from_source("");
1618 $price_src->price(0);
1619 } elsif (defined $item->sellprice) {
1620 $price_src = $price_source->price_from_source("");
1621 $price_src->price($item->sellprice);
1623 $price_src = $price_source->best_price
1624 ? $price_source->best_price
1625 : $price_source->price_from_source("");
1626 $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
1627 $price_src->price(0) if !$price_source->best_price;
1631 if (defined $item->discount) {
1632 $discount_src = $price_source->discount_from_source("");
1633 $discount_src->discount($item->discount);
1635 $discount_src = $price_source->best_discount
1636 ? $price_source->best_discount
1637 : $price_source->discount_from_source("");
1638 $discount_src->discount(0) if !$price_source->best_discount;
1642 $new_attr{part} = $part;
1643 $new_attr{description} = $part->description if ! $item->description;
1644 $new_attr{qty} = 1.0 if ! $item->qty;
1645 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1646 $new_attr{sellprice} = $price_src->price;
1647 $new_attr{discount} = $discount_src->discount;
1648 $new_attr{active_price_source} = $price_src;
1649 $new_attr{active_discount_source} = $discount_src;
1650 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1651 $new_attr{project_id} = $record->globalproject_id;
1652 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1654 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1655 # they cannot be retrieved via custom_variables until the order/orderitem is
1656 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1657 $new_attr{custom_variables} = [];
1659 my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
1661 $item->assign_attributes(%new_attr, %{ $texts });
1666 sub setup_order_from_cv {
1669 $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
1671 $order->intnotes($order->customervendor->notes);
1673 return if !$order->is_sales;
1675 $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
1676 $order->taxincluded(defined($order->customer->taxincluded_checked)
1677 ? $order->customer->taxincluded_checked
1678 : $::myconfig{taxincluded_checked});
1680 my $address = $order->customer->default_billing_address;;
1681 $order->billing_address_id($address ? $address->id : undef);
1684 # setup custom shipto from form
1686 # The dialog returns form variables starting with 'shipto' and cvars starting
1687 # with 'shiptocvar_'.
1688 # Mark it to be deleted if a shipto from master data is selected
1689 # (i.e. order has a shipto).
1690 # Else, update or create a new custom shipto. If the fields are empty, it
1691 # will not be saved on save.
1692 sub setup_custom_shipto_from_form {
1693 my ($self, $order, $form) = @_;
1695 if ($order->shipto) {
1696 $self->is_custom_shipto_to_delete(1);
1698 my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1700 my $shipto_cvars = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
1701 my $shipto_attrs = {map { $_ => delete $form->{$_}} grep { m{^shipto} } keys %$form};
1703 $custom_shipto->assign_attributes(%$shipto_attrs);
1704 $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
1708 # recalculate prices and taxes
1710 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1714 my %pat = $self->order->calculate_prices_and_taxes();
1716 $self->{taxes} = [];
1717 foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
1718 my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
1720 push(@{ $self->{taxes} }, { amount => $pat{taxes_by_tax_id}->{$tax_id},
1721 netamount => $netamount,
1722 tax => SL::DB::Tax->new(id => $tax_id)->load });
1724 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
1727 # get data for saving, printing, ..., that is not changed in the form
1729 # Only cvars for now.
1730 sub get_unalterable_data {
1733 foreach my $item (@{ $self->order->items }) {
1734 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1735 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1736 foreach my $var (@{ $item->cvars_by_config }) {
1737 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1739 $item->parse_custom_variable_values;
1745 # And remove related files in the spool directory
1750 my $db = $self->order->db;
1752 $db->with_transaction(
1754 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1755 $self->order->delete;
1756 my $spool = $::lx_office_conf{paths}->{spool};
1757 unlink map { "$spool/$_" } @spoolfiles if $spool;
1759 $self->save_history('DELETED');
1762 }) || push(@{$errors}, $db->error);
1769 # And delete items that are deleted in the form.
1774 my $db = $self->order->db;
1776 $db->with_transaction(sub {
1777 # delete custom shipto if it is to be deleted or if it is empty
1778 if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
1779 $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
1780 $self->order->custom_shipto(undef);
1783 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
1784 $self->order->save(cascade => 1);
1787 if ($::form->{converted_from_oe_id}) {
1788 my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
1790 foreach my $converted_from_oe_id (@converted_from_oe_ids) {
1791 my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
1792 $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
1793 $src->link_to_record($self->order);
1795 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1797 foreach (@{ $self->order->items_sorted }) {
1798 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1800 SL::DB::RecordLink->new(from_table => 'orderitems',
1801 from_id => $from_id,
1802 to_table => 'orderitems',
1809 $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
1812 $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
1814 $self->save_history('SAVED');
1817 }) || push(@{$errors}, $db->error);
1822 sub workflow_sales_or_request_for_quotation {
1826 my $errors = $self->save();
1828 if (scalar @{ $errors }) {
1829 $self->js->flash('error', $_) for @{ $errors };
1830 return $self->js->render();
1833 my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
1835 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1836 $self->{converted_from_oe_id} = delete $::form->{id};
1838 # set item ids to new fake id, to identify them as new items
1839 foreach my $item (@{$self->order->items_sorted}) {
1840 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1844 $::form->{type} = $destination_type;
1845 $self->type($self->init_type);
1846 $self->cv ($self->init_cv);
1850 $self->get_unalterable_data();
1851 $self->pre_render();
1853 # trigger rendering values for second row as hidden, because they
1854 # are loaded only on demand. So we need to keep the values from the
1856 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1860 title => $self->get_title_for('edit'),
1861 %{$self->{template_args}}
1865 sub workflow_sales_or_purchase_order {
1869 my $errors = $self->save();
1871 if (scalar @{ $errors }) {
1872 $self->js->flash('error', $_) foreach @{ $errors };
1873 return $self->js->render();
1876 my $destination_type = $::form->{type} eq sales_quotation_type() ? sales_order_type()
1877 : $::form->{type} eq request_quotation_type() ? purchase_order_type()
1878 : $::form->{type} eq purchase_order_type() ? sales_order_type()
1879 : $::form->{type} eq sales_order_type() ? purchase_order_type()
1882 # check for direct delivery
1883 # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
1885 if ( $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
1886 && $::form->{use_shipto} && $self->order->shipto) {
1887 $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
1890 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1891 $self->{converted_from_oe_id} = delete $::form->{id};
1893 # set item ids to new fake id, to identify them as new items
1894 foreach my $item (@{$self->order->items_sorted}) {
1895 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1898 if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
1899 if ($::form->{use_shipto}) {
1900 $self->order->custom_shipto($custom_shipto) if $custom_shipto;
1902 # remove any custom shipto if not wanted
1903 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1908 $::form->{type} = $destination_type;
1909 $self->type($self->init_type);
1910 $self->cv ($self->init_cv);
1914 $self->get_unalterable_data();
1915 $self->pre_render();
1917 # trigger rendering values for second row as hidden, because they
1918 # are loaded only on demand. So we need to keep the values from the
1920 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1924 title => $self->get_title_for('edit'),
1925 %{$self->{template_args}}
1933 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1934 $self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
1935 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1936 $self->{all_languages} = SL::DB::Manager::Language->get_all_sorted();
1937 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1940 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1943 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1945 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1946 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1947 $self->{periodic_invoices_status} = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
1948 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1949 $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
1951 my $print_form = Form->new('');
1952 $print_form->{type} = $self->type;
1953 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1954 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1955 form => $print_form,
1956 options => {dialog_name_prefix => 'print_options.',
1960 no_opendocument => 0,
1964 foreach my $item (@{$self->order->orderitems}) {
1965 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1966 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1967 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1970 if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
1971 # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
1972 # Do not use write_to_objects to prevent order->delivered to be set, because this should be
1973 # the value from db, which can be set manually or is set when linked delivery orders are saved.
1974 SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
1977 if ($self->order->number && $::instance_conf->get_webdav) {
1978 my $webdav = SL::Webdav->new(
1979 type => $self->type,
1980 number => $self->order->number,
1982 my @all_objects = $webdav->get_all_objects;
1983 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1985 link => File::Spec->catfile($_->full_filedescriptor),
1989 if ( (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
1990 && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
1991 $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
1994 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1996 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
1997 edit_periodic_invoices_config calculate_qty follow_up show_history);
1998 $self->setup_edit_action_bar;
2001 sub setup_edit_action_bar {
2002 my ($self, %params) = @_;
2004 my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
2005 || (($self->type eq sales_order_type()) && $::instance_conf->get_sales_order_show_delete)
2006 || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
2008 my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
2009 my @req_cusordnumber = qw(kivi.Order.check_cusordnumber_presence) x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
2011 my $has_invoice_for_advance_payment;
2012 if ($self->order->id && $self->type eq sales_order_type()) {
2013 my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
2014 $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
2017 my $has_final_invoice;
2018 if ($self->order->id && $self->type eq sales_order_type()) {
2019 my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
2020 $has_final_invoice = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
2023 for my $bar ($::request->layout->get('actionbar')) {
2028 call => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
2029 $::instance_conf->get_order_warn_no_deliverydate,
2031 checks => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
2032 @req_trans_cost_art, @req_cusordnumber,
2037 call => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
2038 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2039 @req_trans_cost_art, @req_cusordnumber,
2041 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2043 ], # end of combobox "Save"
2050 t8('Save and Quotation'),
2051 submit => [ '#order_form', { action => "Order/sales_quotation" } ],
2052 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2053 only_if => (any { $self->type eq $_ } (sales_order_type())),
2057 submit => [ '#order_form', { action => "Order/request_for_quotation" } ],
2058 only_if => (any { $self->type eq $_ } (purchase_order_type())),
2061 t8('Save and Sales Order'),
2062 submit => [ '#order_form', { action => "Order/sales_order" } ],
2063 checks => [ @req_trans_cost_art ],
2064 only_if => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
2067 t8('Save and Purchase Order'),
2068 call => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
2069 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2070 only_if => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
2073 t8('Save and Delivery Order'),
2074 call => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
2075 $::instance_conf->get_order_warn_no_deliverydate,
2077 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2078 @req_trans_cost_art, @req_cusordnumber,
2080 only_if => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
2083 t8('Save and Supplier Delivery Order'),
2084 call => [ 'kivi.Order.save', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
2085 $::instance_conf->get_order_warn_no_deliverydate,
2087 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2088 @req_trans_cost_art, @req_cusordnumber,
2090 only_if => (any { $self->type eq $_ } (purchase_order_type()))
2093 t8('Save and Invoice'),
2094 call => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
2095 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2096 @req_trans_cost_art, @req_cusordnumber,
2100 ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
2101 call => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::instance_conf->get_order_warn_duplicate_parts ],
2102 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2103 @req_trans_cost_art, @req_cusordnumber,
2105 disabled => $has_final_invoice ? t8('This order has already a final invoice.')
2107 only_if => (any { $self->type eq $_ } (sales_order_type())),
2110 t8('Save and Final Invoice'),
2111 call => [ 'kivi.Order.save', 'save_and_final_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
2112 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2113 @req_trans_cost_art, @req_cusordnumber,
2115 disabled => $has_final_invoice ? t8('This order has already a final invoice.')
2117 only_if => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
2120 t8('Save and AP Transaction'),
2121 call => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
2122 only_if => (any { $self->type eq $_ } (purchase_order_type()))
2125 ], # end of combobox "Workflow"
2132 t8('Save and preview PDF'),
2133 call => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
2134 $::instance_conf->get_order_warn_no_deliverydate,
2136 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2139 t8('Save and print'),
2140 call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
2141 $::instance_conf->get_order_warn_no_deliverydate,
2143 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2146 t8('Save and E-mail'),
2147 id => 'save_and_email_action',
2148 call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
2149 $::instance_conf->get_order_warn_no_deliverydate,
2151 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2154 t8('Download attachments of all parts'),
2155 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
2156 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2157 only_if => $::instance_conf->get_doc_storage,
2159 ], # end of combobox "Export"
2163 call => [ 'kivi.Order.delete_order' ],
2164 confirm => $::locale->text('Do you really want to delete this object?'),
2165 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2166 only_if => $deletion_allowed,
2175 call => [ 'set_history_window', $self->order->id, 'id' ],
2176 disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
2180 call => [ 'kivi.Order.follow_up_window' ],
2181 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2182 only_if => $::auth->assert('productivity', 1),
2184 ], # end of combobox "more"
2190 my ($self, $doc_ref, $params) = @_;
2192 my $order = $self->order;
2195 my $print_form = Form->new('');
2196 $print_form->{type} = $order->type;
2197 $print_form->{formname} = $params->{formname} || $order->type;
2198 $print_form->{format} = $params->{format} || 'pdf';
2199 $print_form->{media} = $params->{media} || 'file';
2200 $print_form->{groupitems} = $params->{groupitems};
2201 $print_form->{printer_id} = $params->{printer_id};
2202 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
2204 $order->language($params->{language});
2205 $order->flatten_to_form($print_form, format_amounts => 1);
2209 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
2210 $template_ext = 'odt';
2211 $template_type = 'OpenDocument';
2212 } elsif ($print_form->{format} =~ m{html}i) {
2213 $template_ext = 'html';
2214 $template_type = 'HTML';
2217 # search for the template
2218 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
2219 name => $print_form->{formname},
2220 extension => $template_ext,
2221 email => $print_form->{media} eq 'email',
2222 language => $params->{language},
2223 printer_id => $print_form->{printer_id},
2226 if (!defined $template_file) {
2227 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);
2230 return @errors if scalar @errors;
2232 $print_form->throw_on_error(sub {
2234 $print_form->prepare_for_printing;
2236 $$doc_ref = SL::Helper::CreatePDF->create_pdf(
2237 format => $print_form->{format},
2238 template_type => $template_type,
2239 template => $template_file,
2240 variables => $print_form,
2241 variable_content_types => {
2242 longdescription => 'html',
2243 partnotes => 'html',
2245 $::form->get_variable_content_types_for_cvars,
2249 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
2255 sub get_files_for_email_dialog {
2258 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
2260 return %files if !$::instance_conf->get_doc_storage;
2262 if ($self->order->id) {
2263 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
2264 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
2265 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
2266 $files{project_files} = [ SL::File->get_all( object_id => $self->order->globalproject_id, object_type => 'project', file_type => 'attachment') ];
2270 uniq_by { $_->{id} }
2272 +{ id => $_->part->id,
2273 partnumber => $_->part->partnumber }
2274 } @{$self->order->items_sorted};
2276 foreach my $part (@parts) {
2277 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
2278 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
2281 foreach my $key (keys %files) {
2282 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
2288 sub make_periodic_invoices_config_from_yaml {
2289 my ($yaml_config) = @_;
2291 return if !$yaml_config;
2292 my $attr = SL::YAML::Load($yaml_config);
2293 return if 'HASH' ne ref $attr;
2294 return SL::DB::PeriodicInvoicesConfig->new(%$attr);
2298 sub get_periodic_invoices_status {
2299 my ($self, $config) = @_;
2301 return if $self->type ne sales_order_type();
2302 return t8('not configured') if !$config;
2304 my $active = ('HASH' eq ref $config) ? $config->{active}
2305 : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
2306 : die "Cannot get status of periodic invoices config";
2308 return $active ? t8('active') : t8('inactive');
2312 my ($self, $action) = @_;
2314 return '' if none { lc($action)} qw(add edit);
2317 # $::locale->text("Add Sales Order");
2318 # $::locale->text("Add Purchase Order");
2319 # $::locale->text("Add Quotation");
2320 # $::locale->text("Add Request for Quotation");
2321 # $::locale->text("Edit Sales Order");
2322 # $::locale->text("Edit Purchase Order");
2323 # $::locale->text("Edit Quotation");
2324 # $::locale->text("Edit Request for Quotation");
2326 $action = ucfirst(lc($action));
2327 return $self->type eq sales_order_type() ? $::locale->text("$action Sales Order")
2328 : $self->type eq purchase_order_type() ? $::locale->text("$action Purchase Order")
2329 : $self->type eq sales_quotation_type() ? $::locale->text("$action Quotation")
2330 : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
2334 sub get_item_cvpartnumber {
2335 my ($self, $item) = @_;
2337 return if !$self->search_cvpartnumber;
2338 return if !$self->order->customervendor;
2340 if ($self->cv eq 'vendor') {
2341 my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
2342 $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
2343 } elsif ($self->cv eq 'customer') {
2344 my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
2345 $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
2349 sub get_part_texts {
2350 my ($part_or_id, $language_or_id, %defaults) = @_;
2352 my $part = ref($part_or_id) ? $part_or_id : SL::DB::Part->load_cached($part_or_id);
2353 my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
2355 description => $defaults{description} // $part->description,
2356 longdescription => $defaults{longdescription} // $part->notes,
2359 return $texts unless $language_id;
2361 my $translation = SL::DB::Manager::Translation->get_first(
2363 parts_id => $part->id,
2364 language_id => $language_id,
2367 $texts->{description} = $translation->translation if $translation && $translation->translation;
2368 $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
2373 sub sales_order_type {
2377 sub purchase_order_type {
2381 sub sales_quotation_type {
2385 sub request_quotation_type {
2386 'request_quotation';
2390 return $_[0]->type eq sales_order_type() ? 'ordnumber'
2391 : $_[0]->type eq purchase_order_type() ? 'ordnumber'
2392 : $_[0]->type eq sales_quotation_type() ? 'quonumber'
2393 : $_[0]->type eq request_quotation_type() ? 'quonumber'
2397 sub save_and_redirect_to {
2398 my ($self, %params) = @_;
2400 my $errors = $self->save();
2402 if (scalar @{ $errors }) {
2403 $self->js->flash('error', $_) foreach @{ $errors };
2404 return $self->js->render();
2407 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
2408 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
2409 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
2410 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
2412 flash_later('info', $text);
2414 $self->redirect_to(%params, id => $self->order->id);
2418 my ($self, $addition) = @_;
2420 my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
2421 my $snumbers = $number_type . '_' . $self->order->$number_type;
2423 SL::DB::History->new(
2424 trans_id => $self->order->id,
2425 employee_id => SL::DB::Manager::Employee->current->id,
2426 what_done => $self->order->type,
2427 snumbers => $snumbers,
2428 addition => $addition,
2432 sub store_doc_to_webdav_and_filemanagement {
2433 my ($self, $content, $filename, $variant) = @_;
2435 my $order = $self->order;
2438 # copy file to webdav folder
2439 if ($order->number && $::instance_conf->get_webdav_documents) {
2440 my $webdav = SL::Webdav->new(
2441 type => $order->type,
2442 number => $order->number,
2444 my $webdav_file = SL::Webdav::File->new(
2446 filename => $filename,
2449 $webdav_file->store(data => \$content);
2452 push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
2455 if ($order->id && $::instance_conf->get_doc_storage) {
2457 SL::File->save(object_id => $order->id,
2458 object_type => $order->type,
2459 mime_type => SL::MIME->mime_type_from_ext($filename),
2460 source => 'created',
2461 file_type => 'document',
2462 file_name => $filename,
2463 file_contents => $content,
2464 print_variant => $variant);
2467 push @errors, t8('Storing the document in the storage backend failed: #1', $@);
2474 sub link_requirement_specs_linking_to_created_from_objects {
2475 my ($self, @converted_from_oe_ids) = @_;
2477 return unless @converted_from_oe_ids;
2479 my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
2480 foreach my $rs_order (@{ $rs_orders }) {
2481 SL::DB::RequirementSpecOrder->new(
2482 order_id => $self->order->id,
2483 requirement_spec_id => $rs_order->requirement_spec_id,
2484 version_id => $rs_order->version_id,
2489 sub set_project_in_linked_requirement_specs {
2492 my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
2493 foreach my $rs_order (@{ $rs_orders }) {
2494 next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
2496 $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
2508 SL::Controller::Order - controller for orders
2512 This is a new form to enter orders, completely rewritten with the use
2513 of controller and java script techniques.
2515 The aim is to provide the user a better experience and a faster workflow. Also
2516 the code should be more readable, more reliable and better to maintain.
2524 One input row, so that input happens every time at the same place.
2528 Use of pickers where possible.
2532 Possibility to enter more than one item at once.
2536 Item list in a scrollable area, so that the workflow buttons stay at
2541 Reordering item rows with drag and drop is possible. Sorting item rows is
2542 possible (by partnumber, description, qty, sellprice and discount for now).
2546 No C<update> is necessary. All entries and calculations are managed
2547 with ajax-calls and the page only reloads on C<save>.
2551 User can see changes immediately, because of the use of java script
2562 =item * C<SL/Controller/Order.pm>
2566 =item * C<template/webpages/order/form.html>
2570 =item * C<template/webpages/order/tabs/basic_data.html>
2572 Main tab for basic_data.
2574 This is the only tab here for now. "linked records" and "webdav" tabs are
2575 reused from generic code.
2579 =item * C<template/webpages/order/tabs/_business_info_row.html>
2581 For displaying information on business type
2583 =item * C<template/webpages/order/tabs/_item_input.html>
2585 The input line for items
2587 =item * C<template/webpages/order/tabs/_row.html>
2589 One row for already entered items
2591 =item * C<template/webpages/order/tabs/_tax_row.html>
2593 Displaying tax information
2595 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
2597 Dialog for selecting price and discount sources
2601 =item * C<js/kivi.Order.js>
2603 java script functions
2613 =item * price sources: little symbols showing better price / better discount
2615 =item * select units in input row?
2617 =item * check for direct delivery (workflow sales order -> purchase order)
2619 =item * access rights
2621 =item * display weights
2625 =item * optional client/user behaviour
2627 (transactions has to be set - department has to be set -
2628 force project if enabled in client config)
2632 =head1 KNOWN BUGS AND CAVEATS
2638 Customer discount is not displayed as a valid discount in price source popup
2639 (this might be a bug in price sources)
2641 (I cannot reproduce this (Bernd))
2645 No indication that <shift>-up/down expands/collapses second row.
2649 Inline creation of parts is not currently supported
2653 Table header is not sticky in the scrolling area.
2657 Sorting does not include C<position>, neither does reordering.
2659 This behavior was implemented intentionally. But we can discuss, which behavior
2660 should be implemented.
2664 =head1 To discuss / Nice to have
2670 How to expand/collapse second row. Now it can be done clicking the icon or
2675 Possibility to select PriceSources in input row?
2679 This controller uses a (changed) copy of the template for the PriceSource
2680 dialog. Maybe there could be used one code source.
2684 Rounding-differences between this controller (PriceTaxCalculator) and the old
2685 form. This is not only a problem here, but also in all parts using the PTC.
2686 There exists a ticket and a patch. This patch should be testet.
2690 An indicator, if the actual inputs are saved (like in an
2691 editor or on text processing application).
2695 A warning when leaving the page without saveing unchanged inputs.
2702 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>