1 package SL::Controller::Order;
 
   4 use parent qw(SL::Controller::Base);
 
   6 use SL::Helper::Flash qw(flash_later);
 
   8 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
 
   9 use SL::Locale::String qw(t8);
 
  10 use SL::SessionFile::Random;
 
  15 use SL::Util qw(trim);
 
  17 use SL::DB::AdditionalBillingAddress;
 
  24 use SL::DB::PartClassification;
 
  25 use SL::DB::PartsGroup;
 
  28 use SL::DB::RecordLink;
 
  29 use SL::DB::RequirementSpec;
 
  31 use SL::DB::Translation;
 
  33 use SL::Helper::CreatePDF qw(:all);
 
  34 use SL::Helper::PrintOptions;
 
  35 use SL::Helper::ShippedQty;
 
  36 use SL::Helper::UserPreferences::DisplayPreferences;
 
  37 use SL::Helper::UserPreferences::PositionsScrollbar;
 
  38 use SL::Helper::UserPreferences::UpdatePositions;
 
  40 use SL::Controller::Helper::GetModels;
 
  42 use List::Util qw(first sum0);
 
  43 use List::UtilsBy qw(sort_by uniq_by);
 
  44 use List::MoreUtils qw(any none pairwise first_index);
 
  45 use English qw(-no_match_vars);
 
  50 use Rose::Object::MakeMethods::Generic
 
  52  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 
  53  'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
 
  58 __PACKAGE__->run_before('check_auth');
 
  60 __PACKAGE__->run_before('check_auth_for_edit',
 
  61                         except => [ qw(edit show_customer_vendor_details_dialog price_popup load_second_rows) ]);
 
  63 __PACKAGE__->run_before('recalc',
 
  64                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
 
  67 __PACKAGE__->run_before('get_unalterable_data',
 
  68                         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
 
  79   $self->order->transdate(DateTime->now_local());
 
  80   my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
  81                    $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
  83   if (   ($self->type eq sales_order_type()     &&  $::instance_conf->get_deliverydate_on)
 
  84       || ($self->type eq sales_quotation_type() &&  $::instance_conf->get_reqdate_on)
 
  85       && (!$self->order->reqdate)) {
 
  86     $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
 
  93     title => $self->get_title_for('add'),
 
  94     %{$self->{template_args}}
 
  98 # edit an existing order
 
 106     # this is to edit an order from an unsaved order object
 
 108     # set item ids to new fake id, to identify them as new items
 
 109     foreach my $item (@{$self->order->items_sorted}) {
 
 110       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 112     # trigger rendering values for second row as hidden, because they
 
 113     # are loaded only on demand. So we need to keep the values from
 
 115     $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
 122     title => $self->get_title_for('edit'),
 
 123     %{$self->{template_args}}
 
 127 # edit a collective order (consisting of one or more existing orders)
 
 128 sub action_edit_collective {
 
 132   my @multi_ids = map {
 
 133     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 134   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 136   # fall back to add if no ids are given
 
 137   if (scalar @multi_ids == 0) {
 
 142   # fall back to save as new if only one id is given
 
 143   if (scalar @multi_ids == 1) {
 
 144     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 145     $self->action_save_as_new();
 
 149   # make new order from given orders
 
 150   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 151   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 152   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 154   $self->action_edit();
 
 161   my $errors = $self->delete();
 
 163   if (scalar @{ $errors }) {
 
 164     $self->js->flash('error', $_) foreach @{ $errors };
 
 165     return $self->js->render();
 
 168   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 169            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 170            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 171            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 173   flash_later('info', $text);
 
 175   my @redirect_params = (
 
 180   $self->redirect_to(@redirect_params);
 
 187   my $errors = $self->save();
 
 189   if (scalar @{ $errors }) {
 
 190     $self->js->flash('error', $_) foreach @{ $errors };
 
 191     return $self->js->render();
 
 194   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 195            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 196            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 197            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 199   flash_later('info', $text);
 
 202   if ($::form->{back_to_caller}) {
 
 203     @redirect_params = $::form->{callback} ? ($::form->{callback})
 
 204                                            : (controller => 'LoginScreen', action => 'user_login');
 
 210       id       => $self->order->id,
 
 211       callback => $::form->{callback},
 
 215   $self->redirect_to(@redirect_params);
 
 218 # save the order as new document an open it for edit
 
 219 sub action_save_as_new {
 
 222   my $order = $self->order;
 
 225     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 226     return $self->js->render();
 
 229   # load order from db to check if values changed
 
 230   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 233   # Lets assign a new number if the user hasn't changed the previous one.
 
 234   # If it has been changed manually then use it as-is.
 
 235   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 237                         : trim($order->number);
 
 239   # Clear transdate unless changed
 
 240   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 241                         ? DateTime->today_local
 
 244   # Set new reqdate unless changed if it is enabled in client config
 
 245   if ($order->reqdate == $saved_order->reqdate) {
 
 246     my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
 247                      $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
 249     if (   ($self->type eq sales_order_type()     &&  !$::instance_conf->get_deliverydate_on)
 
 250         || ($self->type eq sales_quotation_type() &&  !$::instance_conf->get_reqdate_on)) {
 
 251       $new_attrs{reqdate} = '';
 
 253       $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 256     $new_attrs{reqdate} = $order->reqdate;
 
 260   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 262   # Warn on obsolete items
 
 263   my @obsolete_positions = map { $_->position } grep { $_->part->obsolete } @{ $order->items_sorted };
 
 264   flash_later('warning', t8('This record containts obsolete items at position #1', join ', ', @obsolete_positions)) if @obsolete_positions;
 
 266   # Create new record from current one
 
 267   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 269   # no linked records on save as new
 
 270   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 273   $self->action_save();
 
 278 # This is called if "print" is pressed in the print dialog.
 
 279 # If PDF creation was requested and succeeded, the pdf is offered for download
 
 280 # via send_file (which uses ajax in this case).
 
 284   my $errors = $self->save();
 
 286   if (scalar @{ $errors }) {
 
 287     $self->js->flash('error', $_) foreach @{ $errors };
 
 288     return $self->js->render();
 
 291   $self->js_reset_order_and_item_ids_after_save;
 
 293   my $format      = $::form->{print_options}->{format};
 
 294   my $media       = $::form->{print_options}->{media};
 
 295   my $formname    = $::form->{print_options}->{formname};
 
 296   my $copies      = $::form->{print_options}->{copies};
 
 297   my $groupitems  = $::form->{print_options}->{groupitems};
 
 298   my $printer_id  = $::form->{print_options}->{printer_id};
 
 300   # only PDF, OpenDocument & HTML for now
 
 301   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
 
 302     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 305   # only screen or printer by now
 
 306   if (none { $media eq $_ } qw(screen printer)) {
 
 307     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 310   # create a form for generate_attachment_filename
 
 311   my $form   = Form->new;
 
 312   $form->{$self->nr_key()}  = $self->order->number;
 
 313   $form->{type}             = $self->type;
 
 314   $form->{format}           = $format;
 
 315   $form->{formname}         = $formname;
 
 316   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 317   my $doc_filename          = $form->generate_attachment_filename();
 
 320   my @errors = $self->generate_doc(\$doc, { media      => $media,
 
 322                                             formname   => $formname,
 
 323                                             language   => $self->order->language,
 
 324                                             printer_id => $printer_id,
 
 325                                             groupitems => $groupitems });
 
 326   if (scalar @errors) {
 
 327     return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
 
 330   if ($media eq 'screen') {
 
 332     $self->js->flash('info', t8('The document has been created.'));
 
 335       type         => SL::MIME->mime_type_from_ext($doc_filename),
 
 336       name         => $doc_filename,
 
 340   } elsif ($media eq 'printer') {
 
 342     my $printer_id = $::form->{print_options}->{printer_id};
 
 343     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 348     $self->js->flash('info', t8('The document has been printed.'));
 
 351   my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
 
 352   if (scalar @warnings) {
 
 353     $self->js->flash('warning', $_) for @warnings;
 
 356   $self->save_history('PRINTED');
 
 359     ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
 
 362 sub action_preview_pdf {
 
 365   my $errors = $self->save();
 
 366   if (scalar @{ $errors }) {
 
 367     $self->js->flash('error', $_) foreach @{ $errors };
 
 368     return $self->js->render();
 
 371   $self->js_reset_order_and_item_ids_after_save;
 
 374   my $media       = 'screen';
 
 375   my $formname    = $self->type;
 
 378   # create a form for generate_attachment_filename
 
 379   my $form   = Form->new;
 
 380   $form->{$self->nr_key()}  = $self->order->number;
 
 381   $form->{type}             = $self->type;
 
 382   $form->{format}           = $format;
 
 383   $form->{formname}         = $formname;
 
 384   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 385   my $pdf_filename          = $form->generate_attachment_filename();
 
 388   my @errors = $self->generate_doc(\$pdf, { media      => $media,
 
 390                                             formname   => $formname,
 
 391                                             language   => $self->order->language,
 
 393   if (scalar @errors) {
 
 394     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 396   $self->save_history('PREVIEWED');
 
 397   $self->js->flash('info', t8('The PDF has been previewed'));
 
 401     type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 402     name         => $pdf_filename,
 
 407 # open the email dialog
 
 408 sub action_save_and_show_email_dialog {
 
 411   my $errors = $self->save();
 
 413   if (scalar @{ $errors }) {
 
 414     $self->js->flash('error', $_) foreach @{ $errors };
 
 415     return $self->js->render();
 
 418   my $cv_method = $self->cv;
 
 420   if (!$self->order->$cv_method) {
 
 421     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'))
 
 426   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 427   $email_form->{to} ||= $self->order->$cv_method->email;
 
 428   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 429   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 430   # Todo: get addresses from shipto, if any
 
 432   my $form = Form->new;
 
 433   $form->{$self->nr_key()}  = $self->order->number;
 
 434   $form->{cusordnumber}     = $self->order->cusordnumber;
 
 435   $form->{formname}         = $self->type;
 
 436   $form->{type}             = $self->type;
 
 437   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 438   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
 
 439   $form->{format}           = 'pdf';
 
 440   $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
 442   $email_form->{subject}             = $form->generate_email_subject();
 
 443   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 444   $email_form->{message}             = $form->generate_email_body();
 
 445   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 447   my %files = $self->get_files_for_email_dialog();
 
 449   my @employees_with_email = grep {
 
 450     my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
 
 451     $user && !!trim($user->get_config_value('email'));
 
 452   } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
 
 455   my $all_partner_email_addresses = $self->order->customervendor->get_all_email_addresses();
 
 457   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 458                                   email_form    => $email_form,
 
 459                                   show_bcc      => $::auth->assert('email_bcc', 'may fail'),
 
 461                                   is_customer   => $self->cv eq 'customer',
 
 462                                   ALL_EMPLOYEES => \@employees_with_email,
 
 463                                   ALL_PARTNER_EMAIL_ADDRESSES => $all_partner_email_addresses,
 
 467       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 474 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 475 sub action_send_email {
 
 478   my $errors = $self->save();
 
 480   if (scalar @{ $errors }) {
 
 481     $self->js->run('kivi.Order.close_email_dialog');
 
 482     $self->js->flash('error', $_) foreach @{ $errors };
 
 483     return $self->js->render();
 
 486   $self->js_reset_order_and_item_ids_after_save;
 
 488   my $email_form  = delete $::form->{email_form};
 
 490   if ($email_form->{additional_to}) {
 
 491     $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
 
 492     delete $email_form->{additional_to};
 
 495   my %field_names = (to => 'email');
 
 497   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 499   # for Form::cleanup which may be called in Form::send_email
 
 500   $::form->{cwd}    = getcwd();
 
 501   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 503   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 504   $::form->{media}  = 'email';
 
 506   $::form->{attachment_policy} //= '';
 
 508   # Is an old file version available?
 
 510   if ($::form->{attachment_policy} eq 'old_file') {
 
 511     $attfile = SL::File->get_all(object_id     => $self->order->id,
 
 512                                  object_type   => $self->type,
 
 513                                  file_type     => 'document',
 
 514                                  print_variant => $::form->{formname});
 
 517   if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
 
 519     my @errors = $self->generate_doc(\$doc, {media      => $::form->{media},
 
 520                                              format     => $::form->{print_options}->{format},
 
 521                                              formname   => $::form->{print_options}->{formname},
 
 522                                              language   => $self->order->language,
 
 523                                              printer_id => $::form->{print_options}->{printer_id},
 
 524                                              groupitems => $::form->{print_options}->{groupitems}});
 
 525     if (scalar @errors) {
 
 526       return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
 
 529     my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename}, $::form->{formname});
 
 530     if (scalar @warnings) {
 
 531       flash_later('warning', $_) for @warnings;
 
 534     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 535     $sfile->fh->print($doc);
 
 538     $::form->{tmpfile} = $sfile->file_name;
 
 539     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 542   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 543   $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
 
 545   # internal notes unless no email journal
 
 546   unless ($::instance_conf->get_email_journal) {
 
 547     my $intnotes = $self->order->intnotes;
 
 548     $intnotes   .= "\n\n" if $self->order->intnotes;
 
 549     $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 550     $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 551     $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 552     $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 553     $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 554     $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 555     $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
 
 557     $self->order->update_attributes(intnotes => $intnotes);
 
 560   $self->save_history('MAILED');
 
 562   flash_later('info', t8('The email has been sent.'));
 
 564   my @redirect_params = (
 
 567     id     => $self->order->id,
 
 570   $self->redirect_to(@redirect_params);
 
 573 # open the periodic invoices config dialog
 
 575 # If there are values in the form (i.e. dialog was opened before),
 
 576 # then use this values. Create new ones, else.
 
 577 sub action_show_periodic_invoices_config_dialog {
 
 580   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 581   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 582   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 583                                                    order_value_periodicity => 'p', # = same as periodicity
 
 584                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 585                                                    extend_automatically_by => 12,
 
 587                                                    email_subject           => GenericTranslations->get(
 
 588                                                                                 language_id      => $::form->{language_id},
 
 589                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 590                                                    email_body              => GenericTranslations->get(
 
 591                                                                                 language_id      => $::form->{language_id},
 
 592                                                                                 translation_type => "salutation_general")
 
 593                                                                             . GenericTranslations->get(
 
 594                                                                                 language_id      => $::form->{language_id},
 
 595                                                                                 translation_type => "salutation_punctuation_mark") . "\n\n"
 
 596                                                                             . GenericTranslations->get(
 
 597                                                                                 language_id      => $::form->{language_id},
 
 598                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 600   # for older configs, replace email preset text if not yet set.
 
 601   $config->email_subject(GenericTranslations->get(
 
 602                                               language_id      => $::form->{language_id},
 
 603                                               translation_type =>"preset_text_periodic_invoices_email_subject")
 
 604                         ) unless $config->email_subject;
 
 606   $config->email_body(GenericTranslations->get(
 
 607                                               language_id      => $::form->{language_id},
 
 608                                               translation_type => "salutation_general")
 
 609                     . GenericTranslations->get(
 
 610                                               language_id      => $::form->{language_id},
 
 611                                               translation_type => "salutation_punctuation_mark") . "\n\n"
 
 612                     . GenericTranslations->get(
 
 613                                               language_id      => $::form->{language_id},
 
 614                                               translation_type =>"preset_text_periodic_invoices_email_body")
 
 615                      ) unless $config->email_body;
 
 617   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 618   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 620   $::form->get_lists(printers => "ALL_PRINTERS",
 
 621                      charts   => { key       => 'ALL_CHARTS',
 
 622                                    transdate => 'current_date' });
 
 624   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 626   if ($::form->{customer_id}) {
 
 627     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 628     my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
 
 629     $::form->{postal_invoice}                  = $customer_object->postal_invoice;
 
 630     $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
 
 631     $config->send_email(0) if $::form->{postal_invoice};
 
 634   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 636                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 637                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 642 # assign the values of the periodic invoices config dialog
 
 643 # as yaml in the hidden tag and set the status.
 
 644 sub action_assign_periodic_invoices_config {
 
 647   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 649   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 650                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 651                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 652                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 653                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 654                  start_date_as_date         => $::form->{start_date_as_date},
 
 655                  end_date_as_date           => $::form->{end_date_as_date},
 
 656                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 657                  print                      => $::form->{print}      ? 1                         : 0,
 
 658                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 659                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 660                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 661                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 662                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 663                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 664                  email_recipient_address    => $::form->{email_recipient_address},
 
 665                  email_sender               => $::form->{email_sender},
 
 666                  email_subject              => $::form->{email_subject},
 
 667                  email_body                 => $::form->{email_body},
 
 670   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 672   my $status = $self->get_periodic_invoices_status($config);
 
 675     ->remove('#order_periodic_invoices_config')
 
 676     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 677     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 678     ->html('#periodic_invoices_status', $status)
 
 679     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 683 sub action_get_has_active_periodic_invoices {
 
 686   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 687   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 689   my $has_active_periodic_invoices =
 
 690        $self->type eq sales_order_type()
 
 693     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 694     && $config->get_previous_billed_period_start_date;
 
 696   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 699 # save the order and redirect to the frontend subroutine for a new
 
 701 sub action_save_and_delivery_order {
 
 704   $self->save_and_redirect_to(
 
 705     controller => 'oe.pl',
 
 706     action     => 'oe_delivery_order_from_order',
 
 710 sub action_save_and_supplier_delivery_order {
 
 713   $self->save_and_redirect_to(
 
 714     controller => 'controller.pl',
 
 715     action     => 'DeliveryOrder/add_from_order',
 
 716     type       => 'supplier_delivery_order',
 
 720 # save the order and redirect to the frontend subroutine for a new
 
 722 sub action_save_and_invoice {
 
 725   $self->save_and_redirect_to(
 
 726     controller => 'oe.pl',
 
 727     action     => 'oe_invoice_from_order',
 
 731 sub action_save_and_invoice_for_advance_payment {
 
 734   $self->save_and_redirect_to(
 
 735     controller       => 'oe.pl',
 
 736     action           => 'oe_invoice_from_order',
 
 737     new_invoice_type => 'invoice_for_advance_payment',
 
 741 sub action_save_and_final_invoice {
 
 744   $self->save_and_redirect_to(
 
 745     controller       => 'oe.pl',
 
 746     action           => 'oe_invoice_from_order',
 
 747     new_invoice_type => 'final_invoice',
 
 751 # workflow from sales order to sales quotation
 
 752 sub action_sales_quotation {
 
 753   $_[0]->workflow_sales_or_request_for_quotation();
 
 756 # workflow from sales order to sales quotation
 
 757 sub action_request_for_quotation {
 
 758   $_[0]->workflow_sales_or_request_for_quotation();
 
 761 # workflow from sales quotation to sales order
 
 762 sub action_sales_order {
 
 763   $_[0]->workflow_sales_or_purchase_order();
 
 766 # workflow from rfq to purchase order
 
 767 sub action_purchase_order {
 
 768   $_[0]->workflow_sales_or_purchase_order();
 
 771 # workflow from purchase order to ap transaction
 
 772 sub action_save_and_ap_transaction {
 
 775   $self->save_and_redirect_to(
 
 776     controller => 'ap.pl',
 
 777     action     => 'add_from_purchase_order',
 
 781 # set form elements in respect to a changed customer or vendor
 
 783 # This action is called on an change of the customer/vendor picker.
 
 784 sub action_customer_vendor_changed {
 
 787   setup_order_from_cv($self->order);
 
 790   my $cv_method = $self->cv;
 
 792   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 793     $self->js->show('#cp_row');
 
 795     $self->js->hide('#cp_row');
 
 798   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 799     $self->js->show('#shipto_selection');
 
 801     $self->js->hide('#shipto_selection');
 
 804   if ($cv_method eq 'customer') {
 
 805     my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
 
 806     $self->js->$show_hide('#billing_address_row');
 
 809   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 812     ->replaceWith('#order_cp_id',              $self->build_contact_select)
 
 813     ->replaceWith('#order_shipto_id',          $self->build_shipto_select)
 
 814     ->replaceWith('#shipto_inputs  ',          $self->build_shipto_inputs)
 
 815     ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
 
 816     ->replaceWith('#business_info_row',        $self->build_business_info_row)
 
 817     ->val(        '#order_taxzone_id',         $self->order->taxzone_id)
 
 818     ->val(        '#order_taxincluded',        $self->order->taxincluded)
 
 819     ->val(        '#order_currency_id',        $self->order->currency_id)
 
 820     ->val(        '#order_payment_id',         $self->order->payment_id)
 
 821     ->val(        '#order_delivery_term_id',   $self->order->delivery_term_id)
 
 822     ->val(        '#order_intnotes',           $self->order->intnotes)
 
 823     ->val(        '#order_language_id',        $self->order->$cv_method->language_id)
 
 824     ->focus(      '#order_' . $self->cv . '_id')
 
 825     ->run('kivi.Order.update_exchangerate');
 
 827   $self->js_redisplay_amounts_and_taxes;
 
 828   $self->js_redisplay_cvpartnumbers;
 
 832 # open the dialog for customer/vendor details
 
 833 sub action_show_customer_vendor_details_dialog {
 
 836   my $is_customer = 'customer' eq $::form->{vc};
 
 839     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 841     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 844   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 845   $details{discount_as_percent} = $cv->discount_as_percent;
 
 846   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 847   $details{business}            = $cv->business->description      if $cv->business;
 
 848   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 849   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 850   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 851   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 854     foreach my $entry (@{ $cv->additional_billing_addresses }) {
 
 855       push @{ $details{ADDITIONAL_BILLING_ADDRESSES} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 858   foreach my $entry (@{ $cv->shipto }) {
 
 859     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 861   foreach my $entry (@{ $cv->contacts }) {
 
 862     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 865   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 866                 is_customer => $is_customer,
 
 871 # called if a unit in an existing item row is changed
 
 872 sub action_unit_changed {
 
 875   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 876   my $item = $self->order->items_sorted->[$idx];
 
 878   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 879   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 884     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 885   $self->js_redisplay_line_values;
 
 886   $self->js_redisplay_amounts_and_taxes;
 
 890 # update item input row when a part ist picked
 
 891 sub action_update_item_input_row {
 
 894   delete $::form->{add_item}->{$_} for qw(create_part_type sellprice_as_number discount_as_percent);
 
 896   my $form_attr = $::form->{add_item};
 
 898   return unless $form_attr->{parts_id};
 
 900   my $record       = $self->order;
 
 901   my $item         = SL::DB::OrderItem->new(%$form_attr);
 
 902   my $part         = SL::DB::Part->new(id => $::form->{add_item}->{parts_id})->load;
 
 903   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
 905   $item->unit($part->unit);
 
 908   if ( $part->is_assortment ) {
 
 909     # add assortment items with price 0, as the components carry the price
 
 910     $price_src = $price_source->price_from_source("");
 
 911     $price_src->price(0);
 
 912   } elsif (defined $item->sellprice) {
 
 913     $price_src = $price_source->price_from_source("");
 
 914     $price_src->price($item->sellprice);
 
 916     $price_src = $price_source->best_price
 
 917                ? $price_source->best_price
 
 918                : $price_source->price_from_source("");
 
 919     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
 920     $price_src->price(0) if !$price_source->best_price;
 
 924   if (defined $item->discount) {
 
 925     $discount_src = $price_source->discount_from_source("");
 
 926     $discount_src->discount($item->discount);
 
 928     $discount_src = $price_source->best_discount
 
 929                   ? $price_source->best_discount
 
 930                   : $price_source->discount_from_source("");
 
 931     $discount_src->discount(0) if !$price_source->best_discount;
 
 935     ->val     ('#add_item_unit',                $item->unit)
 
 936     ->val     ('#add_item_description',         $part->description)
 
 937     ->val     ('#add_item_sellprice_as_number', '')
 
 938     ->attr    ('#add_item_sellprice_as_number', 'placeholder', $price_src->price_as_number)
 
 939     ->attr    ('#add_item_sellprice_as_number', 'title',       $price_src->source_description)
 
 940     ->val     ('#add_item_discount_as_percent', '')
 
 941     ->attr    ('#add_item_discount_as_percent', 'placeholder', $discount_src->discount_as_percent)
 
 942     ->attr    ('#add_item_discount_as_percent', 'title',       $discount_src->source_description)
 
 946 # add an item row for a new item entered in the input row
 
 947 sub action_add_item {
 
 950   delete $::form->{add_item}->{create_part_type};
 
 952   my $form_attr = $::form->{add_item};
 
 954   return unless $form_attr->{parts_id};
 
 956   my $item = new_item($self->order, $form_attr);
 
 958   $self->order->add_items($item);
 
 962   $self->get_item_cvpartnumber($item);
 
 964   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 965   my $row_as_html = $self->p->render('order/tabs/_row',
 
 971   if ($::form->{insert_before_item_id}) {
 
 973       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 976       ->append('#row_table_id', $row_as_html);
 
 979   if ( $item->part->is_assortment ) {
 
 980     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 981     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 982       my $attr = { parts_id => $assortment_item->parts_id,
 
 983                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 984                    unit     => $assortment_item->unit,
 
 985                    description => $assortment_item->part->description,
 
 987       my $item = new_item($self->order, $attr);
 
 989       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 990       $item->discount(1) unless $assortment_item->charge;
 
 992       $self->order->add_items( $item );
 
 994       $self->get_item_cvpartnumber($item);
 
 995       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 996       my $row_as_html = $self->p->render('order/tabs/_row',
 
1001       if ($::form->{insert_before_item_id}) {
 
1003           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
1006           ->append('#row_table_id', $row_as_html);
 
1012     ->val('.add_item_input', '')
 
1013     ->attr('.add_item_input', 'placeholder', '')
 
1014     ->attr('.add_item_input', 'title', '')
 
1015     ->run('kivi.Order.init_row_handlers')
 
1016     ->run('kivi.Order.renumber_positions')
 
1017     ->focus('#add_item_parts_id_name');
 
1019   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
1021   $self->js_redisplay_amounts_and_taxes;
 
1022   $self->js->render();
 
1025 # add item rows for multiple items at once
 
1026 sub action_add_multi_items {
 
1029   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
 
1030   return $self->js->render() unless scalar @form_attr;
 
1033   foreach my $attr (@form_attr) {
 
1034     my $item = new_item($self->order, $attr);
 
1036     if ( $item->part->is_assortment ) {
 
1037       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
1038         my $attr = { parts_id => $assortment_item->parts_id,
 
1039                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
1040                      unit     => $assortment_item->unit,
 
1041                      description => $assortment_item->part->description,
 
1043         my $item = new_item($self->order, $attr);
 
1045         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
1046         $item->discount(1) unless $assortment_item->charge;
 
1051   $self->order->add_items(@items);
 
1055   foreach my $item (@items) {
 
1056     $self->get_item_cvpartnumber($item);
 
1057     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1058     my $row_as_html = $self->p->render('order/tabs/_row',
 
1064     if ($::form->{insert_before_item_id}) {
 
1066         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
1069         ->append('#row_table_id', $row_as_html);
 
1074     ->run('kivi.Part.close_picker_dialogs')
 
1075     ->run('kivi.Order.init_row_handlers')
 
1076     ->run('kivi.Order.renumber_positions')
 
1077     ->focus('#add_item_parts_id_name');
 
1079   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
1081   $self->js_redisplay_amounts_and_taxes;
 
1082   $self->js->render();
 
1085 # recalculate all linetotals, amounts and taxes and redisplay them
 
1086 sub action_recalc_amounts_and_taxes {
 
1091   $self->js_redisplay_line_values;
 
1092   $self->js_redisplay_amounts_and_taxes;
 
1093   $self->js->render();
 
1096 sub action_update_exchangerate {
 
1100     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
1101     currency_name => $self->order->currency->name,
 
1102     exchangerate  => $self->order->daily_exchangerate_as_null_number,
 
1105   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
1108 # redisplay item rows if they are sorted by an attribute
 
1109 sub action_reorder_items {
 
1113     partnumber   => sub { $_[0]->part->partnumber },
 
1114     description  => sub { $_[0]->description },
 
1115     qty          => sub { $_[0]->qty },
 
1116     sellprice    => sub { $_[0]->sellprice },
 
1117     discount     => sub { $_[0]->discount },
 
1118     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
1121   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1123   my $method = $sort_keys{$::form->{order_by}};
 
1124   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
1125   if ($::form->{sort_dir}) {
 
1126     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1127       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
1129       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
1132     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1133       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
1135       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
1139     ->run('kivi.Order.redisplay_items', \@to_sort)
 
1143 # show the popup to choose a price/discount source
 
1144 sub action_price_popup {
 
1147   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
1148   my $item = $self->order->items_sorted->[$idx];
 
1150   $self->render_price_dialog($item);
 
1153 # save the order in a session variable and redirect to the part controller
 
1154 sub action_create_part {
 
1157   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
 
1159   my $callback     = $self->url_for(
 
1160     action       => 'return_from_create_part',
 
1161     type         => $self->type, # type is needed for check_auth on return
 
1162     previousform => $previousform,
 
1165   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.'));
 
1167   my @redirect_params = (
 
1168     controller    => 'Part',
 
1170     part_type     => $::form->{add_item}->{create_part_type},
 
1171     callback      => $callback,
 
1175   $self->redirect_to(@redirect_params);
 
1178 sub action_return_from_create_part {
 
1181   $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
 
1183   $::auth->restore_form_from_session(delete $::form->{previousform});
 
1185   # set item ids to new fake id, to identify them as new items
 
1186   foreach my $item (@{$self->order->items_sorted}) {
 
1187     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1191   $self->get_unalterable_data();
 
1192   $self->pre_render();
 
1194   # trigger rendering values for second row/longdescription as hidden,
 
1195   # because they are loaded only on demand. So we need to keep the values
 
1197   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1198   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1202     title => $self->get_title_for('edit'),
 
1203     %{$self->{template_args}}
 
1208 # load the second row for one or more items
 
1210 # This action gets the html code for all items second rows by rendering a template for
 
1211 # the second row and sets the html code via client js.
 
1212 sub action_load_second_rows {
 
1215   $self->recalc() if $self->order->is_sales; # for margin calculation
 
1217   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1218     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1219     my $item = $self->order->items_sorted->[$idx];
 
1221     $self->js_load_second_row($item, $item_id, 0);
 
1224   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
1226   $self->js->render();
 
1229 # update description, notes and sellprice from master data
 
1230 sub action_update_row_from_master_data {
 
1233   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1234     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1235     my $item  = $self->order->items_sorted->[$idx];
 
1236     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1238     $item->description($texts->{description});
 
1239     $item->longdescription($texts->{longdescription});
 
1241     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1244     if ($item->part->is_assortment) {
 
1245     # add assortment items with price 0, as the components carry the price
 
1246       $price_src = $price_source->price_from_source("");
 
1247       $price_src->price(0);
 
1249       $price_src = $price_source->best_price
 
1250                  ? $price_source->best_price
 
1251                  : $price_source->price_from_source("");
 
1252       $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
 
1253       $price_src->price(0) if !$price_source->best_price;
 
1257     $discount_src = $price_source->best_discount
 
1258                   ? $price_source->best_discount
 
1259                   : $price_source->discount_from_source("");
 
1260     $discount_src->discount(0) if !$price_source->best_discount;
 
1262     $item->sellprice($price_src->price);
 
1263     $item->active_price_source($price_src);
 
1264     $item->discount($discount_src->discount);
 
1265     $item->active_discount_source($discount_src);
 
1267     my $price_editable = $self->order->is_sales ? $::auth->assert('sales_edit_prices', 1) : $::auth->assert('purchase_edit_prices', 1);
 
1270       ->run('kivi.Order.set_price_and_source_text',    $item_id, $price_src   ->source, $price_src   ->source_description, $item->sellprice_as_number, $price_editable)
 
1271       ->run('kivi.Order.set_discount_and_source_text', $item_id, $discount_src->source, $discount_src->source_description, $item->discount_as_percent, $price_editable)
 
1272       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1273       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1274       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1276     if ($self->search_cvpartnumber) {
 
1277       $self->get_item_cvpartnumber($item);
 
1278       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1283   $self->js_redisplay_line_values;
 
1284   $self->js_redisplay_amounts_and_taxes;
 
1286   $self->js->render();
 
1289 sub js_load_second_row {
 
1290   my ($self, $item, $item_id, $do_parse) = @_;
 
1293     # Parse values from form (they are formated while rendering (template)).
 
1294     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1295     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1296     foreach my $var (@{ $item->cvars_by_config }) {
 
1297       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1299     $item->parse_custom_variable_values;
 
1302   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1305     ->html('#second_row_' . $item_id, $row_as_html)
 
1306     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1309 sub js_redisplay_line_values {
 
1312   my $is_sales = $self->order->is_sales;
 
1314   # sales orders with margins
 
1319        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1320        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1321        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1322       ]} @{ $self->order->items_sorted };
 
1326        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1327       ]} @{ $self->order->items_sorted };
 
1331     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1334 sub js_redisplay_amounts_and_taxes {
 
1337   if (scalar @{ $self->{taxes} }) {
 
1338     $self->js->show('#taxincluded_row_id');
 
1340     $self->js->hide('#taxincluded_row_id');
 
1343   if ($self->order->taxincluded) {
 
1344     $self->js->hide('#subtotal_row_id');
 
1346     $self->js->show('#subtotal_row_id');
 
1349   if ($self->order->is_sales) {
 
1350     my $is_neg = $self->order->marge_total < 0;
 
1352       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1353       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1354       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1355       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1356       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1357       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1358       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1359       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1363     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1364     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1365     ->remove('.tax_row')
 
1366     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1369 sub js_redisplay_cvpartnumbers {
 
1372   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1374   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1377     ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
 
1380 sub js_reset_order_and_item_ids_after_save {
 
1384     ->val('#id', $self->order->id)
 
1385     ->val('#converted_from_oe_id', '')
 
1386     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1389   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1390     next if !$self->order->items_sorted->[$idx]->id;
 
1391     next if $form_item_id !~ m{^new};
 
1393       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1394       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1395       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1399   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1406 sub init_valid_types {
 
1407   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1413   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1414     die "Not a valid type for order";
 
1417   $self->type($::form->{type});
 
1423   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1424          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1425          : die "Not a valid type for order";
 
1430 sub init_search_cvpartnumber {
 
1433   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1434   my $search_cvpartnumber;
 
1435   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1436   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1438   return $search_cvpartnumber;
 
1441 sub init_show_update_button {
 
1444   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1455 sub init_all_price_factors {
 
1456   SL::DB::Manager::PriceFactor->get_all;
 
1459 sub init_part_picker_classification_ids {
 
1461   my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
 
1463   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
 
1469   my $right_for = { map { $_ => $_.'_edit' . ' | ' . $_.'_view' } @{$self->valid_types} };
 
1471   my $right   = $right_for->{ $self->type };
 
1472   $right    ||= 'DOES_NOT_EXIST';
 
1474   $::auth->assert($right);
 
1477 sub check_auth_for_edit {
 
1480   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1482   my $right   = $right_for->{ $self->type };
 
1483   $right    ||= 'DOES_NOT_EXIST';
 
1485   $::auth->assert($right);
 
1488 # build the selection box for contacts
 
1490 # Needed, if customer/vendor changed.
 
1491 sub build_contact_select {
 
1494   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1495     value_key  => 'cp_id',
 
1496     title_key  => 'full_name_dep',
 
1497     default    => $self->order->cp_id,
 
1499     style      => 'width: 300px',
 
1503 # build the selection box for the additional billing address
 
1505 # Needed, if customer/vendor changed.
 
1506 sub build_billing_address_select {
 
1509   return '' if $self->cv ne 'customer';
 
1511   select_tag('order.billing_address_id',
 
1512              [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
 
1514              title_key  => 'displayable_id',
 
1515              default    => $self->order->billing_address_id,
 
1517              style      => 'width: 300px',
 
1521 # build the selection box for shiptos
 
1523 # Needed, if customer/vendor changed.
 
1524 sub build_shipto_select {
 
1527   select_tag('order.shipto_id',
 
1528              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1529              value_key  => 'shipto_id',
 
1530              title_key  => 'displayable_id',
 
1531              default    => $self->order->shipto_id,
 
1533              style      => 'width: 300px',
 
1537 # build the inputs for the cusom shipto dialog
 
1539 # Needed, if customer/vendor changed.
 
1540 sub build_shipto_inputs {
 
1543   my $content = $self->p->render('common/_ship_to_dialog',
 
1544                                  vc_obj      => $self->order->customervendor,
 
1545                                  cs_obj      => $self->order->custom_shipto,
 
1546                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1547                                  id_selector => '#order_shipto_id');
 
1549   div_tag($content, id => 'shipto_inputs');
 
1552 # render the info line for business
 
1554 # Needed, if customer/vendor changed.
 
1555 sub build_business_info_row
 
1557   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1560 # build the rows for displaying taxes
 
1562 # Called if amounts where recalculated and redisplayed.
 
1563 sub build_tax_rows {
 
1567   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1568     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1570   return $rows_as_html;
 
1574 sub render_price_dialog {
 
1575   my ($self, $record_item) = @_;
 
1577   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1581       'kivi.io.price_chooser_dialog',
 
1582       t8('Available Prices'),
 
1583       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1588 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1589 #     $self->js->show('#dialog_flash_error');
 
1598   return if !$::form->{id};
 
1600   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1602   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1603   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1604   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1606   return $self->order;
 
1609 # load or create a new order object
 
1611 # And assign changes from the form to this object.
 
1612 # If the order is loaded from db, check if items are deleted in the form,
 
1613 # remove them form the object and collect them for removing from db on saving.
 
1614 # Then create/update items from form (via make_item) and add them.
 
1618   # add_items adds items to an order with no items for saving, but they cannot
 
1619   # be retrieved via items until the order is saved. Adding empty items to new
 
1620   # order here solves this problem.
 
1622   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1623   $order ||= SL::DB::Order->new(orderitems  => [],
 
1624                                 quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
 
1625                                 currency_id => $::instance_conf->get_currency_id(),);
 
1627   my $cv_id_method = $self->cv . '_id';
 
1628   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1629     $order->$cv_id_method($::form->{$cv_id_method});
 
1630     setup_order_from_cv($order);
 
1633   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1634   my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
 
1636   $order->assign_attributes(%{$::form->{order}});
 
1638   $self->setup_custom_shipto_from_form($order, $::form);
 
1640   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1641     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1642     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1645   # remove deleted items
 
1646   $self->item_ids_to_delete([]);
 
1647   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1648     my $item = $order->orderitems->[$idx];
 
1649     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1650       splice @{$order->orderitems}, $idx, 1;
 
1651       push @{$self->item_ids_to_delete}, $item->id;
 
1657   foreach my $form_attr (@{$form_orderitems}) {
 
1658     my $item = make_item($order, $form_attr);
 
1659     $item->position($pos);
 
1663   $order->add_items(grep {!$_->id} @items);
 
1668 # create or update items from form
 
1670 # Make item objects from form values. For items already existing read from db.
 
1671 # Create a new item else. And assign attributes.
 
1673   my ($record, $attr) = @_;
 
1676   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1678   my $is_new = !$item;
 
1680   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1681   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1682   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1683   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1685   $item->assign_attributes(%$attr);
 
1688     my $texts = get_part_texts($item->part, $record->language_id);
 
1689     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1690     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1691     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1699 # This is used to add one item
 
1701   my ($record, $attr) = @_;
 
1703   my $item = SL::DB::OrderItem->new;
 
1705   # Remove attributes where the user left or set the inputs empty.
 
1706   # So these attributes will be undefined and we can distinguish them
 
1707   # from zero later on.
 
1708   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1709     delete $attr->{$_} if $attr->{$_} eq '';
 
1712   $item->assign_attributes(%$attr);
 
1714   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1715   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1717   $item->qty(1.0)          if !$item->qty;
 
1718   $item->unit($part->unit) if !$item->unit;
 
1721   if ( $part->is_assortment ) {
 
1722     # add assortment items with price 0, as the components carry the price
 
1723     $price_src = $price_source->price_from_source("");
 
1724     $price_src->price(0);
 
1725   } elsif (defined $item->sellprice) {
 
1726     $price_src = $price_source->price_from_source("");
 
1727     $price_src->price($item->sellprice);
 
1729     $price_src = $price_source->best_price
 
1730                ? $price_source->best_price
 
1731                : $price_source->price_from_source("");
 
1732     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
1733     $price_src->price(0) if !$price_source->best_price;
 
1737   if (defined $item->discount) {
 
1738     $discount_src = $price_source->discount_from_source("");
 
1739     $discount_src->discount($item->discount);
 
1741     $discount_src = $price_source->best_discount
 
1742                   ? $price_source->best_discount
 
1743                   : $price_source->discount_from_source("");
 
1744     $discount_src->discount(0) if !$price_source->best_discount;
 
1748   $new_attr{part}                   = $part;
 
1749   $new_attr{description}            = $part->description     if ! $item->description;
 
1750   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1751   $new_attr{sellprice}              = $price_src->price;
 
1752   $new_attr{discount}               = $discount_src->discount;
 
1753   $new_attr{active_price_source}    = $price_src;
 
1754   $new_attr{active_discount_source} = $discount_src;
 
1755   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1756   $new_attr{project_id}             = $record->globalproject_id;
 
1757   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1759   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1760   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1761   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1762   $new_attr{custom_variables} = [];
 
1764   my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1766   $item->assign_attributes(%new_attr, %{ $texts });
 
1771 sub setup_order_from_cv {
 
1774   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id language_id));
 
1776   $order->intnotes($order->customervendor->notes);
 
1778   return if !$order->is_sales;
 
1780   $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1781   $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1782                       ? $order->customer->taxincluded_checked
 
1783                       : $::myconfig{taxincluded_checked});
 
1785   my $address = $order->customer->default_billing_address;;
 
1786   $order->billing_address_id($address ? $address->id : undef);
 
1789 # setup custom shipto from form
 
1791 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1792 # with 'shiptocvar_'.
 
1793 # Mark it to be deleted if a shipto from master data is selected
 
1794 # (i.e. order has a shipto).
 
1795 # Else, update or create a new custom shipto. If the fields are empty, it
 
1796 # will not be saved on save.
 
1797 sub setup_custom_shipto_from_form {
 
1798   my ($self, $order, $form) = @_;
 
1800   if ($order->shipto) {
 
1801     $self->is_custom_shipto_to_delete(1);
 
1803     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1805     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1806     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1808     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1809     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1813 # recalculate prices and taxes
 
1815 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1819   my %pat = $self->order->calculate_prices_and_taxes();
 
1821   $self->{taxes} = [];
 
1822   foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
 
1823     my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
1825     push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
 
1826                                 netamount => $netamount,
 
1827                                 tax       => SL::DB::Tax->new(id => $tax_id)->load });
 
1829   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1832 # get data for saving, printing, ..., that is not changed in the form
 
1834 # Only cvars for now.
 
1835 sub get_unalterable_data {
 
1838   foreach my $item (@{ $self->order->items }) {
 
1839     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1840     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1841     foreach my $var (@{ $item->cvars_by_config }) {
 
1842       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1844     $item->parse_custom_variable_values;
 
1850 # And remove related files in the spool directory
 
1855   my $db     = $self->order->db;
 
1857   $db->with_transaction(
 
1859       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1860       $self->order->delete;
 
1861       my $spool = $::lx_office_conf{paths}->{spool};
 
1862       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1864       $self->save_history('DELETED');
 
1867   }) || push(@{$errors}, $db->error);
 
1874 # And delete items that are deleted in the form.
 
1879   my $db     = $self->order->db;
 
1881   $db->with_transaction(sub {
 
1882     # delete custom shipto if it is to be deleted or if it is empty
 
1883     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1884       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1885       $self->order->custom_shipto(undef);
 
1888     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1889     $self->order->save(cascade => 1);
 
1892     if ($::form->{converted_from_oe_id}) {
 
1893       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1895       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1896         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1897         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1898         $src->link_to_record($self->order);
 
1900       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1902         foreach (@{ $self->order->items_sorted }) {
 
1903           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1905           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1906                                   from_id    => $from_id,
 
1907                                   to_table   => 'orderitems',
 
1914       $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
 
1917     $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
 
1919     $self->save_history('SAVED');
 
1922   }) || push(@{$errors}, $db->error);
 
1927 sub workflow_sales_or_request_for_quotation {
 
1931   my $errors = $self->save();
 
1933   if (scalar @{ $errors }) {
 
1934     $self->js->flash('error', $_) for @{ $errors };
 
1935     return $self->js->render();
 
1938   my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
 
1940   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1941   delete $::form->{id};
 
1943   # no linked records from order to quotations
 
1944   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
1946   # set item ids to new fake id, to identify them as new items
 
1947   foreach my $item (@{$self->order->items_sorted}) {
 
1948     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1952   $::form->{type} = $destination_type;
 
1953   $self->type($self->init_type);
 
1954   $self->cv  ($self->init_cv);
 
1958   $self->get_unalterable_data();
 
1959   $self->pre_render();
 
1961   # trigger rendering values for second row as hidden, because they
 
1962   # are loaded only on demand. So we need to keep the values from the
 
1964   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1968     title => $self->get_title_for('edit'),
 
1969     %{$self->{template_args}}
 
1973 sub workflow_sales_or_purchase_order {
 
1977   my $errors = $self->save();
 
1979   if (scalar @{ $errors }) {
 
1980     $self->js->flash('error', $_) foreach @{ $errors };
 
1981     return $self->js->render();
 
1984   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1985                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1986                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1987                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1990   # check for direct delivery
 
1991   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1993   if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
 
1994       && $::form->{use_shipto} && $self->order->shipto) {
 
1995     $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
 
1998   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1999   $self->{converted_from_oe_id} = delete $::form->{id};
 
2001   # set item ids to new fake id, to identify them as new items
 
2002   foreach my $item (@{$self->order->items_sorted}) {
 
2003     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
2006   if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
 
2007     if ($::form->{use_shipto}) {
 
2008       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
2010       # remove any custom shipto if not wanted
 
2011       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
2016   $::form->{type} = $destination_type;
 
2017   $self->type($self->init_type);
 
2018   $self->cv  ($self->init_cv);
 
2022   $self->get_unalterable_data();
 
2023   $self->pre_render();
 
2025   # trigger rendering values for second row as hidden, because they
 
2026   # are loaded only on demand. So we need to keep the values from the
 
2028   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
2032     title => $self->get_title_for('edit'),
 
2033     %{$self->{template_args}}
 
2041   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
2042   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
2043   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
2044   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
 
2045   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
2048   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
2051   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
2053   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
2054   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
2055   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
2056   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
2057   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
2059   my $print_form = Form->new('');
 
2060   $print_form->{type}        = $self->type;
 
2061   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
2062   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
2063     form => $print_form,
 
2064     options => {dialog_name_prefix => 'print_options.',
 
2068                 no_opendocument    => 0,
 
2072   foreach my $item (@{$self->order->orderitems}) {
 
2073     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
2074     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
2075     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
2078   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
2079     # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
 
2080     # Do not use write_to_objects to prevent order->delivered to be set, because this should be
 
2081     # the value from db, which can be set manually or is set when linked delivery orders are saved.
 
2082     SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
 
2085   if ($self->order->number && $::instance_conf->get_webdav) {
 
2086     my $webdav = SL::Webdav->new(
 
2087       type     => $self->type,
 
2088       number   => $self->order->number,
 
2090     my @all_objects = $webdav->get_all_objects;
 
2091     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
2093                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
2097   if (   (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
 
2098       && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
 
2099     $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
 
2101   $self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
 
2103   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
2105   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
2106                                                          edit_periodic_invoices_config calculate_qty follow_up show_history);
 
2107   $self->setup_edit_action_bar;
 
2110 sub setup_edit_action_bar {
 
2111   my ($self, %params) = @_;
 
2113   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
2114                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
2115                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
2117   my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
 
2118   my @req_cusordnumber   = qw(kivi.Order.check_cusordnumber_presence)           x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
 
2120   my $has_invoice_for_advance_payment;
 
2121   if ($self->order->id && $self->type eq sales_order_type()) {
 
2122     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2123     $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
 
2126   my $has_final_invoice;
 
2127   if ($self->order->id && $self->type eq sales_order_type()) {
 
2128     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2129     $has_final_invoice               = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
 
2132   my $right_for         = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
2133   my $right             = $right_for->{ $self->type };
 
2134   $right              ||= 'DOES_NOT_EXIST';
 
2135   my $may_edit_create   = $::auth->assert($right, 'may fail');
 
2137   for my $bar ($::request->layout->get('actionbar')) {
 
2142           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
2143                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
2145           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
 
2146                          @req_trans_cost_art, @req_cusordnumber,
 
2148           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2151           t8('Save and Close'),
 
2152           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
2153                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
2156           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
 
2157                          @req_trans_cost_art, @req_cusordnumber,
 
2159           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2163           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
2164           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2165                          @req_trans_cost_art, @req_cusordnumber,
 
2167           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.')
 
2168                      : !$self->order->id ? t8('This object has not been saved yet.')
 
2171       ], # end of combobox "Save"
 
2178           t8('Save and Quotation'),
 
2179           submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
 
2180           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2181           only_if  => (any { $self->type eq $_ } (sales_order_type())),
 
2182           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2186           submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
 
2187           only_if  => (any { $self->type eq $_ } (purchase_order_type())),
 
2188           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2191           t8('Save and Sales Order'),
 
2192           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
2193           checks   => [ @req_trans_cost_art ],
 
2194           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
2195           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2198           t8('Save and Purchase Order'),
 
2199           call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
 
2200           checks    => [ @req_trans_cost_art, @req_cusordnumber ],
 
2201           only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
2202           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2205           t8('Save and Delivery Order'),
 
2206           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2207                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2209           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2210                          @req_trans_cost_art, @req_cusordnumber,
 
2212           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())),
 
2213           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2216           t8('Save and Supplier Delivery Order'),
 
2217           call      => [ 'kivi.Order.save', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2218                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2220           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2221                          @req_trans_cost_art, @req_cusordnumber,
 
2223           only_if   => (any { $self->type eq $_ } (purchase_order_type())),
 
2224           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2227           t8('Save and Invoice'),
 
2228           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2229           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2230                          @req_trans_cost_art, @req_cusordnumber,
 
2232           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2235           ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
 
2236           call      => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::instance_conf->get_order_warn_duplicate_parts ],
 
2237           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2238                          @req_trans_cost_art, @req_cusordnumber,
 
2240           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2241                      : $has_final_invoice ? t8('This order has already a final invoice.')
 
2243           only_if   => (any { $self->type eq $_ } (sales_order_type())),
 
2246           t8('Save and Final Invoice'),
 
2247           call      => [ 'kivi.Order.save', 'save_and_final_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2248           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2249                          @req_trans_cost_art, @req_cusordnumber,
 
2251           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2252                      : $has_final_invoice ? t8('This order has already a final invoice.')
 
2254           only_if   => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
 
2257           t8('Save and AP Transaction'),
 
2258           call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
2259           only_if   => (any { $self->type eq $_ } (purchase_order_type())),
 
2260           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2263       ], # end of combobox "Workflow"
 
2270           t8('Save and preview PDF'),
 
2271           call     => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
2272                                                           $::instance_conf->get_order_warn_no_deliverydate,
 
2274           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2275           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2278           t8('Save and print'),
 
2279           call     => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
2280                                                          $::instance_conf->get_order_warn_no_deliverydate,
 
2282           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2283           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2286           t8('Save and E-mail'),
 
2287           id       => 'save_and_email_action',
 
2288           call     => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
2289                                                                          $::instance_conf->get_order_warn_no_deliverydate,
 
2291           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2292                     : !$self->order->id  ? t8('This object has not been saved yet.')
 
2296           t8('Download attachments of all parts'),
 
2297           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
2298           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2299           only_if  => $::instance_conf->get_doc_storage,
 
2301       ], # end of combobox "Export"
 
2305         call     => [ 'kivi.Order.delete_order' ],
 
2306         confirm  => $::locale->text('Do you really want to delete this object?'),
 
2307         disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2308                   : !$self->order->id  ? t8('This object has not been saved yet.')
 
2310         only_if  => $deletion_allowed,
 
2319           call     => [ 'set_history_window', $self->order->id, 'id' ],
 
2320           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
 
2324           call     => [ 'kivi.Order.follow_up_window' ],
 
2325           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2326           only_if  => $::auth->assert('productivity', 1),
 
2328       ], # end of combobox "more"
 
2334   my ($self, $doc_ref, $params) = @_;
 
2336   my $order  = $self->order;
 
2339   my $print_form = Form->new('');
 
2340   $print_form->{type}        = $order->type;
 
2341   $print_form->{formname}    = $params->{formname} || $order->type;
 
2342   $print_form->{format}      = $params->{format}   || 'pdf';
 
2343   $print_form->{media}       = $params->{media}    || 'file';
 
2344   $print_form->{groupitems}  = $params->{groupitems};
 
2345   $print_form->{printer_id}  = $params->{printer_id};
 
2346   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
2348   $order->language($params->{language});
 
2349   $order->flatten_to_form($print_form, format_amounts => 1);
 
2353   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
2354     $template_ext  = 'odt';
 
2355     $template_type = 'OpenDocument';
 
2356   } elsif ($print_form->{format} =~ m{html}i) {
 
2357     $template_ext  = 'html';
 
2358     $template_type = 'HTML';
 
2361   # search for the template
 
2362   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
2363     name        => $print_form->{formname},
 
2364     extension   => $template_ext,
 
2365     email       => $print_form->{media} eq 'email',
 
2366     language    => $params->{language},
 
2367     printer_id  => $print_form->{printer_id},
 
2370   if (!defined $template_file) {
 
2371     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);
 
2374   return @errors if scalar @errors;
 
2376   $print_form->throw_on_error(sub {
 
2378       $print_form->prepare_for_printing;
 
2380       $$doc_ref = SL::Helper::CreatePDF->create_pdf(
 
2381         format        => $print_form->{format},
 
2382         template_type => $template_type,
 
2383         template      => $template_file,
 
2384         variables     => $print_form,
 
2385         variable_content_types => {
 
2386           longdescription => 'html',
 
2387           partnotes       => 'html',
 
2389           $::form->get_variable_content_types_for_cvars,
 
2393     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2399 sub get_files_for_email_dialog {
 
2402   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2404   return %files if !$::instance_conf->get_doc_storage;
 
2406   if ($self->order->id) {
 
2407     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2408     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2409     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2410     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2414     uniq_by { $_->{id} }
 
2416       +{ id         => $_->part->id,
 
2417          partnumber => $_->part->partnumber }
 
2418     } @{$self->order->items_sorted};
 
2420   foreach my $part (@parts) {
 
2421     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2422     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2425   foreach my $key (keys %files) {
 
2426     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2432 sub make_periodic_invoices_config_from_yaml {
 
2433   my ($yaml_config) = @_;
 
2435   return if !$yaml_config;
 
2436   my $attr = SL::YAML::Load($yaml_config);
 
2437   return if 'HASH' ne ref $attr;
 
2438   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
2442 sub get_periodic_invoices_status {
 
2443   my ($self, $config) = @_;
 
2445   return                      if $self->type ne sales_order_type();
 
2446   return t8('not configured') if !$config;
 
2448   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
2449              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
2450              :                                                     die "Cannot get status of periodic invoices config";
 
2452   return $active ? t8('active') : t8('inactive');
 
2456   my ($self, $action) = @_;
 
2458   return '' if none { lc($action)} qw(add edit);
 
2461   # $::locale->text("Add Sales Order");
 
2462   # $::locale->text("Add Purchase Order");
 
2463   # $::locale->text("Add Quotation");
 
2464   # $::locale->text("Add Request for Quotation");
 
2465   # $::locale->text("Edit Sales Order");
 
2466   # $::locale->text("Edit Purchase Order");
 
2467   # $::locale->text("Edit Quotation");
 
2468   # $::locale->text("Edit Request for Quotation");
 
2470   $action = ucfirst(lc($action));
 
2471   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
2472        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
2473        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
2474        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
2478 sub get_item_cvpartnumber {
 
2479   my ($self, $item) = @_;
 
2481   return if !$self->search_cvpartnumber;
 
2482   return if !$self->order->customervendor;
 
2484   if ($self->cv eq 'vendor') {
 
2485     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2486     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2487   } elsif ($self->cv eq 'customer') {
 
2488     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2489     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2493 sub get_part_texts {
 
2494   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2496   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2497   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2499     description     => $defaults{description}     // $part->description,
 
2500     longdescription => $defaults{longdescription} // $part->notes,
 
2503   return $texts unless $language_id;
 
2505   my $translation = SL::DB::Manager::Translation->get_first(
 
2507       parts_id    => $part->id,
 
2508       language_id => $language_id,
 
2511   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2512   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2517 sub sales_order_type {
 
2521 sub purchase_order_type {
 
2525 sub sales_quotation_type {
 
2529 sub request_quotation_type {
 
2530   'request_quotation';
 
2534   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
2535        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
2536        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
2537        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
2541 sub save_and_redirect_to {
 
2542   my ($self, %params) = @_;
 
2544   my $errors = $self->save();
 
2546   if (scalar @{ $errors }) {
 
2547     $self->js->flash('error', $_) foreach @{ $errors };
 
2548     return $self->js->render();
 
2551   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
2552            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
2553            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
2554            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
2556   flash_later('info', $text);
 
2558   $self->redirect_to(%params, id => $self->order->id);
 
2562   my ($self, $addition) = @_;
 
2564   my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
 
2565   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2567   SL::DB::History->new(
 
2568     trans_id    => $self->order->id,
 
2569     employee_id => SL::DB::Manager::Employee->current->id,
 
2570     what_done   => $self->order->type,
 
2571     snumbers    => $snumbers,
 
2572     addition    => $addition,
 
2576 sub store_doc_to_webdav_and_filemanagement {
 
2577   my ($self, $content, $filename, $variant) = @_;
 
2579   my $order = $self->order;
 
2582   # copy file to webdav folder
 
2583   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2584     my $webdav = SL::Webdav->new(
 
2585       type     => $order->type,
 
2586       number   => $order->number,
 
2588     my $webdav_file = SL::Webdav::File->new(
 
2590       filename => $filename,
 
2593       $webdav_file->store(data => \$content);
 
2596       push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
 
2599   if ($order->id && $::instance_conf->get_doc_storage) {
 
2601       SL::File->save(object_id     => $order->id,
 
2602                      object_type   => $order->type,
 
2603                      mime_type     => SL::MIME->mime_type_from_ext($filename),
 
2604                      source        => 'created',
 
2605                      file_type     => 'document',
 
2606                      file_name     => $filename,
 
2607                      file_contents => $content,
 
2608                      print_variant => $variant);
 
2611       push @errors, t8('Storing the document in the storage backend failed: #1', $@);
 
2618 sub link_requirement_specs_linking_to_created_from_objects {
 
2619   my ($self, @converted_from_oe_ids) = @_;
 
2621   return unless @converted_from_oe_ids;
 
2623   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
 
2624   foreach my $rs_order (@{ $rs_orders }) {
 
2625     SL::DB::RequirementSpecOrder->new(
 
2626       order_id            => $self->order->id,
 
2627       requirement_spec_id => $rs_order->requirement_spec_id,
 
2628       version_id          => $rs_order->version_id,
 
2633 sub set_project_in_linked_requirement_specs {
 
2636   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
 
2637   foreach my $rs_order (@{ $rs_orders }) {
 
2638     next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
 
2640     $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
 
2652 SL::Controller::Order - controller for orders
 
2656 This is a new form to enter orders, completely rewritten with the use
 
2657 of controller and java script techniques.
 
2659 The aim is to provide the user a better experience and a faster workflow. Also
 
2660 the code should be more readable, more reliable and better to maintain.
 
2668 One input row, so that input happens every time at the same place.
 
2672 Use of pickers where possible.
 
2676 Possibility to enter more than one item at once.
 
2680 Item list in a scrollable area, so that the workflow buttons stay at
 
2685 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2686 possible (by partnumber, description, qty, sellprice and discount for now).
 
2690 No C<update> is necessary. All entries and calculations are managed
 
2691 with ajax-calls and the page only reloads on C<save>.
 
2695 User can see changes immediately, because of the use of java script
 
2706 =item * C<SL/Controller/Order.pm>
 
2710 =item * C<template/webpages/order/form.html>
 
2714 =item * C<template/webpages/order/tabs/basic_data.html>
 
2716 Main tab for basic_data.
 
2718 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2719 reused from generic code.
 
2723 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
2725 For displaying information on business type
 
2727 =item * C<template/webpages/order/tabs/_item_input.html>
 
2729 The input line for items
 
2731 =item * C<template/webpages/order/tabs/_row.html>
 
2733 One row for already entered items
 
2735 =item * C<template/webpages/order/tabs/_tax_row.html>
 
2737 Displaying tax information
 
2739 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
2741 Dialog for selecting price and discount sources
 
2745 =item * C<js/kivi.Order.js>
 
2747 java script functions
 
2757 =item * price sources: little symbols showing better price / better discount
 
2759 =item * select units in input row?
 
2761 =item * check for direct delivery (workflow sales order -> purchase order)
 
2763 =item * access rights
 
2765 =item * display weights
 
2769 =item * optional client/user behaviour
 
2771 (transactions has to be set - department has to be set -
 
2772  force project if enabled in client config)
 
2776 =head1 KNOWN BUGS AND CAVEATS
 
2782 No indication that <shift>-up/down expands/collapses second row.
 
2786 Table header is not sticky in the scrolling area.
 
2790 Sorting does not include C<position>, neither does reordering.
 
2792 This behavior was implemented intentionally. But we can discuss, which behavior
 
2793 should be implemented.
 
2797 =head1 To discuss / Nice to have
 
2803 How to expand/collapse second row. Now it can be done clicking the icon or
 
2808 This controller uses a (changed) copy of the template for the PriceSource
 
2809 dialog. Maybe there could be used one code source.
 
2813 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2814 form. This is not only a problem here, but also in all parts using the PTC.
 
2815 There exists a ticket and a patch. This patch should be testet.
 
2819 An indicator, if the actual inputs are saved (like in an
 
2820 editor or on text processing application).
 
2824 A warning when leaving the page without saveing unchanged inputs.
 
2831 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>