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>