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, { format => $format,
305 formname => $formname,
306 language => $self->order->language,
307 printer_id => $printer_id,
308 groupitems => $groupitems });
309 if (scalar @errors) {
310 return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
313 if ($media eq 'screen') {
315 $self->js->flash('info', t8('The document has been created.'));
318 type => SL::MIME->mime_type_from_ext($doc_filename),
319 name => $doc_filename,
323 } elsif ($media eq 'printer') {
325 my $printer_id = $::form->{print_options}->{printer_id};
326 SL::DB::Printer->new(id => $printer_id)->load->print_document(
331 $self->js->flash('info', t8('The document has been printed.'));
334 my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
335 if (scalar @warnings) {
336 $self->js->flash('warning', $_) for @warnings;
339 $self->save_history('PRINTED');
342 ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
345 sub action_preview_pdf {
348 my $errors = $self->save();
349 if (scalar @{ $errors }) {
350 $self->js->flash('error', $_) foreach @{ $errors };
351 return $self->js->render();
354 $self->js_reset_order_and_item_ids_after_save;
357 my $media = 'screen';
358 my $formname = $self->type;
361 # create a form for generate_attachment_filename
362 my $form = Form->new;
363 $form->{$self->nr_key()} = $self->order->number;
364 $form->{type} = $self->type;
365 $form->{format} = $format;
366 $form->{formname} = $formname;
367 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
368 my $pdf_filename = $form->generate_attachment_filename();
371 my @errors = $self->generate_doc(\$pdf, { format => $format,
372 formname => $formname,
373 language => $self->order->language,
375 if (scalar @errors) {
376 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
378 $self->save_history('PREVIEWED');
379 $self->js->flash('info', t8('The PDF has been previewed'));
383 type => SL::MIME->mime_type_from_ext($pdf_filename),
384 name => $pdf_filename,
389 # open the email dialog
390 sub action_save_and_show_email_dialog {
393 my $errors = $self->save();
395 if (scalar @{ $errors }) {
396 $self->js->flash('error', $_) foreach @{ $errors };
397 return $self->js->render();
400 my $cv_method = $self->cv;
402 if (!$self->order->$cv_method) {
403 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'))
408 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
409 $email_form->{to} ||= $self->order->$cv_method->email;
410 $email_form->{cc} = $self->order->$cv_method->cc;
411 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
412 # Todo: get addresses from shipto, if any
414 my $form = Form->new;
415 $form->{$self->nr_key()} = $self->order->number;
416 $form->{cusordnumber} = $self->order->cusordnumber;
417 $form->{formname} = $self->type;
418 $form->{type} = $self->type;
419 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
420 $form->{language_id} = $self->order->language->id if $self->order->language;
421 $form->{format} = 'pdf';
422 $form->{cp_id} = $self->order->contact->cp_id if $self->order->contact;
424 $email_form->{subject} = $form->generate_email_subject();
425 $email_form->{attachment_filename} = $form->generate_attachment_filename();
426 $email_form->{message} = $form->generate_email_body();
427 $email_form->{js_send_function} = 'kivi.Order.send_email()';
429 my %files = $self->get_files_for_email_dialog();
431 my @employees_with_email = grep {
432 my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
433 $user && !!trim($user->get_config_value('email'));
434 } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
436 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
437 email_form => $email_form,
438 show_bcc => $::auth->assert('email_bcc', 'may fail'),
440 is_customer => $self->cv eq 'customer',
441 ALL_EMPLOYEES => \@employees_with_email,
445 ->run('kivi.Order.show_email_dialog', $dialog_html)
452 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
453 sub action_send_email {
456 my $errors = $self->save();
458 if (scalar @{ $errors }) {
459 $self->js->run('kivi.Order.close_email_dialog');
460 $self->js->flash('error', $_) foreach @{ $errors };
461 return $self->js->render();
464 $self->js_reset_order_and_item_ids_after_save;
466 my $email_form = delete $::form->{email_form};
467 my %field_names = (to => 'email');
469 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
471 # for Form::cleanup which may be called in Form::send_email
472 $::form->{cwd} = getcwd();
473 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
475 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
476 $::form->{media} = 'email';
478 $::form->{attachment_policy} //= '';
480 # Is an old file version available?
482 if ($::form->{attachment_policy} eq 'old_file') {
483 $attfile = SL::File->get_all(object_id => $self->order->id,
484 object_type => $self->type,
485 file_type => 'document',
486 print_variant => $::form->{formname});
489 if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
491 my @errors = $self->generate_doc(\$doc, {media => $::form->{media},
492 format => $::form->{print_options}->{format},
493 formname => $::form->{print_options}->{formname},
494 language => $self->order->language,
495 printer_id => $::form->{print_options}->{printer_id},
496 groupitems => $::form->{print_options}->{groupitems}});
497 if (scalar @errors) {
498 return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
501 my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename}, $::form->{formname});
502 if (scalar @warnings) {
503 flash_later('warning', $_) for @warnings;
506 my $sfile = SL::SessionFile::Random->new(mode => "w");
507 $sfile->fh->print($doc);
510 $::form->{tmpfile} = $sfile->file_name;
511 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
514 $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
515 $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
518 my $intnotes = $self->order->intnotes;
519 $intnotes .= "\n\n" if $self->order->intnotes;
520 $intnotes .= t8('[email]') . "\n";
521 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
522 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
523 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
524 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
525 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
526 $intnotes .= t8('Message') . ": " . SL::HTML::Util->strip($::form->{message});
528 $self->order->update_attributes(intnotes => $intnotes);
530 $self->save_history('MAILED');
532 flash_later('info', t8('The email has been sent.'));
534 my @redirect_params = (
537 id => $self->order->id,
540 $self->redirect_to(@redirect_params);
543 # open the periodic invoices config dialog
545 # If there are values in the form (i.e. dialog was opened before),
546 # then use this values. Create new ones, else.
547 sub action_show_periodic_invoices_config_dialog {
550 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
551 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
552 $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm',
553 order_value_periodicity => 'p', # = same as periodicity
554 start_date_as_date => $::form->{transdate_as_date} || $::form->current_date,
555 extend_automatically_by => 12,
557 email_subject => GenericTranslations->get(
558 language_id => $::form->{language_id},
559 translation_type =>"preset_text_periodic_invoices_email_subject"),
560 email_body => GenericTranslations->get(
561 language_id => $::form->{language_id},
562 translation_type => "salutation_general")
563 . GenericTranslations->get(
564 language_id => $::form->{language_id},
565 translation_type => "salutation_punctuation_mark") . "\n\n"
566 . GenericTranslations->get(
567 language_id => $::form->{language_id},
568 translation_type =>"preset_text_periodic_invoices_email_body"),
570 # for older configs, replace email preset text if not yet set.
571 $config->email_subject(GenericTranslations->get(
572 language_id => $::form->{language_id},
573 translation_type =>"preset_text_periodic_invoices_email_subject")
574 ) unless $config->email_subject;
576 $config->email_body(GenericTranslations->get(
577 language_id => $::form->{language_id},
578 translation_type => "salutation_general")
579 . GenericTranslations->get(
580 language_id => $::form->{language_id},
581 translation_type => "salutation_punctuation_mark") . "\n\n"
582 . GenericTranslations->get(
583 language_id => $::form->{language_id},
584 translation_type =>"preset_text_periodic_invoices_email_body")
585 ) unless $config->email_body;
587 $config->periodicity('m') if none { $_ eq $config->periodicity } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
588 $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
590 $::form->get_lists(printers => "ALL_PRINTERS",
591 charts => { key => 'ALL_CHARTS',
592 transdate => 'current_date' });
594 $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
596 if ($::form->{customer_id}) {
597 $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
598 my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
599 $::form->{postal_invoice} = $customer_object->postal_invoice;
600 $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
601 $config->send_email(0) if $::form->{postal_invoice};
604 $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
606 popup_js_close_function => 'kivi.Order.close_periodic_invoices_config_dialog()',
607 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
612 # assign the values of the periodic invoices config dialog
613 # as yaml in the hidden tag and set the status.
614 sub action_assign_periodic_invoices_config {
617 $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
619 my $config = { active => $::form->{active} ? 1 : 0,
620 terminated => $::form->{terminated} ? 1 : 0,
621 direct_debit => $::form->{direct_debit} ? 1 : 0,
622 periodicity => (any { $_ eq $::form->{periodicity} } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES) ? $::form->{periodicity} : 'm',
623 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
624 start_date_as_date => $::form->{start_date_as_date},
625 end_date_as_date => $::form->{end_date_as_date},
626 first_billing_date_as_date => $::form->{first_billing_date_as_date},
627 print => $::form->{print} ? 1 : 0,
628 printer_id => $::form->{print} ? $::form->{printer_id} * 1 : undef,
629 copies => $::form->{copies} * 1 ? $::form->{copies} : 1,
630 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
631 ar_chart_id => $::form->{ar_chart_id} * 1,
632 send_email => $::form->{send_email} ? 1 : 0,
633 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
634 email_recipient_address => $::form->{email_recipient_address},
635 email_sender => $::form->{email_sender},
636 email_subject => $::form->{email_subject},
637 email_body => $::form->{email_body},
640 my $periodic_invoices_config = SL::YAML::Dump($config);
642 my $status = $self->get_periodic_invoices_status($config);
645 ->remove('#order_periodic_invoices_config')
646 ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
647 ->run('kivi.Order.close_periodic_invoices_config_dialog')
648 ->html('#periodic_invoices_status', $status)
649 ->flash('info', t8('The periodic invoices config has been assigned.'))
653 sub action_get_has_active_periodic_invoices {
656 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
657 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
659 my $has_active_periodic_invoices =
660 $self->type eq sales_order_type()
663 && (!$config->end_date || ($config->end_date > DateTime->today_local))
664 && $config->get_previous_billed_period_start_date;
666 $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
669 # save the order and redirect to the frontend subroutine for a new
671 sub action_save_and_delivery_order {
674 $self->save_and_redirect_to(
675 controller => 'oe.pl',
676 action => 'oe_delivery_order_from_order',
680 # save the order and redirect to the frontend subroutine for a new
682 sub action_save_and_invoice {
685 $self->save_and_redirect_to(
686 controller => 'oe.pl',
687 action => 'oe_invoice_from_order',
691 sub action_save_and_invoice_for_advance_payment {
694 $self->save_and_redirect_to(
695 controller => 'oe.pl',
696 action => 'oe_invoice_from_order',
697 new_invoice_type => 'invoice_for_advance_payment',
701 sub action_save_and_final_invoice {
704 $self->save_and_redirect_to(
705 controller => 'oe.pl',
706 action => 'oe_invoice_from_order',
707 new_invoice_type => 'final_invoice',
711 # workflow from sales order to sales quotation
712 sub action_sales_quotation {
713 $_[0]->workflow_sales_or_request_for_quotation();
716 # workflow from sales order to sales quotation
717 sub action_request_for_quotation {
718 $_[0]->workflow_sales_or_request_for_quotation();
721 # workflow from sales quotation to sales order
722 sub action_sales_order {
723 $_[0]->workflow_sales_or_purchase_order();
726 # workflow from rfq to purchase order
727 sub action_purchase_order {
728 $_[0]->workflow_sales_or_purchase_order();
731 # workflow from purchase order to ap transaction
732 sub action_save_and_ap_transaction {
735 $self->save_and_redirect_to(
736 controller => 'ap.pl',
737 action => 'add_from_purchase_order',
741 # set form elements in respect to a changed customer or vendor
743 # This action is called on an change of the customer/vendor picker.
744 sub action_customer_vendor_changed {
747 setup_order_from_cv($self->order);
750 my $cv_method = $self->cv;
752 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
753 $self->js->show('#cp_row');
755 $self->js->hide('#cp_row');
758 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
759 $self->js->show('#shipto_selection');
761 $self->js->hide('#shipto_selection');
764 if ($cv_method eq 'customer') {
765 my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
766 $self->js->$show_hide('#billing_address_row');
769 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
772 ->replaceWith('#order_cp_id', $self->build_contact_select)
773 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
774 ->replaceWith('#shipto_inputs ', $self->build_shipto_inputs)
775 ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
776 ->replaceWith('#business_info_row', $self->build_business_info_row)
777 ->val( '#order_taxzone_id', $self->order->taxzone_id)
778 ->val( '#order_taxincluded', $self->order->taxincluded)
779 ->val( '#order_currency_id', $self->order->currency_id)
780 ->val( '#order_payment_id', $self->order->payment_id)
781 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
782 ->val( '#order_intnotes', $self->order->intnotes)
783 ->val( '#order_language_id', $self->order->$cv_method->language_id)
784 ->focus( '#order_' . $self->cv . '_id')
785 ->run('kivi.Order.update_exchangerate');
787 $self->js_redisplay_amounts_and_taxes;
788 $self->js_redisplay_cvpartnumbers;
792 # open the dialog for customer/vendor details
793 sub action_show_customer_vendor_details_dialog {
796 my $is_customer = 'customer' eq $::form->{vc};
799 $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
801 $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
804 my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
805 $details{discount_as_percent} = $cv->discount_as_percent;
806 $details{creditlimt} = $cv->creditlimit_as_number;
807 $details{business} = $cv->business->description if $cv->business;
808 $details{language} = $cv->language_obj->description if $cv->language_obj;
809 $details{delivery_terms} = $cv->delivery_term->description if $cv->delivery_term;
810 $details{payment_terms} = $cv->payment->description if $cv->payment;
811 $details{pricegroup} = $cv->pricegroup->pricegroup if $is_customer && $cv->pricegroup;
814 foreach my $entry (@{ $cv->additional_billing_addresses }) {
815 push @{ $details{ADDITIONAL_BILLING_ADDRESSES} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
818 foreach my $entry (@{ $cv->shipto }) {
819 push @{ $details{SHIPTO} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
821 foreach my $entry (@{ $cv->contacts }) {
822 push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
825 $_[0]->render('common/show_vc_details', { layout => 0 },
826 is_customer => $is_customer,
831 # called if a unit in an existing item row is changed
832 sub action_unit_changed {
835 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
836 my $item = $self->order->items_sorted->[$idx];
838 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
839 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
844 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
845 $self->js_redisplay_line_values;
846 $self->js_redisplay_amounts_and_taxes;
850 # add an item row for a new item entered in the input row
851 sub action_add_item {
854 delete $::form->{add_item}->{create_part_type};
856 my $form_attr = $::form->{add_item};
858 return unless $form_attr->{parts_id};
860 my $item = new_item($self->order, $form_attr);
862 $self->order->add_items($item);
866 $self->get_item_cvpartnumber($item);
868 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
869 my $row_as_html = $self->p->render('order/tabs/_row',
875 if ($::form->{insert_before_item_id}) {
877 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
880 ->append('#row_table_id', $row_as_html);
883 if ( $item->part->is_assortment ) {
884 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
885 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
886 my $attr = { parts_id => $assortment_item->parts_id,
887 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
888 unit => $assortment_item->unit,
889 description => $assortment_item->part->description,
891 my $item = new_item($self->order, $attr);
893 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
894 $item->discount(1) unless $assortment_item->charge;
896 $self->order->add_items( $item );
898 $self->get_item_cvpartnumber($item);
899 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
900 my $row_as_html = $self->p->render('order/tabs/_row',
905 if ($::form->{insert_before_item_id}) {
907 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
910 ->append('#row_table_id', $row_as_html);
916 ->val('.add_item_input', '')
917 ->run('kivi.Order.init_row_handlers')
918 ->run('kivi.Order.renumber_positions')
919 ->focus('#add_item_parts_id_name');
921 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
923 $self->js_redisplay_amounts_and_taxes;
927 # add item rows for multiple items at once
928 sub action_add_multi_items {
931 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
932 return $self->js->render() unless scalar @form_attr;
935 foreach my $attr (@form_attr) {
936 my $item = new_item($self->order, $attr);
938 if ( $item->part->is_assortment ) {
939 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
940 my $attr = { parts_id => $assortment_item->parts_id,
941 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
942 unit => $assortment_item->unit,
943 description => $assortment_item->part->description,
945 my $item = new_item($self->order, $attr);
947 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
948 $item->discount(1) unless $assortment_item->charge;
953 $self->order->add_items(@items);
957 foreach my $item (@items) {
958 $self->get_item_cvpartnumber($item);
959 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
960 my $row_as_html = $self->p->render('order/tabs/_row',
966 if ($::form->{insert_before_item_id}) {
968 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
971 ->append('#row_table_id', $row_as_html);
976 ->run('kivi.Part.close_picker_dialogs')
977 ->run('kivi.Order.init_row_handlers')
978 ->run('kivi.Order.renumber_positions')
979 ->focus('#add_item_parts_id_name');
981 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
983 $self->js_redisplay_amounts_and_taxes;
987 # recalculate all linetotals, amounts and taxes and redisplay them
988 sub action_recalc_amounts_and_taxes {
993 $self->js_redisplay_line_values;
994 $self->js_redisplay_amounts_and_taxes;
998 sub action_update_exchangerate {
1002 is_standard => $self->order->currency_id == $::instance_conf->get_currency_id,
1003 currency_name => $self->order->currency->name,
1004 exchangerate => $self->order->daily_exchangerate_as_null_number,
1007 $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
1010 # redisplay item rows if they are sorted by an attribute
1011 sub action_reorder_items {
1015 partnumber => sub { $_[0]->part->partnumber },
1016 description => sub { $_[0]->description },
1017 qty => sub { $_[0]->qty },
1018 sellprice => sub { $_[0]->sellprice },
1019 discount => sub { $_[0]->discount },
1020 cvpartnumber => sub { $_[0]->{cvpartnumber} },
1023 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1025 my $method = $sort_keys{$::form->{order_by}};
1026 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
1027 if ($::form->{sort_dir}) {
1028 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
1029 @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
1031 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
1034 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
1035 @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
1037 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
1041 ->run('kivi.Order.redisplay_items', \@to_sort)
1045 # show the popup to choose a price/discount source
1046 sub action_price_popup {
1049 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
1050 my $item = $self->order->items_sorted->[$idx];
1052 $self->render_price_dialog($item);
1055 # save the order in a session variable and redirect to the part controller
1056 sub action_create_part {
1059 my $previousform = $::auth->save_form_in_session(non_scalars => 1);
1061 my $callback = $self->url_for(
1062 action => 'return_from_create_part',
1063 type => $self->type, # type is needed for check_auth on return
1064 previousform => $previousform,
1067 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.'));
1069 my @redirect_params = (
1070 controller => 'Part',
1072 part_type => $::form->{add_item}->{create_part_type},
1073 callback => $callback,
1077 $self->redirect_to(@redirect_params);
1080 sub action_return_from_create_part {
1083 $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
1085 $::auth->restore_form_from_session(delete $::form->{previousform});
1087 # set item ids to new fake id, to identify them as new items
1088 foreach my $item (@{$self->order->items_sorted}) {
1089 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1093 $self->get_unalterable_data();
1094 $self->pre_render();
1096 # trigger rendering values for second row/longdescription as hidden,
1097 # because they are loaded only on demand. So we need to keep the values
1099 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1100 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
1104 title => $self->get_title_for('edit'),
1105 %{$self->{template_args}}
1110 # load the second row for one or more items
1112 # This action gets the html code for all items second rows by rendering a template for
1113 # the second row and sets the html code via client js.
1114 sub action_load_second_rows {
1117 $self->recalc() if $self->order->is_sales; # for margin calculation
1119 foreach my $item_id (@{ $::form->{item_ids} }) {
1120 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1121 my $item = $self->order->items_sorted->[$idx];
1123 $self->js_load_second_row($item, $item_id, 0);
1126 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
1128 $self->js->render();
1131 # update description, notes and sellprice from master data
1132 sub action_update_row_from_master_data {
1135 foreach my $item_id (@{ $::form->{item_ids} }) {
1136 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1137 my $item = $self->order->items_sorted->[$idx];
1138 my $texts = get_part_texts($item->part, $self->order->language_id);
1140 $item->description($texts->{description});
1141 $item->longdescription($texts->{longdescription});
1143 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1146 if ($item->part->is_assortment) {
1147 # add assortment items with price 0, as the components carry the price
1148 $price_src = $price_source->price_from_source("");
1149 $price_src->price(0);
1151 $price_src = $price_source->best_price
1152 ? $price_source->best_price
1153 : $price_source->price_from_source("");
1154 $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
1155 $price_src->price(0) if !$price_source->best_price;
1159 $item->sellprice($price_src->price);
1160 $item->active_price_source($price_src);
1163 ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
1164 ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
1165 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
1166 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
1168 if ($self->search_cvpartnumber) {
1169 $self->get_item_cvpartnumber($item);
1170 $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
1175 $self->js_redisplay_line_values;
1176 $self->js_redisplay_amounts_and_taxes;
1178 $self->js->render();
1181 sub js_load_second_row {
1182 my ($self, $item, $item_id, $do_parse) = @_;
1185 # Parse values from form (they are formated while rendering (template)).
1186 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1187 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
1188 foreach my $var (@{ $item->cvars_by_config }) {
1189 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1191 $item->parse_custom_variable_values;
1194 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
1197 ->html('#second_row_' . $item_id, $row_as_html)
1198 ->data('#second_row_' . $item_id, 'loaded', 1);
1201 sub js_redisplay_line_values {
1204 my $is_sales = $self->order->is_sales;
1206 # sales orders with margins
1211 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1212 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
1213 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
1214 ]} @{ $self->order->items_sorted };
1218 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1219 ]} @{ $self->order->items_sorted };
1223 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
1226 sub js_redisplay_amounts_and_taxes {
1229 if (scalar @{ $self->{taxes} }) {
1230 $self->js->show('#taxincluded_row_id');
1232 $self->js->hide('#taxincluded_row_id');
1235 if ($self->order->taxincluded) {
1236 $self->js->hide('#subtotal_row_id');
1238 $self->js->show('#subtotal_row_id');
1241 if ($self->order->is_sales) {
1242 my $is_neg = $self->order->marge_total < 0;
1244 ->html('#marge_total_id', $::form->format_amount(\%::myconfig, $self->order->marge_total, 2))
1245 ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
1246 ->action_if( $is_neg, 'addClass', '#marge_total_id', 'plus0')
1247 ->action_if( $is_neg, 'addClass', '#marge_percent_id', 'plus0')
1248 ->action_if( $is_neg, 'addClass', '#marge_percent_sign_id', 'plus0')
1249 ->action_if(!$is_neg, 'removeClass', '#marge_total_id', 'plus0')
1250 ->action_if(!$is_neg, 'removeClass', '#marge_percent_id', 'plus0')
1251 ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
1255 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
1256 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
1257 ->remove('.tax_row')
1258 ->insertBefore($self->build_tax_rows, '#amount_row_id');
1261 sub js_redisplay_cvpartnumbers {
1264 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1266 my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
1269 ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
1272 sub js_reset_order_and_item_ids_after_save {
1276 ->val('#id', $self->order->id)
1277 ->val('#converted_from_oe_id', '')
1278 ->val('#order_' . $self->nr_key(), $self->order->number);
1281 foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
1282 next if !$self->order->items_sorted->[$idx]->id;
1283 next if $form_item_id !~ m{^new};
1285 ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
1286 ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
1287 ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
1291 $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
1298 sub init_valid_types {
1299 [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
1305 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
1306 die "Not a valid type for order";
1309 $self->type($::form->{type});
1315 my $cv = (any { $self->type eq $_ } (sales_order_type(), sales_quotation_type())) ? 'customer'
1316 : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
1317 : die "Not a valid type for order";
1322 sub init_search_cvpartnumber {
1325 my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1326 my $search_cvpartnumber;
1327 $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
1328 $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel() if $self->cv eq 'vendor';
1330 return $search_cvpartnumber;
1333 sub init_show_update_button {
1336 !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1347 sub init_all_price_factors {
1348 SL::DB::Manager::PriceFactor->get_all;
1351 sub init_part_picker_classification_ids {
1353 my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
1355 return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
1361 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
1363 my $right = $right_for->{ $self->type };
1364 $right ||= 'DOES_NOT_EXIST';
1366 $::auth->assert($right);
1369 # build the selection box for contacts
1371 # Needed, if customer/vendor changed.
1372 sub build_contact_select {
1375 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1376 value_key => 'cp_id',
1377 title_key => 'full_name_dep',
1378 default => $self->order->cp_id,
1380 style => 'width: 300px',
1384 # build the selection box for the additional billing address
1386 # Needed, if customer/vendor changed.
1387 sub build_billing_address_select {
1390 return '' if $self->cv ne 'customer';
1392 select_tag('order.billing_address_id',
1393 [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
1395 title_key => 'displayable_id',
1396 default => $self->order->billing_address_id,
1398 style => 'width: 300px',
1402 # build the selection box for shiptos
1404 # Needed, if customer/vendor changed.
1405 sub build_shipto_select {
1408 select_tag('order.shipto_id',
1409 [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
1410 value_key => 'shipto_id',
1411 title_key => 'displayable_id',
1412 default => $self->order->shipto_id,
1414 style => 'width: 300px',
1418 # build the inputs for the cusom shipto dialog
1420 # Needed, if customer/vendor changed.
1421 sub build_shipto_inputs {
1424 my $content = $self->p->render('common/_ship_to_dialog',
1425 vc_obj => $self->order->customervendor,
1426 cs_obj => $self->order->custom_shipto,
1427 cvars => $self->order->custom_shipto->cvars_by_config,
1428 id_selector => '#order_shipto_id');
1430 div_tag($content, id => 'shipto_inputs');
1433 # render the info line for business
1435 # Needed, if customer/vendor changed.
1436 sub build_business_info_row
1438 $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
1441 # build the rows for displaying taxes
1443 # Called if amounts where recalculated and redisplayed.
1444 sub build_tax_rows {
1448 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
1449 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
1451 return $rows_as_html;
1455 sub render_price_dialog {
1456 my ($self, $record_item) = @_;
1458 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1462 'kivi.io.price_chooser_dialog',
1463 t8('Available Prices'),
1464 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1469 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1470 # $self->js->show('#dialog_flash_error');
1479 return if !$::form->{id};
1481 $self->order(SL::DB::Order->new(id => $::form->{id})->load);
1483 # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
1484 # You need a custom shipto object to call cvars_by_config to get the cvars.
1485 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
1487 return $self->order;
1490 # load or create a new order object
1492 # And assign changes from the form to this object.
1493 # If the order is loaded from db, check if items are deleted in the form,
1494 # remove them form the object and collect them for removing from db on saving.
1495 # Then create/update items from form (via make_item) and add them.
1499 # add_items adds items to an order with no items for saving, but they cannot
1500 # be retrieved via items until the order is saved. Adding empty items to new
1501 # order here solves this problem.
1503 $order = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
1504 $order ||= SL::DB::Order->new(orderitems => [],
1505 quotation => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
1506 currency_id => $::instance_conf->get_currency_id(),);
1508 my $cv_id_method = $self->cv . '_id';
1509 if (!$::form->{id} && $::form->{$cv_id_method}) {
1510 $order->$cv_id_method($::form->{$cv_id_method});
1511 setup_order_from_cv($order);
1514 my $form_orderitems = delete $::form->{order}->{orderitems};
1515 my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1517 $order->assign_attributes(%{$::form->{order}});
1519 $self->setup_custom_shipto_from_form($order, $::form);
1521 if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
1522 my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
1523 $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
1526 # remove deleted items
1527 $self->item_ids_to_delete([]);
1528 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1529 my $item = $order->orderitems->[$idx];
1530 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1531 splice @{$order->orderitems}, $idx, 1;
1532 push @{$self->item_ids_to_delete}, $item->id;
1538 foreach my $form_attr (@{$form_orderitems}) {
1539 my $item = make_item($order, $form_attr);
1540 $item->position($pos);
1544 $order->add_items(grep {!$_->id} @items);
1549 # create or update items from form
1551 # Make item objects from form values. For items already existing read from db.
1552 # Create a new item else. And assign attributes.
1554 my ($record, $attr) = @_;
1557 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1559 my $is_new = !$item;
1561 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1562 # they cannot be retrieved via custom_variables until the order/orderitem is
1563 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1564 $item ||= SL::DB::OrderItem->new(custom_variables => []);
1566 $item->assign_attributes(%$attr);
1569 my $texts = get_part_texts($item->part, $record->language_id);
1570 $item->longdescription($texts->{longdescription}) if !defined $attr->{longdescription};
1571 $item->project_id($record->globalproject_id) if !defined $attr->{project_id};
1572 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1580 # This is used to add one item
1582 my ($record, $attr) = @_;
1584 my $item = SL::DB::OrderItem->new;
1586 # Remove attributes where the user left or set the inputs empty.
1587 # So these attributes will be undefined and we can distinguish them
1588 # from zero later on.
1589 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1590 delete $attr->{$_} if $attr->{$_} eq '';
1593 $item->assign_attributes(%$attr);
1595 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1596 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1598 $item->unit($part->unit) if !$item->unit;
1601 if ( $part->is_assortment ) {
1602 # add assortment items with price 0, as the components carry the price
1603 $price_src = $price_source->price_from_source("");
1604 $price_src->price(0);
1605 } elsif (defined $item->sellprice) {
1606 $price_src = $price_source->price_from_source("");
1607 $price_src->price($item->sellprice);
1609 $price_src = $price_source->best_price
1610 ? $price_source->best_price
1611 : $price_source->price_from_source("");
1612 $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
1613 $price_src->price(0) if !$price_source->best_price;
1617 if (defined $item->discount) {
1618 $discount_src = $price_source->discount_from_source("");
1619 $discount_src->discount($item->discount);
1621 $discount_src = $price_source->best_discount
1622 ? $price_source->best_discount
1623 : $price_source->discount_from_source("");
1624 $discount_src->discount(0) if !$price_source->best_discount;
1628 $new_attr{part} = $part;
1629 $new_attr{description} = $part->description if ! $item->description;
1630 $new_attr{qty} = 1.0 if ! $item->qty;
1631 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1632 $new_attr{sellprice} = $price_src->price;
1633 $new_attr{discount} = $discount_src->discount;
1634 $new_attr{active_price_source} = $price_src;
1635 $new_attr{active_discount_source} = $discount_src;
1636 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1637 $new_attr{project_id} = $record->globalproject_id;
1638 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1640 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1641 # they cannot be retrieved via custom_variables until the order/orderitem is
1642 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1643 $new_attr{custom_variables} = [];
1645 my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
1647 $item->assign_attributes(%new_attr, %{ $texts });
1652 sub setup_order_from_cv {
1655 $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
1657 $order->intnotes($order->customervendor->notes);
1659 return if !$order->is_sales;
1661 $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
1662 $order->taxincluded(defined($order->customer->taxincluded_checked)
1663 ? $order->customer->taxincluded_checked
1664 : $::myconfig{taxincluded_checked});
1666 my $address = $order->customer->default_billing_address;;
1667 $order->billing_address_id($address ? $address->id : undef);
1670 # setup custom shipto from form
1672 # The dialog returns form variables starting with 'shipto' and cvars starting
1673 # with 'shiptocvar_'.
1674 # Mark it to be deleted if a shipto from master data is selected
1675 # (i.e. order has a shipto).
1676 # Else, update or create a new custom shipto. If the fields are empty, it
1677 # will not be saved on save.
1678 sub setup_custom_shipto_from_form {
1679 my ($self, $order, $form) = @_;
1681 if ($order->shipto) {
1682 $self->is_custom_shipto_to_delete(1);
1684 my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1686 my $shipto_cvars = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
1687 my $shipto_attrs = {map { $_ => delete $form->{$_}} grep { m{^shipto} } keys %$form};
1689 $custom_shipto->assign_attributes(%$shipto_attrs);
1690 $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
1694 # recalculate prices and taxes
1696 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1700 my %pat = $self->order->calculate_prices_and_taxes();
1702 $self->{taxes} = [];
1703 foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
1704 my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
1706 push(@{ $self->{taxes} }, { amount => $pat{taxes_by_tax_id}->{$tax_id},
1707 netamount => $netamount,
1708 tax => SL::DB::Tax->new(id => $tax_id)->load });
1710 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
1713 # get data for saving, printing, ..., that is not changed in the form
1715 # Only cvars for now.
1716 sub get_unalterable_data {
1719 foreach my $item (@{ $self->order->items }) {
1720 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1721 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1722 foreach my $var (@{ $item->cvars_by_config }) {
1723 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1725 $item->parse_custom_variable_values;
1731 # And remove related files in the spool directory
1736 my $db = $self->order->db;
1738 $db->with_transaction(
1740 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1741 $self->order->delete;
1742 my $spool = $::lx_office_conf{paths}->{spool};
1743 unlink map { "$spool/$_" } @spoolfiles if $spool;
1745 $self->save_history('DELETED');
1748 }) || push(@{$errors}, $db->error);
1755 # And delete items that are deleted in the form.
1760 my $db = $self->order->db;
1762 $db->with_transaction(sub {
1763 # delete custom shipto if it is to be deleted or if it is empty
1764 if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
1765 $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
1766 $self->order->custom_shipto(undef);
1769 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
1770 $self->order->save(cascade => 1);
1773 if ($::form->{converted_from_oe_id}) {
1774 my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
1776 foreach my $converted_from_oe_id (@converted_from_oe_ids) {
1777 my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
1778 $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
1779 $src->link_to_record($self->order);
1781 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1783 foreach (@{ $self->order->items_sorted }) {
1784 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1786 SL::DB::RecordLink->new(from_table => 'orderitems',
1787 from_id => $from_id,
1788 to_table => 'orderitems',
1795 $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
1798 $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
1800 $self->save_history('SAVED');
1803 }) || push(@{$errors}, $db->error);
1808 sub workflow_sales_or_request_for_quotation {
1812 my $errors = $self->save();
1814 if (scalar @{ $errors }) {
1815 $self->js->flash('error', $_) for @{ $errors };
1816 return $self->js->render();
1819 my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
1821 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1822 $self->{converted_from_oe_id} = delete $::form->{id};
1824 # set item ids to new fake id, to identify them as new items
1825 foreach my $item (@{$self->order->items_sorted}) {
1826 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1830 $::form->{type} = $destination_type;
1831 $self->type($self->init_type);
1832 $self->cv ($self->init_cv);
1836 $self->get_unalterable_data();
1837 $self->pre_render();
1839 # trigger rendering values for second row as hidden, because they
1840 # are loaded only on demand. So we need to keep the values from the
1842 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1846 title => $self->get_title_for('edit'),
1847 %{$self->{template_args}}
1851 sub workflow_sales_or_purchase_order {
1855 my $errors = $self->save();
1857 if (scalar @{ $errors }) {
1858 $self->js->flash('error', $_) foreach @{ $errors };
1859 return $self->js->render();
1862 my $destination_type = $::form->{type} eq sales_quotation_type() ? sales_order_type()
1863 : $::form->{type} eq request_quotation_type() ? purchase_order_type()
1864 : $::form->{type} eq purchase_order_type() ? sales_order_type()
1865 : $::form->{type} eq sales_order_type() ? purchase_order_type()
1868 # check for direct delivery
1869 # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
1871 if ( $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
1872 && $::form->{use_shipto} && $self->order->shipto) {
1873 $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
1876 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1877 $self->{converted_from_oe_id} = delete $::form->{id};
1879 # set item ids to new fake id, to identify them as new items
1880 foreach my $item (@{$self->order->items_sorted}) {
1881 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1884 if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
1885 if ($::form->{use_shipto}) {
1886 $self->order->custom_shipto($custom_shipto) if $custom_shipto;
1888 # remove any custom shipto if not wanted
1889 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1894 $::form->{type} = $destination_type;
1895 $self->type($self->init_type);
1896 $self->cv ($self->init_cv);
1900 $self->get_unalterable_data();
1901 $self->pre_render();
1903 # trigger rendering values for second row as hidden, because they
1904 # are loaded only on demand. So we need to keep the values from the
1906 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1910 title => $self->get_title_for('edit'),
1911 %{$self->{template_args}}
1919 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1920 $self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
1921 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1922 $self->{all_languages} = SL::DB::Manager::Language->get_all_sorted();
1923 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1926 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1929 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1931 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1932 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1933 $self->{periodic_invoices_status} = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
1934 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1935 $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
1937 my $print_form = Form->new('');
1938 $print_form->{type} = $self->type;
1939 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1940 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1941 form => $print_form,
1942 options => {dialog_name_prefix => 'print_options.',
1946 no_opendocument => 0,
1950 foreach my $item (@{$self->order->orderitems}) {
1951 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1952 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1953 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1956 if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
1957 # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
1958 # Do not use write_to_objects to prevent order->delivered to be set, because this should be
1959 # the value from db, which can be set manually or is set when linked delivery orders are saved.
1960 SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
1963 if ($self->order->number && $::instance_conf->get_webdav) {
1964 my $webdav = SL::Webdav->new(
1965 type => $self->type,
1966 number => $self->order->number,
1968 my @all_objects = $webdav->get_all_objects;
1969 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1971 link => File::Spec->catfile($_->full_filedescriptor),
1975 if ( (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
1976 && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
1977 $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
1980 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1982 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
1983 edit_periodic_invoices_config calculate_qty follow_up show_history);
1984 $self->setup_edit_action_bar;
1987 sub setup_edit_action_bar {
1988 my ($self, %params) = @_;
1990 my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
1991 || (($self->type eq sales_order_type()) && $::instance_conf->get_sales_order_show_delete)
1992 || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
1994 my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
1995 my @req_cusordnumber = qw(kivi.Order.check_cusordnumber_presence) x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
1997 my $has_invoice_for_advance_payment;
1998 if ($self->order->id && $self->type eq sales_order_type()) {
1999 my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
2000 $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
2003 my $has_final_invoice;
2004 if ($self->order->id && $self->type eq sales_order_type()) {
2005 my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
2006 $has_final_invoice = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
2009 for my $bar ($::request->layout->get('actionbar')) {
2014 call => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
2015 $::instance_conf->get_order_warn_no_deliverydate,
2017 checks => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
2018 @req_trans_cost_art, @req_cusordnumber,
2023 call => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
2024 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2025 @req_trans_cost_art, @req_cusordnumber,
2027 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2029 ], # end of combobox "Save"
2036 t8('Save and Quotation'),
2037 submit => [ '#order_form', { action => "Order/sales_quotation" } ],
2038 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2039 only_if => (any { $self->type eq $_ } (sales_order_type())),
2043 submit => [ '#order_form', { action => "Order/request_for_quotation" } ],
2044 only_if => (any { $self->type eq $_ } (purchase_order_type())),
2047 t8('Save and Sales Order'),
2048 submit => [ '#order_form', { action => "Order/sales_order" } ],
2049 checks => [ @req_trans_cost_art ],
2050 only_if => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
2053 t8('Save and Purchase Order'),
2054 call => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
2055 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2056 only_if => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
2059 t8('Save and Delivery Order'),
2060 call => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
2061 $::instance_conf->get_order_warn_no_deliverydate,
2063 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2064 @req_trans_cost_art, @req_cusordnumber,
2066 only_if => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
2069 t8('Save and Invoice'),
2070 call => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
2071 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2072 @req_trans_cost_art, @req_cusordnumber,
2076 ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
2077 call => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::instance_conf->get_order_warn_duplicate_parts ],
2078 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2079 @req_trans_cost_art, @req_cusordnumber,
2081 disabled => $has_final_invoice ? t8('This order has already a final invoice.')
2083 only_if => (any { $self->type eq $_ } (sales_order_type())),
2086 t8('Save and Final Invoice'),
2087 call => [ 'kivi.Order.save', 'save_and_final_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
2088 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2089 @req_trans_cost_art, @req_cusordnumber,
2091 disabled => $has_final_invoice ? t8('This order has already a final invoice.')
2093 only_if => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
2096 t8('Save and AP Transaction'),
2097 call => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
2098 only_if => (any { $self->type eq $_ } (purchase_order_type()))
2101 ], # end of combobox "Workflow"
2108 t8('Save and preview PDF'),
2109 call => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
2110 $::instance_conf->get_order_warn_no_deliverydate,
2112 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2115 t8('Save and print'),
2116 call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
2117 $::instance_conf->get_order_warn_no_deliverydate,
2119 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2122 t8('Save and E-mail'),
2123 id => 'save_and_email_action',
2124 call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
2125 $::instance_conf->get_order_warn_no_deliverydate,
2127 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2130 t8('Download attachments of all parts'),
2131 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
2132 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2133 only_if => $::instance_conf->get_doc_storage,
2135 ], # end of combobox "Export"
2139 call => [ 'kivi.Order.delete_order' ],
2140 confirm => $::locale->text('Do you really want to delete this object?'),
2141 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2142 only_if => $deletion_allowed,
2151 call => [ 'set_history_window', $self->order->id, 'id' ],
2152 disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
2156 call => [ 'kivi.Order.follow_up_window' ],
2157 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2158 only_if => $::auth->assert('productivity', 1),
2160 ], # end of combobox "more"
2166 my ($self, $doc_ref, $params) = @_;
2168 my $order = $self->order;
2171 my $print_form = Form->new('');
2172 $print_form->{type} = $order->type;
2173 $print_form->{formname} = $params->{formname} || $order->type;
2174 $print_form->{format} = $params->{format} || 'pdf';
2175 $print_form->{media} = $params->{media} || 'file';
2176 $print_form->{groupitems} = $params->{groupitems};
2177 $print_form->{printer_id} = $params->{printer_id};
2178 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
2180 $order->language($params->{language});
2181 $order->flatten_to_form($print_form, format_amounts => 1);
2185 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
2186 $template_ext = 'odt';
2187 $template_type = 'OpenDocument';
2188 } elsif ($print_form->{format} =~ m{html}i) {
2189 $template_ext = 'html';
2190 $template_type = 'HTML';
2193 # search for the template
2194 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
2195 name => $print_form->{formname},
2196 extension => $template_ext,
2197 email => $print_form->{media} eq 'email',
2198 language => $params->{language},
2199 printer_id => $print_form->{printer_id},
2202 if (!defined $template_file) {
2203 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);
2206 return @errors if scalar @errors;
2208 $print_form->throw_on_error(sub {
2210 $print_form->prepare_for_printing;
2212 $$doc_ref = SL::Helper::CreatePDF->create_pdf(
2213 format => $print_form->{format},
2214 template_type => $template_type,
2215 template => $template_file,
2216 variables => $print_form,
2217 variable_content_types => {
2218 longdescription => 'html',
2219 partnotes => 'html',
2221 $::form->get_variable_content_types_for_cvars,
2225 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
2231 sub get_files_for_email_dialog {
2234 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
2236 return %files if !$::instance_conf->get_doc_storage;
2238 if ($self->order->id) {
2239 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
2240 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
2241 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
2242 $files{project_files} = [ SL::File->get_all( object_id => $self->order->globalproject_id, object_type => 'project', file_type => 'attachment') ];
2246 uniq_by { $_->{id} }
2248 +{ id => $_->part->id,
2249 partnumber => $_->part->partnumber }
2250 } @{$self->order->items_sorted};
2252 foreach my $part (@parts) {
2253 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
2254 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
2257 foreach my $key (keys %files) {
2258 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
2264 sub make_periodic_invoices_config_from_yaml {
2265 my ($yaml_config) = @_;
2267 return if !$yaml_config;
2268 my $attr = SL::YAML::Load($yaml_config);
2269 return if 'HASH' ne ref $attr;
2270 return SL::DB::PeriodicInvoicesConfig->new(%$attr);
2274 sub get_periodic_invoices_status {
2275 my ($self, $config) = @_;
2277 return if $self->type ne sales_order_type();
2278 return t8('not configured') if !$config;
2280 my $active = ('HASH' eq ref $config) ? $config->{active}
2281 : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
2282 : die "Cannot get status of periodic invoices config";
2284 return $active ? t8('active') : t8('inactive');
2288 my ($self, $action) = @_;
2290 return '' if none { lc($action)} qw(add edit);
2293 # $::locale->text("Add Sales Order");
2294 # $::locale->text("Add Purchase Order");
2295 # $::locale->text("Add Quotation");
2296 # $::locale->text("Add Request for Quotation");
2297 # $::locale->text("Edit Sales Order");
2298 # $::locale->text("Edit Purchase Order");
2299 # $::locale->text("Edit Quotation");
2300 # $::locale->text("Edit Request for Quotation");
2302 $action = ucfirst(lc($action));
2303 return $self->type eq sales_order_type() ? $::locale->text("$action Sales Order")
2304 : $self->type eq purchase_order_type() ? $::locale->text("$action Purchase Order")
2305 : $self->type eq sales_quotation_type() ? $::locale->text("$action Quotation")
2306 : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
2310 sub get_item_cvpartnumber {
2311 my ($self, $item) = @_;
2313 return if !$self->search_cvpartnumber;
2314 return if !$self->order->customervendor;
2316 if ($self->cv eq 'vendor') {
2317 my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
2318 $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
2319 } elsif ($self->cv eq 'customer') {
2320 my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
2321 $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
2325 sub get_part_texts {
2326 my ($part_or_id, $language_or_id, %defaults) = @_;
2328 my $part = ref($part_or_id) ? $part_or_id : SL::DB::Part->load_cached($part_or_id);
2329 my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
2331 description => $defaults{description} // $part->description,
2332 longdescription => $defaults{longdescription} // $part->notes,
2335 return $texts unless $language_id;
2337 my $translation = SL::DB::Manager::Translation->get_first(
2339 parts_id => $part->id,
2340 language_id => $language_id,
2343 $texts->{description} = $translation->translation if $translation && $translation->translation;
2344 $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
2349 sub sales_order_type {
2353 sub purchase_order_type {
2357 sub sales_quotation_type {
2361 sub request_quotation_type {
2362 'request_quotation';
2366 return $_[0]->type eq sales_order_type() ? 'ordnumber'
2367 : $_[0]->type eq purchase_order_type() ? 'ordnumber'
2368 : $_[0]->type eq sales_quotation_type() ? 'quonumber'
2369 : $_[0]->type eq request_quotation_type() ? 'quonumber'
2373 sub save_and_redirect_to {
2374 my ($self, %params) = @_;
2376 my $errors = $self->save();
2378 if (scalar @{ $errors }) {
2379 $self->js->flash('error', $_) foreach @{ $errors };
2380 return $self->js->render();
2383 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
2384 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
2385 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
2386 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
2388 flash_later('info', $text);
2390 $self->redirect_to(%params, id => $self->order->id);
2394 my ($self, $addition) = @_;
2396 my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
2397 my $snumbers = $number_type . '_' . $self->order->$number_type;
2399 SL::DB::History->new(
2400 trans_id => $self->order->id,
2401 employee_id => SL::DB::Manager::Employee->current->id,
2402 what_done => $self->order->type,
2403 snumbers => $snumbers,
2404 addition => $addition,
2408 sub store_doc_to_webdav_and_filemanagement {
2409 my ($self, $content, $filename, $variant) = @_;
2411 my $order = $self->order;
2414 # copy file to webdav folder
2415 if ($order->number && $::instance_conf->get_webdav_documents) {
2416 my $webdav = SL::Webdav->new(
2417 type => $order->type,
2418 number => $order->number,
2420 my $webdav_file = SL::Webdav::File->new(
2422 filename => $filename,
2425 $webdav_file->store(data => \$content);
2428 push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
2431 if ($order->id && $::instance_conf->get_doc_storage) {
2433 SL::File->save(object_id => $order->id,
2434 object_type => $order->type,
2435 mime_type => SL::MIME->mime_type_from_ext($filename),
2436 source => 'created',
2437 file_type => 'document',
2438 file_name => $filename,
2439 file_contents => $content,
2440 print_variant => $variant);
2443 push @errors, t8('Storing the document in the storage backend failed: #1', $@);
2450 sub link_requirement_specs_linking_to_created_from_objects {
2451 my ($self, @converted_from_oe_ids) = @_;
2453 return unless @converted_from_oe_ids;
2455 my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
2456 foreach my $rs_order (@{ $rs_orders }) {
2457 SL::DB::RequirementSpecOrder->new(
2458 order_id => $self->order->id,
2459 requirement_spec_id => $rs_order->requirement_spec_id,
2460 version_id => $rs_order->version_id,
2465 sub set_project_in_linked_requirement_specs {
2468 my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
2469 foreach my $rs_order (@{ $rs_orders }) {
2470 next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
2472 $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
2484 SL::Controller::Order - controller for orders
2488 This is a new form to enter orders, completely rewritten with the use
2489 of controller and java script techniques.
2491 The aim is to provide the user a better experience and a faster workflow. Also
2492 the code should be more readable, more reliable and better to maintain.
2500 One input row, so that input happens every time at the same place.
2504 Use of pickers where possible.
2508 Possibility to enter more than one item at once.
2512 Item list in a scrollable area, so that the workflow buttons stay at
2517 Reordering item rows with drag and drop is possible. Sorting item rows is
2518 possible (by partnumber, description, qty, sellprice and discount for now).
2522 No C<update> is necessary. All entries and calculations are managed
2523 with ajax-calls and the page only reloads on C<save>.
2527 User can see changes immediately, because of the use of java script
2538 =item * C<SL/Controller/Order.pm>
2542 =item * C<template/webpages/order/form.html>
2546 =item * C<template/webpages/order/tabs/basic_data.html>
2548 Main tab for basic_data.
2550 This is the only tab here for now. "linked records" and "webdav" tabs are
2551 reused from generic code.
2555 =item * C<template/webpages/order/tabs/_business_info_row.html>
2557 For displaying information on business type
2559 =item * C<template/webpages/order/tabs/_item_input.html>
2561 The input line for items
2563 =item * C<template/webpages/order/tabs/_row.html>
2565 One row for already entered items
2567 =item * C<template/webpages/order/tabs/_tax_row.html>
2569 Displaying tax information
2571 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
2573 Dialog for selecting price and discount sources
2577 =item * C<js/kivi.Order.js>
2579 java script functions
2589 =item * price sources: little symbols showing better price / better discount
2591 =item * select units in input row?
2593 =item * check for direct delivery (workflow sales order -> purchase order)
2595 =item * access rights
2597 =item * display weights
2601 =item * optional client/user behaviour
2603 (transactions has to be set - department has to be set -
2604 force project if enabled in client config)
2608 =head1 KNOWN BUGS AND CAVEATS
2614 Customer discount is not displayed as a valid discount in price source popup
2615 (this might be a bug in price sources)
2617 (I cannot reproduce this (Bernd))
2621 No indication that <shift>-up/down expands/collapses second row.
2625 Inline creation of parts is not currently supported
2629 Table header is not sticky in the scrolling area.
2633 Sorting does not include C<position>, neither does reordering.
2635 This behavior was implemented intentionally. But we can discuss, which behavior
2636 should be implemented.
2640 =head1 To discuss / Nice to have
2646 How to expand/collapse second row. Now it can be done clicking the icon or
2651 Possibility to select PriceSources in input row?
2655 This controller uses a (changed) copy of the template for the PriceSource
2656 dialog. Maybe there could be used one code source.
2660 Rounding-differences between this controller (PriceTaxCalculator) and the old
2661 form. This is not only a problem here, but also in all parts using the PTC.
2662 There exists a ticket and a patch. This patch should be testet.
2666 An indicator, if the actual inputs are saved (like in an
2667 editor or on text processing application).
2671 A warning when leaving the page without saveing unchanged inputs.
2678 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>