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;
 
  29 use SL::DB::RecordLink;
 
  30 use SL::DB::RequirementSpec;
 
  32 use SL::DB::Translation;
 
  34 use SL::Helper::CreatePDF qw(:all);
 
  35 use SL::Helper::PrintOptions;
 
  36 use SL::Helper::ShippedQty;
 
  37 use SL::Helper::UserPreferences::DisplayPreferences;
 
  38 use SL::Helper::UserPreferences::PositionsScrollbar;
 
  39 use SL::Helper::UserPreferences::UpdatePositions;
 
  41 use SL::Controller::Helper::GetModels;
 
  43 use List::Util qw(first sum0);
 
  44 use List::UtilsBy qw(sort_by uniq_by);
 
  45 use List::MoreUtils qw(any none pairwise first_index);
 
  46 use English qw(-no_match_vars);
 
  51 use Rose::Object::MakeMethods::Generic
 
  53  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 
  54  'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
 
  59 __PACKAGE__->run_before('check_auth');
 
  61 __PACKAGE__->run_before('check_auth_for_edit',
 
  62                         except => [ qw(edit show_customer_vendor_details_dialog price_popup load_second_rows) ]);
 
  64 __PACKAGE__->run_before('recalc',
 
  65                         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
 
  68 __PACKAGE__->run_before('get_unalterable_data',
 
  69                         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
 
  80   $self->order->transdate(DateTime->now_local());
 
  81   my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
  82                    $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
  84   if (   ($self->type eq sales_order_type()     &&  $::instance_conf->get_deliverydate_on)
 
  85       || ($self->type eq sales_quotation_type() &&  $::instance_conf->get_reqdate_on)
 
  86       && (!$self->order->reqdate)) {
 
  87     $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
 
  94     title => $self->get_title_for('add'),
 
  95     %{$self->{template_args}}
 
  99 # edit an existing order
 
 107     # this is to edit an order from an unsaved order object
 
 109     # set item ids to new fake id, to identify them as new items
 
 110     foreach my $item (@{$self->order->items_sorted}) {
 
 111       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 113     # trigger rendering values for second row as hidden, because they
 
 114     # are loaded only on demand. So we need to keep the values from
 
 116     $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
 123     title => $self->get_title_for('edit'),
 
 124     %{$self->{template_args}}
 
 128 # edit a collective order (consisting of one or more existing orders)
 
 129 sub action_edit_collective {
 
 133   my @multi_ids = map {
 
 134     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 135   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 137   # fall back to add if no ids are given
 
 138   if (scalar @multi_ids == 0) {
 
 143   # fall back to save as new if only one id is given
 
 144   if (scalar @multi_ids == 1) {
 
 145     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 146     $self->action_save_as_new();
 
 150   # make new order from given orders
 
 151   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 152   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 153   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 155   $self->action_edit();
 
 162   my $errors = $self->delete();
 
 164   if (scalar @{ $errors }) {
 
 165     $self->js->flash('error', $_) foreach @{ $errors };
 
 166     return $self->js->render();
 
 169   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 170            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 171            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 172            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 174   flash_later('info', $text);
 
 176   my @redirect_params = (
 
 181   $self->redirect_to(@redirect_params);
 
 188   my $errors = $self->save();
 
 190   if (scalar @{ $errors }) {
 
 191     $self->js->flash('error', $_) foreach @{ $errors };
 
 192     return $self->js->render();
 
 195   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 196            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 197            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 198            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 200   flash_later('info', $text);
 
 203   if ($::form->{back_to_caller}) {
 
 204     @redirect_params = $::form->{callback} ? ($::form->{callback})
 
 205                                            : (controller => 'LoginScreen', action => 'user_login');
 
 211       id       => $self->order->id,
 
 212       callback => $::form->{callback},
 
 216   $self->redirect_to(@redirect_params);
 
 219 # save the order as new document an open it for edit
 
 220 sub action_save_as_new {
 
 223   my $order = $self->order;
 
 226     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 227     return $self->js->render();
 
 230   # load order from db to check if values changed
 
 231   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 234   # Lets assign a new number if the user hasn't changed the previous one.
 
 235   # If it has been changed manually then use it as-is.
 
 236   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 238                         : trim($order->number);
 
 240   # Clear transdate unless changed
 
 241   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 242                         ? DateTime->today_local
 
 245   # Set new reqdate unless changed if it is enabled in client config
 
 246   if ($order->reqdate == $saved_order->reqdate) {
 
 247     my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
 248                      $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
 250     if (   ($self->type eq sales_order_type()     &&  !$::instance_conf->get_deliverydate_on)
 
 251         || ($self->type eq sales_quotation_type() &&  !$::instance_conf->get_reqdate_on)) {
 
 252       $new_attrs{reqdate} = '';
 
 254       $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 257     $new_attrs{reqdate} = $order->reqdate;
 
 261   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 263   # Warn on obsolete items
 
 264   my @obsolete_positions = map { $_->position } grep { $_->part->obsolete } @{ $order->items_sorted };
 
 265   flash_later('warning', t8('This record containts obsolete items at position #1', join ', ', @obsolete_positions)) if @obsolete_positions;
 
 267   # Create new record from current one
 
 268   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 270   # no linked records on save as new
 
 271   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 274   $self->action_save();
 
 279 # This is called if "print" is pressed in the print dialog.
 
 280 # If PDF creation was requested and succeeded, the pdf is offered for download
 
 281 # via send_file (which uses ajax in this case).
 
 285   my $errors = $self->save();
 
 287   if (scalar @{ $errors }) {
 
 288     $self->js->flash('error', $_) foreach @{ $errors };
 
 289     return $self->js->render();
 
 292   $self->js_reset_order_and_item_ids_after_save;
 
 294   my $redirect_url = $self->url_for(
 
 297     id     => $self->order->id,
 
 300   my $format      = $::form->{print_options}->{format};
 
 301   my $media       = $::form->{print_options}->{media};
 
 302   my $formname    = $::form->{print_options}->{formname};
 
 303   my $copies      = $::form->{print_options}->{copies};
 
 304   my $groupitems  = $::form->{print_options}->{groupitems};
 
 305   my $printer_id  = $::form->{print_options}->{printer_id};
 
 307   # only PDF, OpenDocument & HTML for now
 
 308   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
 
 309     flash_later('error', t8('Format \'#1\' is not supported yet/anymore.', $format));
 
 310     return $self->js->redirect_to($redirect_url)->render;
 
 313   # only screen or printer by now
 
 314   if (none { $media eq $_ } qw(screen printer)) {
 
 315     flash_later('error', t8('Media \'#1\' is not supported yet/anymore.', $media));
 
 316     return $self->js->redirect_to($redirect_url)->render;
 
 319   # create a form for generate_attachment_filename
 
 320   my $form   = Form->new;
 
 321   $form->{$self->nr_key()}  = $self->order->number;
 
 322   $form->{type}             = $self->type;
 
 323   $form->{format}           = $format;
 
 324   $form->{formname}         = $formname;
 
 325   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 326   my $doc_filename          = $form->generate_attachment_filename();
 
 329   my @errors = $self->generate_doc(\$doc, { media      => $media,
 
 331                                             formname   => $formname,
 
 332                                             language   => $self->order->language,
 
 333                                             printer_id => $printer_id,
 
 334                                             groupitems => $groupitems });
 
 335   if (scalar @errors) {
 
 336     flash_later('error', t8('Generating the document failed: #1', $errors[0]));
 
 337     return $self->js->redirect_to($redirect_url)->render;
 
 340   if ($media eq 'screen') {
 
 342     flash_later('info', t8('The document has been created.'));
 
 345       type         => SL::MIME->mime_type_from_ext($doc_filename),
 
 346       name         => $doc_filename,
 
 350   } elsif ($media eq 'printer') {
 
 352     my $printer_id = $::form->{print_options}->{printer_id};
 
 353     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 358     flash_later('info', t8('The document has been printed.'));
 
 361   my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
 
 362   if (scalar @warnings) {
 
 363     flash_later('warning', $_) for @warnings;
 
 366   $self->save_history('PRINTED');
 
 368   $self->js->redirect_to($redirect_url)->render;
 
 371 sub action_preview_pdf {
 
 374   my $errors = $self->save();
 
 375   if (scalar @{ $errors }) {
 
 376     $self->js->flash('error', $_) foreach @{ $errors };
 
 377     return $self->js->render();
 
 380   $self->js_reset_order_and_item_ids_after_save;
 
 382   my $redirect_url = $self->url_for(
 
 385     id     => $self->order->id,
 
 389   my $media       = 'screen';
 
 390   my $formname    = $self->type;
 
 393   # create a form for generate_attachment_filename
 
 394   my $form   = Form->new;
 
 395   $form->{$self->nr_key()}  = $self->order->number;
 
 396   $form->{type}             = $self->type;
 
 397   $form->{format}           = $format;
 
 398   $form->{formname}         = $formname;
 
 399   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 400   my $pdf_filename          = $form->generate_attachment_filename();
 
 403   my @errors = $self->generate_doc(\$pdf, { media      => $media,
 
 405                                             formname   => $formname,
 
 406                                             language   => $self->order->language,
 
 408   if (scalar @errors) {
 
 409     flash_later('error', t8('Conversion to PDF failed: #1', $errors[0]));
 
 410     return $self->js->redirect_to($redirect_url)->render;
 
 413   $self->save_history('PREVIEWED');
 
 415   flash_later('info', t8('The PDF has been previewed'));
 
 420     type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 421     name         => $pdf_filename,
 
 425   $self->js->redirect_to($redirect_url)->render;
 
 428 # open the email dialog
 
 429 sub action_save_and_show_email_dialog {
 
 432   my $errors = $self->save();
 
 434   if (scalar @{ $errors }) {
 
 435     $self->js->flash('error', $_) foreach @{ $errors };
 
 436     return $self->js->render();
 
 439   $self->js_reset_order_and_item_ids_after_save;
 
 441   my $cv_method = $self->cv;
 
 443   if (!$self->order->$cv_method) {
 
 444     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'))
 
 449   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 450   $email_form->{to} ||= $self->order->$cv_method->email;
 
 451   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 452   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 453   # Todo: get addresses from shipto, if any
 
 455   my $form = Form->new;
 
 456   $form->{$self->nr_key()}  = $self->order->number;
 
 457   $form->{cusordnumber}     = $self->order->cusordnumber;
 
 458   $form->{formname}         = $self->type;
 
 459   $form->{type}             = $self->type;
 
 460   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 461   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
 
 462   $form->{format}           = 'pdf';
 
 463   $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
 465   $email_form->{subject}             = $form->generate_email_subject();
 
 466   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 467   $email_form->{message}             = $form->generate_email_body();
 
 468   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 470   my %files = $self->get_files_for_email_dialog();
 
 472   my @employees_with_email = grep {
 
 473     my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
 
 474     $user && !!trim($user->get_config_value('email'));
 
 475   } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
 
 478   my $all_partner_email_addresses = $self->order->customervendor->get_all_email_addresses();
 
 480   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 481                                   email_form    => $email_form,
 
 482                                   show_bcc      => $::auth->assert('email_bcc', 'may fail'),
 
 484                                   is_customer   => $self->cv eq 'customer',
 
 485                                   ALL_EMPLOYEES => \@employees_with_email,
 
 486                                   ALL_PARTNER_EMAIL_ADDRESSES => $all_partner_email_addresses,
 
 490       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 497 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 498 sub action_send_email {
 
 501   my $errors = $self->save();
 
 503   if (scalar @{ $errors }) {
 
 504     $self->js->run('kivi.Order.close_email_dialog');
 
 505     $self->js->flash('error', $_) foreach @{ $errors };
 
 506     return $self->js->render();
 
 509   $self->js_reset_order_and_item_ids_after_save;
 
 511   my $email_form  = delete $::form->{email_form};
 
 513   if ($email_form->{additional_to}) {
 
 514     $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
 
 515     delete $email_form->{additional_to};
 
 518   my %field_names = (to => 'email');
 
 520   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 522   # for Form::cleanup which may be called in Form::send_email
 
 523   $::form->{cwd}    = getcwd();
 
 524   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 526   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 527   $::form->{media}  = 'email';
 
 529   $::form->{attachment_policy} //= '';
 
 531   # Is an old file version available?
 
 533   if ($::form->{attachment_policy} eq 'old_file') {
 
 534     $attfile = SL::File->get_all(object_id     => $self->order->id,
 
 535                                  object_type   => $self->type,
 
 536                                  file_type     => 'document',
 
 537                                  print_variant => $::form->{formname});
 
 540   if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
 
 542     my @errors = $self->generate_doc(\$doc, {media      => $::form->{media},
 
 543                                              format     => $::form->{print_options}->{format},
 
 544                                              formname   => $::form->{print_options}->{formname},
 
 545                                              language   => $self->order->language,
 
 546                                              printer_id => $::form->{print_options}->{printer_id},
 
 547                                              groupitems => $::form->{print_options}->{groupitems}});
 
 548     if (scalar @errors) {
 
 549       return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
 
 552     my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename}, $::form->{formname});
 
 553     if (scalar @warnings) {
 
 554       flash_later('warning', $_) for @warnings;
 
 557     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 558     $sfile->fh->print($doc);
 
 561     $::form->{tmpfile} = $sfile->file_name;
 
 562     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 565   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 566   $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
 
 568   # internal notes unless no email journal
 
 569   unless ($::instance_conf->get_email_journal) {
 
 570     my $intnotes = $self->order->intnotes;
 
 571     $intnotes   .= "\n\n" if $self->order->intnotes;
 
 572     $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 573     $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 574     $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 575     $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 576     $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 577     $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 578     $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
 
 580     $self->order->update_attributes(intnotes => $intnotes);
 
 583   $self->save_history('MAILED');
 
 585   flash_later('info', t8('The email has been sent.'));
 
 587   my @redirect_params = (
 
 590     id     => $self->order->id,
 
 593   $self->redirect_to(@redirect_params);
 
 596 # open the periodic invoices config dialog
 
 598 # If there are values in the form (i.e. dialog was opened before),
 
 599 # then use this values. Create new ones, else.
 
 600 sub action_show_periodic_invoices_config_dialog {
 
 603   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 604   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 605   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 606                                                    order_value_periodicity => 'p', # = same as periodicity
 
 607                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 608                                                    extend_automatically_by => 12,
 
 610                                                    email_subject           => GenericTranslations->get(
 
 611                                                                                 language_id      => $::form->{language_id},
 
 612                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 613                                                    email_body              => GenericTranslations->get(
 
 614                                                                                 language_id      => $::form->{language_id},
 
 615                                                                                 translation_type => "salutation_general")
 
 616                                                                             . GenericTranslations->get(
 
 617                                                                                 language_id      => $::form->{language_id},
 
 618                                                                                 translation_type => "salutation_punctuation_mark") . "\n\n"
 
 619                                                                             . GenericTranslations->get(
 
 620                                                                                 language_id      => $::form->{language_id},
 
 621                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 623   # for older configs, replace email preset text if not yet set.
 
 624   $config->email_subject(GenericTranslations->get(
 
 625                                               language_id      => $::form->{language_id},
 
 626                                               translation_type =>"preset_text_periodic_invoices_email_subject")
 
 627                         ) unless $config->email_subject;
 
 629   $config->email_body(GenericTranslations->get(
 
 630                                               language_id      => $::form->{language_id},
 
 631                                               translation_type => "salutation_general")
 
 632                     . GenericTranslations->get(
 
 633                                               language_id      => $::form->{language_id},
 
 634                                               translation_type => "salutation_punctuation_mark") . "\n\n"
 
 635                     . GenericTranslations->get(
 
 636                                               language_id      => $::form->{language_id},
 
 637                                               translation_type =>"preset_text_periodic_invoices_email_body")
 
 638                      ) unless $config->email_body;
 
 640   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 641   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 643   $::form->get_lists(printers => "ALL_PRINTERS",
 
 644                      charts   => { key       => 'ALL_CHARTS',
 
 645                                    transdate => 'current_date' });
 
 647   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 649   if ($::form->{customer_id}) {
 
 650     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 651     my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
 
 652     $::form->{postal_invoice}                  = $customer_object->postal_invoice;
 
 653     $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
 
 654     $config->send_email(0) if $::form->{postal_invoice};
 
 657   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 659                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 660                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 665 # assign the values of the periodic invoices config dialog
 
 666 # as yaml in the hidden tag and set the status.
 
 667 sub action_assign_periodic_invoices_config {
 
 670   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 672   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 673                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 674                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 675                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 676                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 677                  start_date_as_date         => $::form->{start_date_as_date},
 
 678                  end_date_as_date           => $::form->{end_date_as_date},
 
 679                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 680                  print                      => $::form->{print}      ? 1                         : 0,
 
 681                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 682                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 683                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 684                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 685                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 686                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 687                  email_recipient_address    => $::form->{email_recipient_address},
 
 688                  email_sender               => $::form->{email_sender},
 
 689                  email_subject              => $::form->{email_subject},
 
 690                  email_body                 => $::form->{email_body},
 
 693   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 695   my $status = $self->get_periodic_invoices_status($config);
 
 698     ->remove('#order_periodic_invoices_config')
 
 699     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 700     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 701     ->html('#periodic_invoices_status', $status)
 
 702     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 706 sub action_get_has_active_periodic_invoices {
 
 709   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 710   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 712   my $has_active_periodic_invoices =
 
 713        $self->type eq sales_order_type()
 
 716     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 717     && $config->get_previous_billed_period_start_date;
 
 719   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 722 # save the order and redirect to the frontend subroutine for a new
 
 724 sub action_save_and_delivery_order {
 
 727   $self->save_and_redirect_to(
 
 728     controller => 'oe.pl',
 
 729     action     => 'oe_delivery_order_from_order',
 
 733 sub action_save_and_supplier_delivery_order {
 
 736   $self->save_and_redirect_to(
 
 737     controller => 'controller.pl',
 
 738     action     => 'DeliveryOrder/add_from_order',
 
 739     type       => 'supplier_delivery_order',
 
 743 # save the order and redirect to the frontend subroutine for a new
 
 745 sub action_save_and_invoice {
 
 748   $self->save_and_redirect_to(
 
 749     controller => 'oe.pl',
 
 750     action     => 'oe_invoice_from_order',
 
 754 sub action_save_and_invoice_for_advance_payment {
 
 757   $self->save_and_redirect_to(
 
 758     controller       => 'oe.pl',
 
 759     action           => 'oe_invoice_from_order',
 
 760     new_invoice_type => 'invoice_for_advance_payment',
 
 764 sub action_save_and_final_invoice {
 
 767   $self->save_and_redirect_to(
 
 768     controller       => 'oe.pl',
 
 769     action           => 'oe_invoice_from_order',
 
 770     new_invoice_type => 'final_invoice',
 
 774 # workflow from sales order to sales quotation
 
 775 sub action_sales_quotation {
 
 776   $_[0]->workflow_sales_or_request_for_quotation();
 
 779 # workflow from sales order to sales quotation
 
 780 sub action_request_for_quotation {
 
 781   $_[0]->workflow_sales_or_request_for_quotation();
 
 784 # workflow from sales quotation to sales order
 
 785 sub action_sales_order {
 
 786   $_[0]->workflow_sales_or_purchase_order();
 
 789 # workflow from rfq to purchase order
 
 790 sub action_purchase_order {
 
 791   $_[0]->workflow_sales_or_purchase_order();
 
 794 # workflow from purchase order to ap transaction
 
 795 sub action_save_and_ap_transaction {
 
 798   $self->save_and_redirect_to(
 
 799     controller => 'ap.pl',
 
 800     action     => 'add_from_purchase_order',
 
 804 # set form elements in respect to a changed customer or vendor
 
 806 # This action is called on an change of the customer/vendor picker.
 
 807 sub action_customer_vendor_changed {
 
 810   setup_order_from_cv($self->order);
 
 813   my $cv_method = $self->cv;
 
 815   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 816     $self->js->show('#cp_row');
 
 818     $self->js->hide('#cp_row');
 
 821   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 822     $self->js->show('#shipto_selection');
 
 824     $self->js->hide('#shipto_selection');
 
 827   if ($cv_method eq 'customer') {
 
 828     my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
 
 829     $self->js->$show_hide('#billing_address_row');
 
 832   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 835     ->replaceWith('#order_cp_id',              $self->build_contact_select)
 
 836     ->replaceWith('#order_shipto_id',          $self->build_shipto_select)
 
 837     ->replaceWith('#shipto_inputs  ',          $self->build_shipto_inputs)
 
 838     ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
 
 839     ->replaceWith('#business_info_row',        $self->build_business_info_row)
 
 840     ->val(        '#order_taxzone_id',         $self->order->taxzone_id)
 
 841     ->val(        '#order_taxincluded',        $self->order->taxincluded)
 
 842     ->val(        '#order_currency_id',        $self->order->currency_id)
 
 843     ->val(        '#order_payment_id',         $self->order->payment_id)
 
 844     ->val(        '#order_delivery_term_id',   $self->order->delivery_term_id)
 
 845     ->val(        '#order_intnotes',           $self->order->intnotes)
 
 846     ->val(        '#order_language_id',        $self->order->$cv_method->language_id)
 
 847     ->focus(      '#order_' . $self->cv . '_id')
 
 848     ->run('kivi.Order.update_exchangerate');
 
 850   $self->js_redisplay_amounts_and_taxes;
 
 851   $self->js_redisplay_cvpartnumbers;
 
 855 # open the dialog for customer/vendor details
 
 856 sub action_show_customer_vendor_details_dialog {
 
 859   my $is_customer = 'customer' eq $::form->{vc};
 
 862     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 864     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 867   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 868   $details{discount_as_percent} = $cv->discount_as_percent;
 
 869   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 870   $details{business}            = $cv->business->description      if $cv->business;
 
 871   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 872   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 873   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 874   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 877     foreach my $entry (@{ $cv->additional_billing_addresses }) {
 
 878       push @{ $details{ADDITIONAL_BILLING_ADDRESSES} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 881   foreach my $entry (@{ $cv->shipto }) {
 
 882     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 884   foreach my $entry (@{ $cv->contacts }) {
 
 885     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 888   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 889                 is_customer => $is_customer,
 
 894 # called if a unit in an existing item row is changed
 
 895 sub action_unit_changed {
 
 898   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 899   my $item = $self->order->items_sorted->[$idx];
 
 901   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 902   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 907     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 908   $self->js_redisplay_line_values;
 
 909   $self->js_redisplay_amounts_and_taxes;
 
 913 # update item input row when a part ist picked
 
 914 sub action_update_item_input_row {
 
 917   delete $::form->{add_item}->{$_} for qw(create_part_type sellprice_as_number discount_as_percent);
 
 919   my $form_attr = $::form->{add_item};
 
 921   return unless $form_attr->{parts_id};
 
 923   my $record       = $self->order;
 
 924   my $item         = SL::DB::OrderItem->new(%$form_attr);
 
 925   $item->unit($item->part->unit);
 
 927   my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
 
 930     ->val     ('#add_item_unit',                $item->unit)
 
 931     ->val     ('#add_item_description',         $item->part->description)
 
 932     ->val     ('#add_item_sellprice_as_number', '')
 
 933     ->attr    ('#add_item_sellprice_as_number', 'placeholder', $price_src->price_as_number)
 
 934     ->attr    ('#add_item_sellprice_as_number', 'title',       $price_src->source_description)
 
 935     ->val     ('#add_item_discount_as_percent', '')
 
 936     ->attr    ('#add_item_discount_as_percent', 'placeholder', $discount_src->discount_as_percent)
 
 937     ->attr    ('#add_item_discount_as_percent', 'title',       $discount_src->source_description)
 
 941 # add an item row for a new item entered in the input row
 
 942 sub action_add_item {
 
 945   delete $::form->{add_item}->{create_part_type};
 
 947   my $form_attr = $::form->{add_item};
 
 949   return unless $form_attr->{parts_id};
 
 951   my $item = new_item($self->order, $form_attr);
 
 953   $self->order->add_items($item);
 
 957   $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);
 
 974   if ( $item->part->is_assortment ) {
 
 975     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 976     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 977       my $attr = { parts_id => $assortment_item->parts_id,
 
 978                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 979                    unit     => $assortment_item->unit,
 
 980                    description => $assortment_item->part->description,
 
 982       my $item = new_item($self->order, $attr);
 
 984       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 985       $item->discount(1) unless $assortment_item->charge;
 
 987       $self->order->add_items( $item );
 
 989       $self->get_item_cvpartnumber($item);
 
 990       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 991       my $row_as_html = $self->p->render('order/tabs/_row',
 
 996       if ($::form->{insert_before_item_id}) {
 
 998           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
1001           ->append('#row_table_id', $row_as_html);
 
1007     ->val('.add_item_input', '')
 
1008     ->attr('.add_item_input', 'placeholder', '')
 
1009     ->attr('.add_item_input', 'title', '')
 
1010     ->run('kivi.Order.init_row_handlers')
 
1011     ->run('kivi.Order.renumber_positions')
 
1012     ->focus('#add_item_parts_id_name');
 
1014   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
1016   $self->js_redisplay_amounts_and_taxes;
 
1017   $self->js->render();
 
1020 # add item rows for multiple items at once
 
1021 sub action_add_multi_items {
 
1024   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
 
1025   return $self->js->render() unless scalar @form_attr;
 
1028   foreach my $attr (@form_attr) {
 
1029     my $item = new_item($self->order, $attr);
 
1031     if ( $item->part->is_assortment ) {
 
1032       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
1033         my $attr = { parts_id => $assortment_item->parts_id,
 
1034                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
1035                      unit     => $assortment_item->unit,
 
1036                      description => $assortment_item->part->description,
 
1038         my $item = new_item($self->order, $attr);
 
1040         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
1041         $item->discount(1) unless $assortment_item->charge;
 
1046   $self->order->add_items(@items);
 
1050   foreach my $item (@items) {
 
1051     $self->get_item_cvpartnumber($item);
 
1052     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1053     my $row_as_html = $self->p->render('order/tabs/_row',
 
1059     if ($::form->{insert_before_item_id}) {
 
1061         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
1064         ->append('#row_table_id', $row_as_html);
 
1069     ->run('kivi.Part.close_picker_dialogs')
 
1070     ->run('kivi.Order.init_row_handlers')
 
1071     ->run('kivi.Order.renumber_positions')
 
1072     ->focus('#add_item_parts_id_name');
 
1074   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
1076   $self->js_redisplay_amounts_and_taxes;
 
1077   $self->js->render();
 
1080 # recalculate all linetotals, amounts and taxes and redisplay them
 
1081 sub action_recalc_amounts_and_taxes {
 
1086   $self->js_redisplay_line_values;
 
1087   $self->js_redisplay_amounts_and_taxes;
 
1088   $self->js->render();
 
1091 sub action_update_exchangerate {
 
1095     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
1096     currency_name => $self->order->currency->name,
 
1097     exchangerate  => $self->order->daily_exchangerate_as_null_number,
 
1100   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
1103 # redisplay item rows if they are sorted by an attribute
 
1104 sub action_reorder_items {
 
1108     partnumber   => sub { $_[0]->part->partnumber },
 
1109     description  => sub { $_[0]->description },
 
1110     qty          => sub { $_[0]->qty },
 
1111     sellprice    => sub { $_[0]->sellprice },
 
1112     discount     => sub { $_[0]->discount },
 
1113     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
1116   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1118   my $method = $sort_keys{$::form->{order_by}};
 
1119   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
1120   if ($::form->{sort_dir}) {
 
1121     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1122       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
1124       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
1127     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1128       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
1130       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
1134     ->run('kivi.Order.redisplay_items', \@to_sort)
 
1138 # show the popup to choose a price/discount source
 
1139 sub action_price_popup {
 
1142   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
1143   my $item = $self->order->items_sorted->[$idx];
 
1145   $self->render_price_dialog($item);
 
1148 # save the order in a session variable and redirect to the part controller
 
1149 sub action_create_part {
 
1152   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
 
1154   my $callback     = $self->url_for(
 
1155     action       => 'return_from_create_part',
 
1156     type         => $self->type, # type is needed for check_auth on return
 
1157     previousform => $previousform,
 
1160   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.'));
 
1162   my @redirect_params = (
 
1163     controller    => 'Part',
 
1165     part_type     => $::form->{add_item}->{create_part_type},
 
1166     callback      => $callback,
 
1170   $self->redirect_to(@redirect_params);
 
1173 sub action_return_from_create_part {
 
1176   $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
 
1178   $::auth->restore_form_from_session(delete $::form->{previousform});
 
1180   # set item ids to new fake id, to identify them as new items
 
1181   foreach my $item (@{$self->order->items_sorted}) {
 
1182     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1186   $self->get_unalterable_data();
 
1187   $self->pre_render();
 
1189   # trigger rendering values for second row/longdescription as hidden,
 
1190   # because they are loaded only on demand. So we need to keep the values
 
1192   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1193   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1197     title => $self->get_title_for('edit'),
 
1198     %{$self->{template_args}}
 
1203 # load the second row for one or more items
 
1205 # This action gets the html code for all items second rows by rendering a template for
 
1206 # the second row and sets the html code via client js.
 
1207 sub action_load_second_rows {
 
1210   $self->recalc() if $self->order->is_sales; # for margin calculation
 
1212   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1213     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1214     my $item = $self->order->items_sorted->[$idx];
 
1216     $self->js_load_second_row($item, $item_id, 0);
 
1219   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
1221   $self->js->render();
 
1224 # update description, notes and sellprice from master data
 
1225 sub action_update_row_from_master_data {
 
1228   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1229     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1230     my $item  = $self->order->items_sorted->[$idx];
 
1231     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1233     $item->description($texts->{description});
 
1234     $item->longdescription($texts->{longdescription});
 
1236     my ($price_src, $discount_src) = get_best_price_and_discount_source($self->order, $item, 1);
 
1238     $item->sellprice($price_src->price);
 
1239     $item->active_price_source($price_src);
 
1240     $item->discount($discount_src->discount);
 
1241     $item->active_discount_source($discount_src);
 
1243     my $price_editable = $self->order->is_sales ? $::auth->assert('sales_edit_prices', 1) : $::auth->assert('purchase_edit_prices', 1);
 
1246       ->run('kivi.Order.set_price_and_source_text',    $item_id, $price_src   ->source, $price_src   ->source_description, $item->sellprice_as_number, $price_editable)
 
1247       ->run('kivi.Order.set_discount_and_source_text', $item_id, $discount_src->source, $discount_src->source_description, $item->discount_as_percent, $price_editable)
 
1248       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1249       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1250       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1252     if ($self->search_cvpartnumber) {
 
1253       $self->get_item_cvpartnumber($item);
 
1254       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1259   $self->js_redisplay_line_values;
 
1260   $self->js_redisplay_amounts_and_taxes;
 
1262   $self->js->render();
 
1265 sub action_save_phone_note {
 
1268   if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
 
1269     return $self->js->flash('error', t8('Phone note needs a subject and a body.'))->render;
 
1273   if ($::form->{phone_note}->{id}) {
 
1274     $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
 
1275     return $self->js->flash('error', t8('Phone note not found for this order.'))->render if !$phone_note;
 
1278   $phone_note = SL::DB::Note->new() if !$phone_note;
 
1279   my $is_new  = !$phone_note->id;
 
1281   $phone_note->assign_attributes(%{ $::form->{phone_note} },
 
1282                                  trans_id     => $self->order->id,
 
1283                                  trans_module => 'oe',
 
1284                                  employee     => SL::DB::Manager::Employee->current);
 
1287   $self->order(SL::DB::Order->new(id => $self->order->id)->load);
 
1289   my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
 
1292     ->replaceWith('#phone-notes', $tab_as_html)
 
1293     ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
 
1294     ->flash('info', $is_new ? t8('Phone note has been created.') : t8('Phone note has been updated.'))
 
1298 sub action_delete_phone_note {
 
1301   my $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
 
1303   return $self->js->flash('error', t8('Phone note not found for this order.'))->render if !$phone_note;
 
1305   $phone_note->delete;
 
1306   $self->order(SL::DB::Order->new(id => $self->order->id)->load);
 
1308   my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
 
1311     ->replaceWith('#phone-notes', $tab_as_html)
 
1312     ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
 
1313     ->flash('info', t8('Phone note has been deleted.'))
 
1317 sub js_load_second_row {
 
1318   my ($self, $item, $item_id, $do_parse) = @_;
 
1321     # Parse values from form (they are formated while rendering (template)).
 
1322     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1323     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1324     foreach my $var (@{ $item->cvars_by_config }) {
 
1325       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1327     $item->parse_custom_variable_values;
 
1330   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1333     ->html('#second_row_' . $item_id, $row_as_html)
 
1334     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1337 sub js_redisplay_line_values {
 
1340   my $is_sales = $self->order->is_sales;
 
1342   # sales orders with margins
 
1347        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1348        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1349        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1350       ]} @{ $self->order->items_sorted };
 
1354        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1355       ]} @{ $self->order->items_sorted };
 
1359     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1362 sub js_redisplay_amounts_and_taxes {
 
1365   if (scalar @{ $self->{taxes} }) {
 
1366     $self->js->show('#taxincluded_row_id');
 
1368     $self->js->hide('#taxincluded_row_id');
 
1371   if ($self->order->taxincluded) {
 
1372     $self->js->hide('#subtotal_row_id');
 
1374     $self->js->show('#subtotal_row_id');
 
1377   if ($self->order->is_sales) {
 
1378     my $is_neg = $self->order->marge_total < 0;
 
1380       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1381       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1382       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1383       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1384       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1385       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1386       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1387       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1391     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1392     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1393     ->remove('.tax_row')
 
1394     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1397 sub js_redisplay_cvpartnumbers {
 
1400   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1402   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1405     ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
 
1408 sub js_reset_order_and_item_ids_after_save {
 
1412     ->val('#id', $self->order->id)
 
1413     ->val('#converted_from_oe_id', '')
 
1414     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1417   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1418     next if !$self->order->items_sorted->[$idx]->id;
 
1419     next if $form_item_id !~ m{^new};
 
1421       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1422       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1423       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1427   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1434 sub init_valid_types {
 
1435   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1441   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1442     die "Not a valid type for order";
 
1445   $self->type($::form->{type});
 
1451   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1452          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1453          : die "Not a valid type for order";
 
1458 sub init_search_cvpartnumber {
 
1461   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1462   my $search_cvpartnumber;
 
1463   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1464   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1466   return $search_cvpartnumber;
 
1469 sub init_show_update_button {
 
1472   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1483 sub init_all_price_factors {
 
1484   SL::DB::Manager::PriceFactor->get_all;
 
1487 sub init_part_picker_classification_ids {
 
1489   my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
 
1491   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
 
1497   my $right_for = { map { $_ => $_.'_edit' . ' | ' . $_.'_view' } @{$self->valid_types} };
 
1499   my $right   = $right_for->{ $self->type };
 
1500   $right    ||= 'DOES_NOT_EXIST';
 
1502   $::auth->assert($right);
 
1505 sub check_auth_for_edit {
 
1508   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1510   my $right   = $right_for->{ $self->type };
 
1511   $right    ||= 'DOES_NOT_EXIST';
 
1513   $::auth->assert($right);
 
1516 # build the selection box for contacts
 
1518 # Needed, if customer/vendor changed.
 
1519 sub build_contact_select {
 
1522   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1523     value_key  => 'cp_id',
 
1524     title_key  => 'full_name_dep',
 
1525     default    => $self->order->cp_id,
 
1527     style      => 'width: 300px',
 
1531 # build the selection box for the additional billing address
 
1533 # Needed, if customer/vendor changed.
 
1534 sub build_billing_address_select {
 
1537   return '' if $self->cv ne 'customer';
 
1539   select_tag('order.billing_address_id',
 
1540              [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
 
1542              title_key  => 'displayable_id',
 
1543              default    => $self->order->billing_address_id,
 
1545              style      => 'width: 300px',
 
1549 # build the selection box for shiptos
 
1551 # Needed, if customer/vendor changed.
 
1552 sub build_shipto_select {
 
1555   select_tag('order.shipto_id',
 
1556              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1557              value_key  => 'shipto_id',
 
1558              title_key  => 'displayable_id',
 
1559              default    => $self->order->shipto_id,
 
1561              style      => 'width: 300px',
 
1565 # build the inputs for the cusom shipto dialog
 
1567 # Needed, if customer/vendor changed.
 
1568 sub build_shipto_inputs {
 
1571   my $content = $self->p->render('common/_ship_to_dialog',
 
1572                                  vc_obj      => $self->order->customervendor,
 
1573                                  cs_obj      => $self->order->custom_shipto,
 
1574                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1575                                  id_selector => '#order_shipto_id');
 
1577   div_tag($content, id => 'shipto_inputs');
 
1580 # render the info line for business
 
1582 # Needed, if customer/vendor changed.
 
1583 sub build_business_info_row
 
1585   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1588 # build the rows for displaying taxes
 
1590 # Called if amounts where recalculated and redisplayed.
 
1591 sub build_tax_rows {
 
1595   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1596     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1598   return $rows_as_html;
 
1602 sub render_price_dialog {
 
1603   my ($self, $record_item) = @_;
 
1605   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1609       'kivi.io.price_chooser_dialog',
 
1610       t8('Available Prices'),
 
1611       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1616 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1617 #     $self->js->show('#dialog_flash_error');
 
1626   return if !$::form->{id};
 
1628   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1630   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1631   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1632   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1634   return $self->order;
 
1637 # load or create a new order object
 
1639 # And assign changes from the form to this object.
 
1640 # If the order is loaded from db, check if items are deleted in the form,
 
1641 # remove them form the object and collect them for removing from db on saving.
 
1642 # Then create/update items from form (via make_item) and add them.
 
1646   # add_items adds items to an order with no items for saving, but they cannot
 
1647   # be retrieved via items until the order is saved. Adding empty items to new
 
1648   # order here solves this problem.
 
1650   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1651   $order ||= SL::DB::Order->new(orderitems  => [],
 
1652                                 quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
 
1653                                 currency_id => $::instance_conf->get_currency_id(),);
 
1655   my $cv_id_method = $self->cv . '_id';
 
1656   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1657     $order->$cv_id_method($::form->{$cv_id_method});
 
1658     setup_order_from_cv($order);
 
1661   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1662   my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
 
1664   $order->assign_attributes(%{$::form->{order}});
 
1666   $self->setup_custom_shipto_from_form($order, $::form);
 
1668   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1669     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1670     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1673   # remove deleted items
 
1674   $self->item_ids_to_delete([]);
 
1675   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1676     my $item = $order->orderitems->[$idx];
 
1677     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1678       splice @{$order->orderitems}, $idx, 1;
 
1679       push @{$self->item_ids_to_delete}, $item->id;
 
1685   foreach my $form_attr (@{$form_orderitems}) {
 
1686     my $item = make_item($order, $form_attr);
 
1687     $item->position($pos);
 
1691   $order->add_items(grep {!$_->id} @items);
 
1696 # create or update items from form
 
1698 # Make item objects from form values. For items already existing read from db.
 
1699 # Create a new item else. And assign attributes.
 
1701   my ($record, $attr) = @_;
 
1704   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1706   my $is_new = !$item;
 
1708   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1709   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1710   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1711   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1713   $item->assign_attributes(%$attr);
 
1716     my $texts = get_part_texts($item->part, $record->language_id);
 
1717     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1718     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1719     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1727 # This is used to add one item
 
1729   my ($record, $attr) = @_;
 
1731   my $item = SL::DB::OrderItem->new;
 
1733   # Remove attributes where the user left or set the inputs empty.
 
1734   # So these attributes will be undefined and we can distinguish them
 
1735   # from zero later on.
 
1736   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1737     delete $attr->{$_} if $attr->{$_} eq '';
 
1740   $item->assign_attributes(%$attr);
 
1741   $item->qty(1.0)                   if !$item->qty;
 
1742   $item->unit($item->part->unit)    if !$item->unit;
 
1744   my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
 
1747   $new_attr{description}            = $item->part->description     if ! $item->description;
 
1748   $new_attr{qty}                    = 1.0                          if ! $item->qty;
 
1749   $new_attr{price_factor_id}        = $item->part->price_factor_id if ! $item->price_factor_id;
 
1750   $new_attr{sellprice}              = $price_src->price;
 
1751   $new_attr{discount}               = $discount_src->discount;
 
1752   $new_attr{active_price_source}    = $price_src;
 
1753   $new_attr{active_discount_source} = $discount_src;
 
1754   $new_attr{longdescription}        = $item->part->notes           if ! defined $attr->{longdescription};
 
1755   $new_attr{project_id}             = $record->globalproject_id;
 
1756   $new_attr{lastcost}               = $record->is_sales ? $item->part->lastcost : 0;
 
1758   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1759   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1760   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1761   $new_attr{custom_variables} = [];
 
1763   my $texts = get_part_texts($item->part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1765   $item->assign_attributes(%new_attr, %{ $texts });
 
1770 sub setup_order_from_cv {
 
1773   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id language_id));
 
1775   $order->intnotes($order->customervendor->notes);
 
1777   return if !$order->is_sales;
 
1779   $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1780   $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1781                       ? $order->customer->taxincluded_checked
 
1782                       : $::myconfig{taxincluded_checked});
 
1784   my $address = $order->customer->default_billing_address;;
 
1785   $order->billing_address_id($address ? $address->id : undef);
 
1788 # setup custom shipto from form
 
1790 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1791 # with 'shiptocvar_'.
 
1792 # Mark it to be deleted if a shipto from master data is selected
 
1793 # (i.e. order has a shipto).
 
1794 # Else, update or create a new custom shipto. If the fields are empty, it
 
1795 # will not be saved on save.
 
1796 sub setup_custom_shipto_from_form {
 
1797   my ($self, $order, $form) = @_;
 
1799   if ($order->shipto) {
 
1800     $self->is_custom_shipto_to_delete(1);
 
1802     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1804     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1805     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1807     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1808     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1812 # recalculate prices and taxes
 
1814 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1818   my %pat = $self->order->calculate_prices_and_taxes();
 
1820   $self->{taxes} = [];
 
1821   foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
 
1822     my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
1824     push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
 
1825                                 netamount => $netamount,
 
1826                                 tax       => SL::DB::Tax->new(id => $tax_id)->load });
 
1828   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1831 # get data for saving, printing, ..., that is not changed in the form
 
1833 # Only cvars for now.
 
1834 sub get_unalterable_data {
 
1837   foreach my $item (@{ $self->order->items }) {
 
1838     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1839     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1840     foreach my $var (@{ $item->cvars_by_config }) {
 
1841       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1843     $item->parse_custom_variable_values;
 
1849 # And remove related files in the spool directory
 
1854   my $db     = $self->order->db;
 
1856   $db->with_transaction(
 
1858       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1859       $self->order->delete;
 
1860       my $spool = $::lx_office_conf{paths}->{spool};
 
1861       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1863       $self->save_history('DELETED');
 
1866   }) || push(@{$errors}, $db->error);
 
1873 # And delete items that are deleted in the form.
 
1878   my $db     = $self->order->db;
 
1880   # check for new or updated phone note
 
1881   if ($::form->{phone_note}->{subject} || $::form->{phone_note}->{body}) {
 
1882     if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
 
1883       return [t8('Phone note needs a subject and a body.')];
 
1887     if ($::form->{phone_note}->{id}) {
 
1888       $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
 
1889       return [t8('Phone note not found for this order.')] if !$phone_note;
 
1892     $phone_note = SL::DB::Note->new() if !$phone_note;
 
1893     my $is_new  = !$phone_note->id;
 
1895     $phone_note->assign_attributes(%{ $::form->{phone_note} },
 
1896                                    trans_id     => $self->order->id,
 
1897                                    trans_module => 'oe',
 
1898                                    employee     => SL::DB::Manager::Employee->current);
 
1900     $self->order->add_phone_notes($phone_note) if $is_new;
 
1903   $db->with_transaction(sub {
 
1904     # delete custom shipto if it is to be deleted or if it is empty
 
1905     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1906       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1907       $self->order->custom_shipto(undef);
 
1910     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1911     $self->order->save(cascade => 1);
 
1914     if ($::form->{converted_from_oe_id}) {
 
1915       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1917       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1918         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1919         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1920         $src->link_to_record($self->order);
 
1922       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1924         foreach (@{ $self->order->items_sorted }) {
 
1925           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1927           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1928                                   from_id    => $from_id,
 
1929                                   to_table   => 'orderitems',
 
1936       $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
 
1939     $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
 
1941     $self->save_history('SAVED');
 
1944   }) || push(@{$errors}, $db->error);
 
1949 sub workflow_sales_or_request_for_quotation {
 
1953   my $errors = $self->save();
 
1955   if (scalar @{ $errors }) {
 
1956     $self->js->flash('error', $_) for @{ $errors };
 
1957     return $self->js->render();
 
1960   my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
 
1962   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1963   delete $::form->{id};
 
1965   # no linked records from order to quotations
 
1966   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
1968   # set item ids to new fake id, to identify them as new items
 
1969   foreach my $item (@{$self->order->items_sorted}) {
 
1970     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1974   $::form->{type} = $destination_type;
 
1975   $self->type($self->init_type);
 
1976   $self->cv  ($self->init_cv);
 
1980   $self->get_unalterable_data();
 
1981   $self->pre_render();
 
1983   # trigger rendering values for second row as hidden, because they
 
1984   # are loaded only on demand. So we need to keep the values from the
 
1986   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1990     title => $self->get_title_for('edit'),
 
1991     %{$self->{template_args}}
 
1995 sub workflow_sales_or_purchase_order {
 
1999   my $errors = $self->save();
 
2001   if (scalar @{ $errors }) {
 
2002     $self->js->flash('error', $_) foreach @{ $errors };
 
2003     return $self->js->render();
 
2006   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
2007                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
2008                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
2009                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
2012   # check for direct delivery
 
2013   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
2015   if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
 
2016       && $::form->{use_shipto} && $self->order->shipto) {
 
2017     $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
 
2020   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
2021   $self->{converted_from_oe_id} = delete $::form->{id};
 
2023   # set item ids to new fake id, to identify them as new items
 
2024   foreach my $item (@{$self->order->items_sorted}) {
 
2025     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
2028   if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
 
2029     if ($::form->{use_shipto}) {
 
2030       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
2032       # remove any custom shipto if not wanted
 
2033       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
2038   $::form->{type} = $destination_type;
 
2039   $self->type($self->init_type);
 
2040   $self->cv  ($self->init_cv);
 
2044   $self->get_unalterable_data();
 
2045   $self->pre_render();
 
2047   # trigger rendering values for second row as hidden, because they
 
2048   # are loaded only on demand. So we need to keep the values from the
 
2050   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
2054     title => $self->get_title_for('edit'),
 
2055     %{$self->{template_args}}
 
2063   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
2064   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
2065   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
2066   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
 
2067   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
2070   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
2073   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
2075   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
2076   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
2077   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
2078   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
2079   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
2081   my $print_form = Form->new('');
 
2082   $print_form->{type}        = $self->type;
 
2083   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
2084   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
2085     form => $print_form,
 
2086     options => {dialog_name_prefix => 'print_options.',
 
2090                 no_opendocument    => 0,
 
2094   foreach my $item (@{$self->order->orderitems}) {
 
2095     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
2096     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
2097     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
2100   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
2101     # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
 
2102     # Do not use write_to_objects to prevent order->delivered to be set, because this should be
 
2103     # the value from db, which can be set manually or is set when linked delivery orders are saved.
 
2104     SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
 
2107   if ($self->order->number && $::instance_conf->get_webdav) {
 
2108     my $webdav = SL::Webdav->new(
 
2109       type     => $self->type,
 
2110       number   => $self->order->number,
 
2112     my @all_objects = $webdav->get_all_objects;
 
2113     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
2115                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
2119   if (   (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
 
2120       && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
 
2121     $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
 
2123   $self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
 
2125   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
2127   $self->{template_args}->{num_phone_notes} = scalar @{ $self->order->phone_notes || [] };
 
2129   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
2130                                                          edit_periodic_invoices_config calculate_qty follow_up show_history);
 
2131   $self->setup_edit_action_bar;
 
2134 sub setup_edit_action_bar {
 
2135   my ($self, %params) = @_;
 
2137   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
2138                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
2139                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
2141   my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
 
2142   my @req_cusordnumber   = qw(kivi.Order.check_cusordnumber_presence)           x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
 
2144   my $has_invoice_for_advance_payment;
 
2145   if ($self->order->id && $self->type eq sales_order_type()) {
 
2146     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2147     $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
 
2150   my $has_final_invoice;
 
2151   if ($self->order->id && $self->type eq sales_order_type()) {
 
2152     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2153     $has_final_invoice               = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
 
2156   my $right_for         = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
2157   my $right             = $right_for->{ $self->type };
 
2158   $right              ||= 'DOES_NOT_EXIST';
 
2159   my $may_edit_create   = $::auth->assert($right, 'may fail');
 
2161   for my $bar ($::request->layout->get('actionbar')) {
 
2166           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
2167                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
2169           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
 
2170                          @req_trans_cost_art, @req_cusordnumber,
 
2172           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2175           t8('Save and Close'),
 
2176           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
2177                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
2180           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
 
2181                          @req_trans_cost_art, @req_cusordnumber,
 
2183           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2187           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
2188           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2189                          @req_trans_cost_art, @req_cusordnumber,
 
2191           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.')
 
2192                      : !$self->order->id ? t8('This object has not been saved yet.')
 
2195       ], # end of combobox "Save"
 
2202           t8('Save and Quotation'),
 
2203           submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
 
2204           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2205           only_if  => (any { $self->type eq $_ } (sales_order_type())),
 
2206           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2210           submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
 
2211           only_if  => (any { $self->type eq $_ } (purchase_order_type())),
 
2212           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2215           t8('Save and Sales Order'),
 
2216           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
2217           checks   => [ @req_trans_cost_art ],
 
2218           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
2219           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2222           t8('Save and Purchase Order'),
 
2223           call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
 
2224           checks    => [ @req_trans_cost_art, @req_cusordnumber ],
 
2225           only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
2226           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2229           t8('Save and Delivery Order'),
 
2230           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2231                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2233           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2234                          @req_trans_cost_art, @req_cusordnumber,
 
2236           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())),
 
2237           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2240           t8('Save and Supplier Delivery Order'),
 
2241           call      => [ 'kivi.Order.save', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2242                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2244           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2245                          @req_trans_cost_art, @req_cusordnumber,
 
2247           only_if   => (any { $self->type eq $_ } (purchase_order_type())),
 
2248           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2251           t8('Save and Invoice'),
 
2252           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2253           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2254                          @req_trans_cost_art, @req_cusordnumber,
 
2256           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2259           ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
 
2260           call      => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::instance_conf->get_order_warn_duplicate_parts ],
 
2261           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2262                          @req_trans_cost_art, @req_cusordnumber,
 
2264           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2265                      : $has_final_invoice ? t8('This order has already a final invoice.')
 
2267           only_if   => (any { $self->type eq $_ } (sales_order_type())),
 
2270           t8('Save and Final Invoice'),
 
2271           call      => [ 'kivi.Order.save', 'save_and_final_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2272           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2273                          @req_trans_cost_art, @req_cusordnumber,
 
2275           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2276                      : $has_final_invoice ? t8('This order has already a final invoice.')
 
2278           only_if   => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
 
2281           t8('Save and AP Transaction'),
 
2282           call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
2283           only_if   => (any { $self->type eq $_ } (purchase_order_type())),
 
2284           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2287       ], # end of combobox "Workflow"
 
2294           t8('Save and preview PDF'),
 
2295           call     => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
2296                                                           $::instance_conf->get_order_warn_no_deliverydate,
 
2298           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2299           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2302           t8('Save and print'),
 
2303           call     => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
2304                                                          $::instance_conf->get_order_warn_no_deliverydate,
 
2306           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2307           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2310           t8('Save and E-mail'),
 
2311           id       => 'save_and_email_action',
 
2312           call     => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
2313                                                                          $::instance_conf->get_order_warn_no_deliverydate,
 
2315           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2316                     : !$self->order->id  ? t8('This object has not been saved yet.')
 
2320           t8('Download attachments of all parts'),
 
2321           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
2322           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2323           only_if  => $::instance_conf->get_doc_storage,
 
2325       ], # end of combobox "Export"
 
2329         call     => [ 'kivi.Order.delete_order' ],
 
2330         confirm  => $::locale->text('Do you really want to delete this object?'),
 
2331         disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2332                   : !$self->order->id  ? t8('This object has not been saved yet.')
 
2334         only_if  => $deletion_allowed,
 
2343           call     => [ 'set_history_window', $self->order->id, 'id' ],
 
2344           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
 
2348           call     => [ 'kivi.Order.follow_up_window' ],
 
2349           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2350           only_if  => $::auth->assert('productivity', 1),
 
2352       ], # end of combobox "more"
 
2358   my ($self, $doc_ref, $params) = @_;
 
2360   my $order  = $self->order;
 
2363   my $print_form = Form->new('');
 
2364   $print_form->{type}        = $order->type;
 
2365   $print_form->{formname}    = $params->{formname} || $order->type;
 
2366   $print_form->{format}      = $params->{format}   || 'pdf';
 
2367   $print_form->{media}       = $params->{media}    || 'file';
 
2368   $print_form->{groupitems}  = $params->{groupitems};
 
2369   $print_form->{printer_id}  = $params->{printer_id};
 
2370   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
2372   $order->language($params->{language});
 
2373   $order->flatten_to_form($print_form, format_amounts => 1);
 
2377   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
2378     $template_ext  = 'odt';
 
2379     $template_type = 'OpenDocument';
 
2380   } elsif ($print_form->{format} =~ m{html}i) {
 
2381     $template_ext  = 'html';
 
2382     $template_type = 'HTML';
 
2385   # search for the template
 
2386   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
2387     name        => $print_form->{formname},
 
2388     extension   => $template_ext,
 
2389     email       => $print_form->{media} eq 'email',
 
2390     language    => $params->{language},
 
2391     printer_id  => $print_form->{printer_id},
 
2394   if (!defined $template_file) {
 
2395     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);
 
2398   return @errors if scalar @errors;
 
2400   $print_form->throw_on_error(sub {
 
2402       $print_form->prepare_for_printing;
 
2404       $$doc_ref = SL::Helper::CreatePDF->create_pdf(
 
2405         format        => $print_form->{format},
 
2406         template_type => $template_type,
 
2407         template      => $template_file,
 
2408         variables     => $print_form,
 
2409         variable_content_types => {
 
2410           longdescription => 'html',
 
2411           partnotes       => 'html',
 
2413           $::form->get_variable_content_types_for_cvars,
 
2417     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2423 sub get_files_for_email_dialog {
 
2426   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2428   return %files if !$::instance_conf->get_doc_storage;
 
2430   if ($self->order->id) {
 
2431     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2432     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2433     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2434     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2438     uniq_by { $_->{id} }
 
2440       +{ id         => $_->part->id,
 
2441          partnumber => $_->part->partnumber }
 
2442     } @{$self->order->items_sorted};
 
2444   foreach my $part (@parts) {
 
2445     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2446     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2449   foreach my $key (keys %files) {
 
2450     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2456 sub make_periodic_invoices_config_from_yaml {
 
2457   my ($yaml_config) = @_;
 
2459   return if !$yaml_config;
 
2460   my $attr = SL::YAML::Load($yaml_config);
 
2461   return if 'HASH' ne ref $attr;
 
2462   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
2466 sub get_periodic_invoices_status {
 
2467   my ($self, $config) = @_;
 
2469   return                      if $self->type ne sales_order_type();
 
2470   return t8('not configured') if !$config;
 
2472   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
2473              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
2474              :                                                     die "Cannot get status of periodic invoices config";
 
2476   return $active ? t8('active') : t8('inactive');
 
2480   my ($self, $action) = @_;
 
2482   return '' if none { lc($action)} qw(add edit);
 
2485   # $::locale->text("Add Sales Order");
 
2486   # $::locale->text("Add Purchase Order");
 
2487   # $::locale->text("Add Quotation");
 
2488   # $::locale->text("Add Request for Quotation");
 
2489   # $::locale->text("Edit Sales Order");
 
2490   # $::locale->text("Edit Purchase Order");
 
2491   # $::locale->text("Edit Quotation");
 
2492   # $::locale->text("Edit Request for Quotation");
 
2494   $action = ucfirst(lc($action));
 
2495   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
2496        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
2497        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
2498        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
2502 sub get_item_cvpartnumber {
 
2503   my ($self, $item) = @_;
 
2505   return if !$self->search_cvpartnumber;
 
2506   return if !$self->order->customervendor;
 
2508   if ($self->cv eq 'vendor') {
 
2509     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2510     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2511   } elsif ($self->cv eq 'customer') {
 
2512     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2513     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2517 sub get_part_texts {
 
2518   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2520   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2521   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2523     description     => $defaults{description}     // $part->description,
 
2524     longdescription => $defaults{longdescription} // $part->notes,
 
2527   return $texts unless $language_id;
 
2529   my $translation = SL::DB::Manager::Translation->get_first(
 
2531       parts_id    => $part->id,
 
2532       language_id => $language_id,
 
2535   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2536   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2541 sub get_best_price_and_discount_source {
 
2542   my ($record, $item, $ignore_given) = @_;
 
2544   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
2547   if ( $item->part->is_assortment ) {
 
2548     # add assortment items with price 0, as the components carry the price
 
2549     $price_src = $price_source->price_from_source("");
 
2550     $price_src->price(0);
 
2551   } elsif (!$ignore_given && defined $item->sellprice) {
 
2552     $price_src = $price_source->price_from_source("");
 
2553     $price_src->price($item->sellprice);
 
2555     $price_src = $price_source->best_price
 
2556                ? $price_source->best_price
 
2557                : $price_source->price_from_source("");
 
2558     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
2559     $price_src->price(0) if !$price_source->best_price;
 
2563   if (!$ignore_given && defined $item->discount) {
 
2564     $discount_src = $price_source->discount_from_source("");
 
2565     $discount_src->discount($item->discount);
 
2567     $discount_src = $price_source->best_discount
 
2568                   ? $price_source->best_discount
 
2569                   : $price_source->discount_from_source("");
 
2570     $discount_src->discount(0) if !$price_source->best_discount;
 
2573   return ($price_src, $discount_src);
 
2576 sub sales_order_type {
 
2580 sub purchase_order_type {
 
2584 sub sales_quotation_type {
 
2588 sub request_quotation_type {
 
2589   'request_quotation';
 
2593   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
2594        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
2595        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
2596        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
2600 sub save_and_redirect_to {
 
2601   my ($self, %params) = @_;
 
2603   my $errors = $self->save();
 
2605   if (scalar @{ $errors }) {
 
2606     $self->js->flash('error', $_) foreach @{ $errors };
 
2607     return $self->js->render();
 
2610   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
2611            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
2612            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
2613            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
2615   flash_later('info', $text);
 
2617   $self->redirect_to(%params, id => $self->order->id);
 
2621   my ($self, $addition) = @_;
 
2623   my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
 
2624   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2626   SL::DB::History->new(
 
2627     trans_id    => $self->order->id,
 
2628     employee_id => SL::DB::Manager::Employee->current->id,
 
2629     what_done   => $self->order->type,
 
2630     snumbers    => $snumbers,
 
2631     addition    => $addition,
 
2635 sub store_doc_to_webdav_and_filemanagement {
 
2636   my ($self, $content, $filename, $variant) = @_;
 
2638   my $order = $self->order;
 
2641   # copy file to webdav folder
 
2642   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2643     my $webdav = SL::Webdav->new(
 
2644       type     => $order->type,
 
2645       number   => $order->number,
 
2647     my $webdav_file = SL::Webdav::File->new(
 
2649       filename => $filename,
 
2652       $webdav_file->store(data => \$content);
 
2655       push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
 
2658   if ($order->id && $::instance_conf->get_doc_storage) {
 
2660       SL::File->save(object_id     => $order->id,
 
2661                      object_type   => $order->type,
 
2662                      mime_type     => SL::MIME->mime_type_from_ext($filename),
 
2663                      source        => 'created',
 
2664                      file_type     => 'document',
 
2665                      file_name     => $filename,
 
2666                      file_contents => $content,
 
2667                      print_variant => $variant);
 
2670       push @errors, t8('Storing the document in the storage backend failed: #1', $@);
 
2677 sub link_requirement_specs_linking_to_created_from_objects {
 
2678   my ($self, @converted_from_oe_ids) = @_;
 
2680   return unless @converted_from_oe_ids;
 
2682   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
 
2683   foreach my $rs_order (@{ $rs_orders }) {
 
2684     SL::DB::RequirementSpecOrder->new(
 
2685       order_id            => $self->order->id,
 
2686       requirement_spec_id => $rs_order->requirement_spec_id,
 
2687       version_id          => $rs_order->version_id,
 
2692 sub set_project_in_linked_requirement_specs {
 
2695   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
 
2696   foreach my $rs_order (@{ $rs_orders }) {
 
2697     next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
 
2699     $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
 
2711 SL::Controller::Order - controller for orders
 
2715 This is a new form to enter orders, completely rewritten with the use
 
2716 of controller and java script techniques.
 
2718 The aim is to provide the user a better experience and a faster workflow. Also
 
2719 the code should be more readable, more reliable and better to maintain.
 
2727 One input row, so that input happens every time at the same place.
 
2731 Use of pickers where possible.
 
2735 Possibility to enter more than one item at once.
 
2739 Item list in a scrollable area, so that the workflow buttons stay at
 
2744 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2745 possible (by partnumber, description, qty, sellprice and discount for now).
 
2749 No C<update> is necessary. All entries and calculations are managed
 
2750 with ajax-calls and the page only reloads on C<save>.
 
2754 User can see changes immediately, because of the use of java script
 
2765 =item * C<SL/Controller/Order.pm>
 
2769 =item * C<template/webpages/order/form.html>
 
2773 =item * C<template/webpages/order/tabs/basic_data.html>
 
2775 Main tab for basic_data.
 
2777 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2778 reused from generic code.
 
2782 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
2784 For displaying information on business type
 
2786 =item * C<template/webpages/order/tabs/_item_input.html>
 
2788 The input line for items
 
2790 =item * C<template/webpages/order/tabs/_row.html>
 
2792 One row for already entered items
 
2794 =item * C<template/webpages/order/tabs/_tax_row.html>
 
2796 Displaying tax information
 
2798 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
2800 Dialog for selecting price and discount sources
 
2804 =item * C<js/kivi.Order.js>
 
2806 java script functions
 
2816 =item * price sources: little symbols showing better price / better discount
 
2818 =item * select units in input row?
 
2820 =item * check for direct delivery (workflow sales order -> purchase order)
 
2822 =item * access rights
 
2824 =item * display weights
 
2828 =item * optional client/user behaviour
 
2830 (transactions has to be set - department has to be set -
 
2831  force project if enabled in client config)
 
2835 =head1 KNOWN BUGS AND CAVEATS
 
2841 No indication that <shift>-up/down expands/collapses second row.
 
2845 Table header is not sticky in the scrolling area.
 
2849 Sorting does not include C<position>, neither does reordering.
 
2851 This behavior was implemented intentionally. But we can discuss, which behavior
 
2852 should be implemented.
 
2856 =head1 To discuss / Nice to have
 
2862 How to expand/collapse second row. Now it can be done clicking the icon or
 
2867 This controller uses a (changed) copy of the template for the PriceSource
 
2868 dialog. Maybe there could be used one code source.
 
2872 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2873 form. This is not only a problem here, but also in all parts using the PTC.
 
2874 There exists a ticket and a patch. This patch should be testet.
 
2878 An indicator, if the actual inputs are saved (like in an
 
2879 editor or on text processing application).
 
2883 A warning when leaving the page without saveing unchanged inputs.
 
2890 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>