1 package SL::Controller::Order;
 
   4 use parent qw(SL::Controller::Base);
 
   6 use SL::Helper::Flash qw(flash_later);
 
   8 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
 
   9 use SL::Locale::String qw(t8);
 
  10 use SL::SessionFile::Random;
 
  15 use SL::Util qw(trim);
 
  17 use SL::DB::AdditionalBillingAddress;
 
  24 use SL::DB::PartClassification;
 
  25 use SL::DB::PartsGroup;
 
  29 use SL::DB::RecordLink;
 
  30 use SL::DB::RequirementSpec;
 
  32 use SL::DB::Translation;
 
  34 use SL::Helper::CreatePDF qw(:all);
 
  35 use SL::Helper::PrintOptions;
 
  36 use SL::Helper::ShippedQty;
 
  37 use SL::Helper::UserPreferences::DisplayPreferences;
 
  38 use SL::Helper::UserPreferences::PositionsScrollbar;
 
  39 use SL::Helper::UserPreferences::UpdatePositions;
 
  41 use SL::Controller::Helper::GetModels;
 
  43 use List::Util qw(first sum0);
 
  44 use List::UtilsBy qw(sort_by uniq_by);
 
  45 use List::MoreUtils qw(any none pairwise first_index);
 
  46 use English qw(-no_match_vars);
 
  51 use Rose::Object::MakeMethods::Generic
 
  53  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 
  54  'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
 
  59 __PACKAGE__->run_before('check_auth');
 
  61 __PACKAGE__->run_before('check_auth_for_edit',
 
  62                         except => [ qw(edit show_customer_vendor_details_dialog price_popup load_second_rows) ]);
 
  64 __PACKAGE__->run_before('recalc',
 
  65                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
 
  68 __PACKAGE__->run_before('get_unalterable_data',
 
  69                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
 
  80   $self->order->transdate(DateTime->now_local());
 
  81   my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
  82                    $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
  84   if (   ($self->type eq sales_order_type()     &&  $::instance_conf->get_deliverydate_on)
 
  85       || ($self->type eq sales_quotation_type() &&  $::instance_conf->get_reqdate_on)
 
  86       && (!$self->order->reqdate)) {
 
  87     $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
 
  94     title => $self->get_title_for('add'),
 
  95     %{$self->{template_args}}
 
  99 # edit an existing order
 
 107     # this is to edit an order from an unsaved order object
 
 109     # set item ids to new fake id, to identify them as new items
 
 110     foreach my $item (@{$self->order->items_sorted}) {
 
 111       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 113     # trigger rendering values for second row as hidden, because they
 
 114     # are loaded only on demand. So we need to keep the values from
 
 116     $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
 123     title => $self->get_title_for('edit'),
 
 124     %{$self->{template_args}}
 
 128 # edit a collective order (consisting of one or more existing orders)
 
 129 sub action_edit_collective {
 
 133   my @multi_ids = map {
 
 134     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 135   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 137   # fall back to add if no ids are given
 
 138   if (scalar @multi_ids == 0) {
 
 143   # fall back to save as new if only one id is given
 
 144   if (scalar @multi_ids == 1) {
 
 145     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 146     $self->action_save_as_new();
 
 150   # make new order from given orders
 
 151   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 152   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 153   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 155   $self->action_edit();
 
 162   my $errors = $self->delete();
 
 164   if (scalar @{ $errors }) {
 
 165     $self->js->flash('error', $_) foreach @{ $errors };
 
 166     return $self->js->render();
 
 169   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 170            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 171            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 172            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 174   flash_later('info', $text);
 
 176   my @redirect_params = (
 
 181   $self->redirect_to(@redirect_params);
 
 188   my $errors = $self->save();
 
 190   if (scalar @{ $errors }) {
 
 191     $self->js->flash('error', $_) foreach @{ $errors };
 
 192     return $self->js->render();
 
 195   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 196            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 197            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 198            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 200   flash_later('info', $text);
 
 203   if ($::form->{back_to_caller}) {
 
 204     @redirect_params = $::form->{callback} ? ($::form->{callback})
 
 205                                            : (controller => 'LoginScreen', action => 'user_login');
 
 211       id       => $self->order->id,
 
 212       callback => $::form->{callback},
 
 216   $self->redirect_to(@redirect_params);
 
 219 # save the order as new document an open it for edit
 
 220 sub action_save_as_new {
 
 223   my $order = $self->order;
 
 226     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 227     return $self->js->render();
 
 230   # load order from db to check if values changed
 
 231   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 234   # Lets assign a new number if the user hasn't changed the previous one.
 
 235   # If it has been changed manually then use it as-is.
 
 236   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 238                         : trim($order->number);
 
 240   # Clear transdate unless changed
 
 241   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 242                         ? DateTime->today_local
 
 245   # Set new reqdate unless changed if it is enabled in client config
 
 246   if ($order->reqdate == $saved_order->reqdate) {
 
 247     my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
 248                      $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
 250     if (   ($self->type eq sales_order_type()     &&  !$::instance_conf->get_deliverydate_on)
 
 251         || ($self->type eq sales_quotation_type() &&  !$::instance_conf->get_reqdate_on)) {
 
 252       $new_attrs{reqdate} = '';
 
 254       $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 257     $new_attrs{reqdate} = $order->reqdate;
 
 261   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 263   # Warn on obsolete items
 
 264   my @obsolete_positions = map { $_->position } grep { $_->part->obsolete } @{ $order->items_sorted };
 
 265   flash_later('warning', t8('This record containts obsolete items at position #1', join ', ', @obsolete_positions)) if @obsolete_positions;
 
 267   # Create new record from current one
 
 268   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 270   # no linked records on save as new
 
 271   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 274   $self->action_save();
 
 279 # This is called if "print" is pressed in the print dialog.
 
 280 # If PDF creation was requested and succeeded, the pdf is offered for download
 
 281 # via send_file (which uses ajax in this case).
 
 285   my $errors = $self->save();
 
 287   if (scalar @{ $errors }) {
 
 288     $self->js->flash('error', $_) foreach @{ $errors };
 
 289     return $self->js->render();
 
 292   $self->js_reset_order_and_item_ids_after_save;
 
 294   my $format      = $::form->{print_options}->{format};
 
 295   my $media       = $::form->{print_options}->{media};
 
 296   my $formname    = $::form->{print_options}->{formname};
 
 297   my $copies      = $::form->{print_options}->{copies};
 
 298   my $groupitems  = $::form->{print_options}->{groupitems};
 
 299   my $printer_id  = $::form->{print_options}->{printer_id};
 
 301   # only PDF, OpenDocument & HTML for now
 
 302   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
 
 303     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 306   # only screen or printer by now
 
 307   if (none { $media eq $_ } qw(screen printer)) {
 
 308     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 311   # create a form for generate_attachment_filename
 
 312   my $form   = Form->new;
 
 313   $form->{$self->nr_key()}  = $self->order->number;
 
 314   $form->{type}             = $self->type;
 
 315   $form->{format}           = $format;
 
 316   $form->{formname}         = $formname;
 
 317   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 318   my $doc_filename          = $form->generate_attachment_filename();
 
 321   my @errors = $self->generate_doc(\$doc, { media      => $media,
 
 323                                             formname   => $formname,
 
 324                                             language   => $self->order->language,
 
 325                                             printer_id => $printer_id,
 
 326                                             groupitems => $groupitems });
 
 327   if (scalar @errors) {
 
 328     return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
 
 331   if ($media eq 'screen') {
 
 333     $self->js->flash('info', t8('The document has been created.'));
 
 336       type         => SL::MIME->mime_type_from_ext($doc_filename),
 
 337       name         => $doc_filename,
 
 341   } elsif ($media eq 'printer') {
 
 343     my $printer_id = $::form->{print_options}->{printer_id};
 
 344     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 349     $self->js->flash('info', t8('The document has been printed.'));
 
 352   my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
 
 353   if (scalar @warnings) {
 
 354     $self->js->flash('warning', $_) for @warnings;
 
 357   $self->save_history('PRINTED');
 
 360     ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
 
 363 sub action_preview_pdf {
 
 366   my $errors = $self->save();
 
 367   if (scalar @{ $errors }) {
 
 368     $self->js->flash('error', $_) foreach @{ $errors };
 
 369     return $self->js->render();
 
 372   $self->js_reset_order_and_item_ids_after_save;
 
 375   my $media       = 'screen';
 
 376   my $formname    = $self->type;
 
 379   # create a form for generate_attachment_filename
 
 380   my $form   = Form->new;
 
 381   $form->{$self->nr_key()}  = $self->order->number;
 
 382   $form->{type}             = $self->type;
 
 383   $form->{format}           = $format;
 
 384   $form->{formname}         = $formname;
 
 385   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 386   my $pdf_filename          = $form->generate_attachment_filename();
 
 389   my @errors = $self->generate_doc(\$pdf, { media      => $media,
 
 391                                             formname   => $formname,
 
 392                                             language   => $self->order->language,
 
 394   if (scalar @errors) {
 
 395     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 397   $self->save_history('PREVIEWED');
 
 398   $self->js->flash('info', t8('The PDF has been previewed'));
 
 402     type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 403     name         => $pdf_filename,
 
 408 # open the email dialog
 
 409 sub action_save_and_show_email_dialog {
 
 412   my $errors = $self->save();
 
 414   if (scalar @{ $errors }) {
 
 415     $self->js->flash('error', $_) foreach @{ $errors };
 
 416     return $self->js->render();
 
 419   my $cv_method = $self->cv;
 
 421   if (!$self->order->$cv_method) {
 
 422     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'))
 
 427   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 428   $email_form->{to} ||= $self->order->$cv_method->email;
 
 429   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 430   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 431   # Todo: get addresses from shipto, if any
 
 433   my $form = Form->new;
 
 434   $form->{$self->nr_key()}  = $self->order->number;
 
 435   $form->{cusordnumber}     = $self->order->cusordnumber;
 
 436   $form->{formname}         = $self->type;
 
 437   $form->{type}             = $self->type;
 
 438   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 439   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
 
 440   $form->{format}           = 'pdf';
 
 441   $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
 443   $email_form->{subject}             = $form->generate_email_subject();
 
 444   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 445   $email_form->{message}             = $form->generate_email_body();
 
 446   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 448   my %files = $self->get_files_for_email_dialog();
 
 450   my @employees_with_email = grep {
 
 451     my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
 
 452     $user && !!trim($user->get_config_value('email'));
 
 453   } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
 
 456   my $all_partner_email_addresses = $self->order->customervendor->get_all_email_addresses();
 
 458   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 459                                   email_form    => $email_form,
 
 460                                   show_bcc      => $::auth->assert('email_bcc', 'may fail'),
 
 462                                   is_customer   => $self->cv eq 'customer',
 
 463                                   ALL_EMPLOYEES => \@employees_with_email,
 
 464                                   ALL_PARTNER_EMAIL_ADDRESSES => $all_partner_email_addresses,
 
 468       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 475 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 476 sub action_send_email {
 
 479   my $errors = $self->save();
 
 481   if (scalar @{ $errors }) {
 
 482     $self->js->run('kivi.Order.close_email_dialog');
 
 483     $self->js->flash('error', $_) foreach @{ $errors };
 
 484     return $self->js->render();
 
 487   $self->js_reset_order_and_item_ids_after_save;
 
 489   my $email_form  = delete $::form->{email_form};
 
 491   if ($email_form->{additional_to}) {
 
 492     $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
 
 493     delete $email_form->{additional_to};
 
 496   my %field_names = (to => 'email');
 
 498   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 500   # for Form::cleanup which may be called in Form::send_email
 
 501   $::form->{cwd}    = getcwd();
 
 502   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 504   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 505   $::form->{media}  = 'email';
 
 507   $::form->{attachment_policy} //= '';
 
 509   # Is an old file version available?
 
 511   if ($::form->{attachment_policy} eq 'old_file') {
 
 512     $attfile = SL::File->get_all(object_id     => $self->order->id,
 
 513                                  object_type   => $self->type,
 
 514                                  file_type     => 'document',
 
 515                                  print_variant => $::form->{formname});
 
 518   if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
 
 520     my @errors = $self->generate_doc(\$doc, {media      => $::form->{media},
 
 521                                              format     => $::form->{print_options}->{format},
 
 522                                              formname   => $::form->{print_options}->{formname},
 
 523                                              language   => $self->order->language,
 
 524                                              printer_id => $::form->{print_options}->{printer_id},
 
 525                                              groupitems => $::form->{print_options}->{groupitems}});
 
 526     if (scalar @errors) {
 
 527       return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
 
 530     my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename}, $::form->{formname});
 
 531     if (scalar @warnings) {
 
 532       flash_later('warning', $_) for @warnings;
 
 535     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 536     $sfile->fh->print($doc);
 
 539     $::form->{tmpfile} = $sfile->file_name;
 
 540     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 543   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 544   $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
 
 546   # internal notes unless no email journal
 
 547   unless ($::instance_conf->get_email_journal) {
 
 548     my $intnotes = $self->order->intnotes;
 
 549     $intnotes   .= "\n\n" if $self->order->intnotes;
 
 550     $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 551     $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 552     $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 553     $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 554     $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 555     $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 556     $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
 
 558     $self->order->update_attributes(intnotes => $intnotes);
 
 561   $self->save_history('MAILED');
 
 563   flash_later('info', t8('The email has been sent.'));
 
 565   my @redirect_params = (
 
 568     id     => $self->order->id,
 
 571   $self->redirect_to(@redirect_params);
 
 574 # open the periodic invoices config dialog
 
 576 # If there are values in the form (i.e. dialog was opened before),
 
 577 # then use this values. Create new ones, else.
 
 578 sub action_show_periodic_invoices_config_dialog {
 
 581   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 582   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 583   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 584                                                    order_value_periodicity => 'p', # = same as periodicity
 
 585                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 586                                                    extend_automatically_by => 12,
 
 588                                                    email_subject           => GenericTranslations->get(
 
 589                                                                                 language_id      => $::form->{language_id},
 
 590                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 591                                                    email_body              => GenericTranslations->get(
 
 592                                                                                 language_id      => $::form->{language_id},
 
 593                                                                                 translation_type => "salutation_general")
 
 594                                                                             . GenericTranslations->get(
 
 595                                                                                 language_id      => $::form->{language_id},
 
 596                                                                                 translation_type => "salutation_punctuation_mark") . "\n\n"
 
 597                                                                             . GenericTranslations->get(
 
 598                                                                                 language_id      => $::form->{language_id},
 
 599                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 601   # for older configs, replace email preset text if not yet set.
 
 602   $config->email_subject(GenericTranslations->get(
 
 603                                               language_id      => $::form->{language_id},
 
 604                                               translation_type =>"preset_text_periodic_invoices_email_subject")
 
 605                         ) unless $config->email_subject;
 
 607   $config->email_body(GenericTranslations->get(
 
 608                                               language_id      => $::form->{language_id},
 
 609                                               translation_type => "salutation_general")
 
 610                     . GenericTranslations->get(
 
 611                                               language_id      => $::form->{language_id},
 
 612                                               translation_type => "salutation_punctuation_mark") . "\n\n"
 
 613                     . GenericTranslations->get(
 
 614                                               language_id      => $::form->{language_id},
 
 615                                               translation_type =>"preset_text_periodic_invoices_email_body")
 
 616                      ) unless $config->email_body;
 
 618   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 619   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 621   $::form->get_lists(printers => "ALL_PRINTERS",
 
 622                      charts   => { key       => 'ALL_CHARTS',
 
 623                                    transdate => 'current_date' });
 
 625   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 627   if ($::form->{customer_id}) {
 
 628     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 629     my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
 
 630     $::form->{postal_invoice}                  = $customer_object->postal_invoice;
 
 631     $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
 
 632     $config->send_email(0) if $::form->{postal_invoice};
 
 635   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 637                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 638                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 643 # assign the values of the periodic invoices config dialog
 
 644 # as yaml in the hidden tag and set the status.
 
 645 sub action_assign_periodic_invoices_config {
 
 648   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 650   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 651                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 652                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 653                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 654                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 655                  start_date_as_date         => $::form->{start_date_as_date},
 
 656                  end_date_as_date           => $::form->{end_date_as_date},
 
 657                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 658                  print                      => $::form->{print}      ? 1                         : 0,
 
 659                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 660                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 661                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 662                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 663                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 664                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 665                  email_recipient_address    => $::form->{email_recipient_address},
 
 666                  email_sender               => $::form->{email_sender},
 
 667                  email_subject              => $::form->{email_subject},
 
 668                  email_body                 => $::form->{email_body},
 
 671   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 673   my $status = $self->get_periodic_invoices_status($config);
 
 676     ->remove('#order_periodic_invoices_config')
 
 677     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 678     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 679     ->html('#periodic_invoices_status', $status)
 
 680     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 684 sub action_get_has_active_periodic_invoices {
 
 687   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 688   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 690   my $has_active_periodic_invoices =
 
 691        $self->type eq sales_order_type()
 
 694     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 695     && $config->get_previous_billed_period_start_date;
 
 697   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 700 # save the order and redirect to the frontend subroutine for a new
 
 702 sub action_save_and_delivery_order {
 
 705   $self->save_and_redirect_to(
 
 706     controller => 'oe.pl',
 
 707     action     => 'oe_delivery_order_from_order',
 
 711 sub action_save_and_supplier_delivery_order {
 
 714   $self->save_and_redirect_to(
 
 715     controller => 'controller.pl',
 
 716     action     => 'DeliveryOrder/add_from_order',
 
 717     type       => 'supplier_delivery_order',
 
 721 # save the order and redirect to the frontend subroutine for a new
 
 723 sub action_save_and_invoice {
 
 726   $self->save_and_redirect_to(
 
 727     controller => 'oe.pl',
 
 728     action     => 'oe_invoice_from_order',
 
 732 sub action_save_and_invoice_for_advance_payment {
 
 735   $self->save_and_redirect_to(
 
 736     controller       => 'oe.pl',
 
 737     action           => 'oe_invoice_from_order',
 
 738     new_invoice_type => 'invoice_for_advance_payment',
 
 742 sub action_save_and_final_invoice {
 
 745   $self->save_and_redirect_to(
 
 746     controller       => 'oe.pl',
 
 747     action           => 'oe_invoice_from_order',
 
 748     new_invoice_type => 'final_invoice',
 
 752 # workflow from sales order to sales quotation
 
 753 sub action_sales_quotation {
 
 754   $_[0]->workflow_sales_or_request_for_quotation();
 
 757 # workflow from sales order to sales quotation
 
 758 sub action_request_for_quotation {
 
 759   $_[0]->workflow_sales_or_request_for_quotation();
 
 762 # workflow from sales quotation to sales order
 
 763 sub action_sales_order {
 
 764   $_[0]->workflow_sales_or_purchase_order();
 
 767 # workflow from rfq to purchase order
 
 768 sub action_purchase_order {
 
 769   $_[0]->workflow_sales_or_purchase_order();
 
 772 # workflow from purchase order to ap transaction
 
 773 sub action_save_and_ap_transaction {
 
 776   $self->save_and_redirect_to(
 
 777     controller => 'ap.pl',
 
 778     action     => 'add_from_purchase_order',
 
 782 # set form elements in respect to a changed customer or vendor
 
 784 # This action is called on an change of the customer/vendor picker.
 
 785 sub action_customer_vendor_changed {
 
 788   setup_order_from_cv($self->order);
 
 791   my $cv_method = $self->cv;
 
 793   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 794     $self->js->show('#cp_row');
 
 796     $self->js->hide('#cp_row');
 
 799   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 800     $self->js->show('#shipto_selection');
 
 802     $self->js->hide('#shipto_selection');
 
 805   if ($cv_method eq 'customer') {
 
 806     my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
 
 807     $self->js->$show_hide('#billing_address_row');
 
 810   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 813     ->replaceWith('#order_cp_id',              $self->build_contact_select)
 
 814     ->replaceWith('#order_shipto_id',          $self->build_shipto_select)
 
 815     ->replaceWith('#shipto_inputs  ',          $self->build_shipto_inputs)
 
 816     ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
 
 817     ->replaceWith('#business_info_row',        $self->build_business_info_row)
 
 818     ->val(        '#order_taxzone_id',         $self->order->taxzone_id)
 
 819     ->val(        '#order_taxincluded',        $self->order->taxincluded)
 
 820     ->val(        '#order_currency_id',        $self->order->currency_id)
 
 821     ->val(        '#order_payment_id',         $self->order->payment_id)
 
 822     ->val(        '#order_delivery_term_id',   $self->order->delivery_term_id)
 
 823     ->val(        '#order_intnotes',           $self->order->intnotes)
 
 824     ->val(        '#order_language_id',        $self->order->$cv_method->language_id)
 
 825     ->focus(      '#order_' . $self->cv . '_id')
 
 826     ->run('kivi.Order.update_exchangerate');
 
 828   $self->js_redisplay_amounts_and_taxes;
 
 829   $self->js_redisplay_cvpartnumbers;
 
 833 # open the dialog for customer/vendor details
 
 834 sub action_show_customer_vendor_details_dialog {
 
 837   my $is_customer = 'customer' eq $::form->{vc};
 
 840     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 842     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 845   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 846   $details{discount_as_percent} = $cv->discount_as_percent;
 
 847   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 848   $details{business}            = $cv->business->description      if $cv->business;
 
 849   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 850   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 851   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 852   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 855     foreach my $entry (@{ $cv->additional_billing_addresses }) {
 
 856       push @{ $details{ADDITIONAL_BILLING_ADDRESSES} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 859   foreach my $entry (@{ $cv->shipto }) {
 
 860     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 862   foreach my $entry (@{ $cv->contacts }) {
 
 863     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 866   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 867                 is_customer => $is_customer,
 
 872 # called if a unit in an existing item row is changed
 
 873 sub action_unit_changed {
 
 876   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 877   my $item = $self->order->items_sorted->[$idx];
 
 879   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 880   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 885     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 886   $self->js_redisplay_line_values;
 
 887   $self->js_redisplay_amounts_and_taxes;
 
 891 # update item input row when a part ist picked
 
 892 sub action_update_item_input_row {
 
 895   delete $::form->{add_item}->{$_} for qw(create_part_type sellprice_as_number discount_as_percent);
 
 897   my $form_attr = $::form->{add_item};
 
 899   return unless $form_attr->{parts_id};
 
 901   my $record       = $self->order;
 
 902   my $item         = SL::DB::OrderItem->new(%$form_attr);
 
 903   $item->unit($item->part->unit);
 
 905   my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
 
 908     ->val     ('#add_item_unit',                $item->unit)
 
 909     ->val     ('#add_item_description',         $item->part->description)
 
 910     ->val     ('#add_item_sellprice_as_number', '')
 
 911     ->attr    ('#add_item_sellprice_as_number', 'placeholder', $price_src->price_as_number)
 
 912     ->attr    ('#add_item_sellprice_as_number', 'title',       $price_src->source_description)
 
 913     ->val     ('#add_item_discount_as_percent', '')
 
 914     ->attr    ('#add_item_discount_as_percent', 'placeholder', $discount_src->discount_as_percent)
 
 915     ->attr    ('#add_item_discount_as_percent', 'title',       $discount_src->source_description)
 
 919 # add an item row for a new item entered in the input row
 
 920 sub action_add_item {
 
 923   delete $::form->{add_item}->{create_part_type};
 
 925   my $form_attr = $::form->{add_item};
 
 927   return unless $form_attr->{parts_id};
 
 929   my $item = new_item($self->order, $form_attr);
 
 931   $self->order->add_items($item);
 
 935   $self->get_item_cvpartnumber($item);
 
 937   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 938   my $row_as_html = $self->p->render('order/tabs/_row',
 
 944   if ($::form->{insert_before_item_id}) {
 
 946       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 949       ->append('#row_table_id', $row_as_html);
 
 952   if ( $item->part->is_assortment ) {
 
 953     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 954     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 955       my $attr = { parts_id => $assortment_item->parts_id,
 
 956                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 957                    unit     => $assortment_item->unit,
 
 958                    description => $assortment_item->part->description,
 
 960       my $item = new_item($self->order, $attr);
 
 962       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 963       $item->discount(1) unless $assortment_item->charge;
 
 965       $self->order->add_items( $item );
 
 967       $self->get_item_cvpartnumber($item);
 
 968       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 969       my $row_as_html = $self->p->render('order/tabs/_row',
 
 974       if ($::form->{insert_before_item_id}) {
 
 976           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 979           ->append('#row_table_id', $row_as_html);
 
 985     ->val('.add_item_input', '')
 
 986     ->attr('.add_item_input', 'placeholder', '')
 
 987     ->attr('.add_item_input', 'title', '')
 
 988     ->run('kivi.Order.init_row_handlers')
 
 989     ->run('kivi.Order.renumber_positions')
 
 990     ->focus('#add_item_parts_id_name');
 
 992   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 994   $self->js_redisplay_amounts_and_taxes;
 
 998 # add item rows for multiple items at once
 
 999 sub action_add_multi_items {
 
1002   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
 
1003   return $self->js->render() unless scalar @form_attr;
 
1006   foreach my $attr (@form_attr) {
 
1007     my $item = new_item($self->order, $attr);
 
1009     if ( $item->part->is_assortment ) {
 
1010       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
1011         my $attr = { parts_id => $assortment_item->parts_id,
 
1012                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
1013                      unit     => $assortment_item->unit,
 
1014                      description => $assortment_item->part->description,
 
1016         my $item = new_item($self->order, $attr);
 
1018         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
1019         $item->discount(1) unless $assortment_item->charge;
 
1024   $self->order->add_items(@items);
 
1028   foreach my $item (@items) {
 
1029     $self->get_item_cvpartnumber($item);
 
1030     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1031     my $row_as_html = $self->p->render('order/tabs/_row',
 
1037     if ($::form->{insert_before_item_id}) {
 
1039         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
1042         ->append('#row_table_id', $row_as_html);
 
1047     ->run('kivi.Part.close_picker_dialogs')
 
1048     ->run('kivi.Order.init_row_handlers')
 
1049     ->run('kivi.Order.renumber_positions')
 
1050     ->focus('#add_item_parts_id_name');
 
1052   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
1054   $self->js_redisplay_amounts_and_taxes;
 
1055   $self->js->render();
 
1058 # recalculate all linetotals, amounts and taxes and redisplay them
 
1059 sub action_recalc_amounts_and_taxes {
 
1064   $self->js_redisplay_line_values;
 
1065   $self->js_redisplay_amounts_and_taxes;
 
1066   $self->js->render();
 
1069 sub action_update_exchangerate {
 
1073     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
1074     currency_name => $self->order->currency->name,
 
1075     exchangerate  => $self->order->daily_exchangerate_as_null_number,
 
1078   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
1081 # redisplay item rows if they are sorted by an attribute
 
1082 sub action_reorder_items {
 
1086     partnumber   => sub { $_[0]->part->partnumber },
 
1087     description  => sub { $_[0]->description },
 
1088     qty          => sub { $_[0]->qty },
 
1089     sellprice    => sub { $_[0]->sellprice },
 
1090     discount     => sub { $_[0]->discount },
 
1091     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
1094   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1096   my $method = $sort_keys{$::form->{order_by}};
 
1097   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
1098   if ($::form->{sort_dir}) {
 
1099     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1100       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
1102       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
1105     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1106       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
1108       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
1112     ->run('kivi.Order.redisplay_items', \@to_sort)
 
1116 # show the popup to choose a price/discount source
 
1117 sub action_price_popup {
 
1120   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
1121   my $item = $self->order->items_sorted->[$idx];
 
1123   $self->render_price_dialog($item);
 
1126 # save the order in a session variable and redirect to the part controller
 
1127 sub action_create_part {
 
1130   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
 
1132   my $callback     = $self->url_for(
 
1133     action       => 'return_from_create_part',
 
1134     type         => $self->type, # type is needed for check_auth on return
 
1135     previousform => $previousform,
 
1138   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.'));
 
1140   my @redirect_params = (
 
1141     controller    => 'Part',
 
1143     part_type     => $::form->{add_item}->{create_part_type},
 
1144     callback      => $callback,
 
1148   $self->redirect_to(@redirect_params);
 
1151 sub action_return_from_create_part {
 
1154   $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
 
1156   $::auth->restore_form_from_session(delete $::form->{previousform});
 
1158   # set item ids to new fake id, to identify them as new items
 
1159   foreach my $item (@{$self->order->items_sorted}) {
 
1160     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1164   $self->get_unalterable_data();
 
1165   $self->pre_render();
 
1167   # trigger rendering values for second row/longdescription as hidden,
 
1168   # because they are loaded only on demand. So we need to keep the values
 
1170   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1171   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1175     title => $self->get_title_for('edit'),
 
1176     %{$self->{template_args}}
 
1181 # load the second row for one or more items
 
1183 # This action gets the html code for all items second rows by rendering a template for
 
1184 # the second row and sets the html code via client js.
 
1185 sub action_load_second_rows {
 
1188   $self->recalc() if $self->order->is_sales; # for margin calculation
 
1190   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1191     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1192     my $item = $self->order->items_sorted->[$idx];
 
1194     $self->js_load_second_row($item, $item_id, 0);
 
1197   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
1199   $self->js->render();
 
1202 # update description, notes and sellprice from master data
 
1203 sub action_update_row_from_master_data {
 
1206   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1207     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1208     my $item  = $self->order->items_sorted->[$idx];
 
1209     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1211     $item->description($texts->{description});
 
1212     $item->longdescription($texts->{longdescription});
 
1214     my ($price_src, $discount_src) = get_best_price_and_discount_source($self->order, $item, 1);
 
1216     $item->sellprice($price_src->price);
 
1217     $item->active_price_source($price_src);
 
1218     $item->discount($discount_src->discount);
 
1219     $item->active_discount_source($discount_src);
 
1221     my $price_editable = $self->order->is_sales ? $::auth->assert('sales_edit_prices', 1) : $::auth->assert('purchase_edit_prices', 1);
 
1224       ->run('kivi.Order.set_price_and_source_text',    $item_id, $price_src   ->source, $price_src   ->source_description, $item->sellprice_as_number, $price_editable)
 
1225       ->run('kivi.Order.set_discount_and_source_text', $item_id, $discount_src->source, $discount_src->source_description, $item->discount_as_percent, $price_editable)
 
1226       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1227       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1228       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1230     if ($self->search_cvpartnumber) {
 
1231       $self->get_item_cvpartnumber($item);
 
1232       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1237   $self->js_redisplay_line_values;
 
1238   $self->js_redisplay_amounts_and_taxes;
 
1240   $self->js->render();
 
1243 sub action_save_phone_note {
 
1246   if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
 
1247     return $self->js->flash('error', t8('Phone note needs a subject and a body.'))->render;
 
1251   if ($::form->{phone_note}->{id}) {
 
1252     $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
 
1253     return $self->js->flash('error', t8('Phone note not found for this order.'))->render if !$phone_note;
 
1256   $phone_note = SL::DB::Note->new() if !$phone_note;
 
1257   my $is_new  = !$phone_note->id;
 
1259   $phone_note->assign_attributes(%{ $::form->{phone_note} },
 
1260                                  trans_id     => $self->order->id,
 
1261                                  trans_module => 'oe',
 
1262                                  employee     => SL::DB::Manager::Employee->current);
 
1265   $self->order(SL::DB::Order->new(id => $self->order->id)->load);
 
1267   my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
 
1270     ->replaceWith('#phone-notes', $tab_as_html)
 
1271     ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
 
1272     ->flash('info', $is_new ? t8('Phone note has been created.') : t8('Phone note has been updated.'))
 
1276 sub action_delete_phone_note {
 
1279   my $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
 
1281   return $self->js->flash('error', t8('Phone note not found for this order.'))->render if !$phone_note;
 
1283   $phone_note->delete;
 
1284   $self->order(SL::DB::Order->new(id => $self->order->id)->load);
 
1286   my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
 
1289     ->replaceWith('#phone-notes', $tab_as_html)
 
1290     ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
 
1291     ->flash('info', t8('Phone note has been deleted.'))
 
1295 sub js_load_second_row {
 
1296   my ($self, $item, $item_id, $do_parse) = @_;
 
1299     # Parse values from form (they are formated while rendering (template)).
 
1300     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1301     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1302     foreach my $var (@{ $item->cvars_by_config }) {
 
1303       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1305     $item->parse_custom_variable_values;
 
1308   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1311     ->html('#second_row_' . $item_id, $row_as_html)
 
1312     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1315 sub js_redisplay_line_values {
 
1318   my $is_sales = $self->order->is_sales;
 
1320   # sales orders with margins
 
1325        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1326        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1327        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1328       ]} @{ $self->order->items_sorted };
 
1332        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1333       ]} @{ $self->order->items_sorted };
 
1337     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1340 sub js_redisplay_amounts_and_taxes {
 
1343   if (scalar @{ $self->{taxes} }) {
 
1344     $self->js->show('#taxincluded_row_id');
 
1346     $self->js->hide('#taxincluded_row_id');
 
1349   if ($self->order->taxincluded) {
 
1350     $self->js->hide('#subtotal_row_id');
 
1352     $self->js->show('#subtotal_row_id');
 
1355   if ($self->order->is_sales) {
 
1356     my $is_neg = $self->order->marge_total < 0;
 
1358       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1359       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1360       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1361       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1362       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1363       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1364       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1365       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1369     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1370     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1371     ->remove('.tax_row')
 
1372     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1375 sub js_redisplay_cvpartnumbers {
 
1378   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1380   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1383     ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
 
1386 sub js_reset_order_and_item_ids_after_save {
 
1390     ->val('#id', $self->order->id)
 
1391     ->val('#converted_from_oe_id', '')
 
1392     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1395   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1396     next if !$self->order->items_sorted->[$idx]->id;
 
1397     next if $form_item_id !~ m{^new};
 
1399       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1400       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1401       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1405   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1412 sub init_valid_types {
 
1413   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1419   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1420     die "Not a valid type for order";
 
1423   $self->type($::form->{type});
 
1429   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1430          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1431          : die "Not a valid type for order";
 
1436 sub init_search_cvpartnumber {
 
1439   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1440   my $search_cvpartnumber;
 
1441   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1442   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1444   return $search_cvpartnumber;
 
1447 sub init_show_update_button {
 
1450   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1461 sub init_all_price_factors {
 
1462   SL::DB::Manager::PriceFactor->get_all;
 
1465 sub init_part_picker_classification_ids {
 
1467   my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
 
1469   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
 
1475   my $right_for = { map { $_ => $_.'_edit' . ' | ' . $_.'_view' } @{$self->valid_types} };
 
1477   my $right   = $right_for->{ $self->type };
 
1478   $right    ||= 'DOES_NOT_EXIST';
 
1480   $::auth->assert($right);
 
1483 sub check_auth_for_edit {
 
1486   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1488   my $right   = $right_for->{ $self->type };
 
1489   $right    ||= 'DOES_NOT_EXIST';
 
1491   $::auth->assert($right);
 
1494 # build the selection box for contacts
 
1496 # Needed, if customer/vendor changed.
 
1497 sub build_contact_select {
 
1500   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1501     value_key  => 'cp_id',
 
1502     title_key  => 'full_name_dep',
 
1503     default    => $self->order->cp_id,
 
1505     style      => 'width: 300px',
 
1509 # build the selection box for the additional billing address
 
1511 # Needed, if customer/vendor changed.
 
1512 sub build_billing_address_select {
 
1515   return '' if $self->cv ne 'customer';
 
1517   select_tag('order.billing_address_id',
 
1518              [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
 
1520              title_key  => 'displayable_id',
 
1521              default    => $self->order->billing_address_id,
 
1523              style      => 'width: 300px',
 
1527 # build the selection box for shiptos
 
1529 # Needed, if customer/vendor changed.
 
1530 sub build_shipto_select {
 
1533   select_tag('order.shipto_id',
 
1534              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1535              value_key  => 'shipto_id',
 
1536              title_key  => 'displayable_id',
 
1537              default    => $self->order->shipto_id,
 
1539              style      => 'width: 300px',
 
1543 # build the inputs for the cusom shipto dialog
 
1545 # Needed, if customer/vendor changed.
 
1546 sub build_shipto_inputs {
 
1549   my $content = $self->p->render('common/_ship_to_dialog',
 
1550                                  vc_obj      => $self->order->customervendor,
 
1551                                  cs_obj      => $self->order->custom_shipto,
 
1552                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1553                                  id_selector => '#order_shipto_id');
 
1555   div_tag($content, id => 'shipto_inputs');
 
1558 # render the info line for business
 
1560 # Needed, if customer/vendor changed.
 
1561 sub build_business_info_row
 
1563   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1566 # build the rows for displaying taxes
 
1568 # Called if amounts where recalculated and redisplayed.
 
1569 sub build_tax_rows {
 
1573   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1574     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1576   return $rows_as_html;
 
1580 sub render_price_dialog {
 
1581   my ($self, $record_item) = @_;
 
1583   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1587       'kivi.io.price_chooser_dialog',
 
1588       t8('Available Prices'),
 
1589       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1594 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1595 #     $self->js->show('#dialog_flash_error');
 
1604   return if !$::form->{id};
 
1606   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1608   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1609   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1610   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1612   return $self->order;
 
1615 # load or create a new order object
 
1617 # And assign changes from the form to this object.
 
1618 # If the order is loaded from db, check if items are deleted in the form,
 
1619 # remove them form the object and collect them for removing from db on saving.
 
1620 # Then create/update items from form (via make_item) and add them.
 
1624   # add_items adds items to an order with no items for saving, but they cannot
 
1625   # be retrieved via items until the order is saved. Adding empty items to new
 
1626   # order here solves this problem.
 
1628   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1629   $order ||= SL::DB::Order->new(orderitems  => [],
 
1630                                 quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
 
1631                                 currency_id => $::instance_conf->get_currency_id(),);
 
1633   my $cv_id_method = $self->cv . '_id';
 
1634   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1635     $order->$cv_id_method($::form->{$cv_id_method});
 
1636     setup_order_from_cv($order);
 
1639   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1640   my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
 
1642   $order->assign_attributes(%{$::form->{order}});
 
1644   $self->setup_custom_shipto_from_form($order, $::form);
 
1646   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1647     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1648     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1651   # remove deleted items
 
1652   $self->item_ids_to_delete([]);
 
1653   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1654     my $item = $order->orderitems->[$idx];
 
1655     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1656       splice @{$order->orderitems}, $idx, 1;
 
1657       push @{$self->item_ids_to_delete}, $item->id;
 
1663   foreach my $form_attr (@{$form_orderitems}) {
 
1664     my $item = make_item($order, $form_attr);
 
1665     $item->position($pos);
 
1669   $order->add_items(grep {!$_->id} @items);
 
1674 # create or update items from form
 
1676 # Make item objects from form values. For items already existing read from db.
 
1677 # Create a new item else. And assign attributes.
 
1679   my ($record, $attr) = @_;
 
1682   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1684   my $is_new = !$item;
 
1686   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1687   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1688   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1689   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1691   $item->assign_attributes(%$attr);
 
1694     my $texts = get_part_texts($item->part, $record->language_id);
 
1695     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1696     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1697     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1705 # This is used to add one item
 
1707   my ($record, $attr) = @_;
 
1709   my $item = SL::DB::OrderItem->new;
 
1711   # Remove attributes where the user left or set the inputs empty.
 
1712   # So these attributes will be undefined and we can distinguish them
 
1713   # from zero later on.
 
1714   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1715     delete $attr->{$_} if $attr->{$_} eq '';
 
1718   $item->assign_attributes(%$attr);
 
1719   $item->qty(1.0)                   if !$item->qty;
 
1720   $item->unit($item->part->unit)    if !$item->unit;
 
1722   my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
 
1725   $new_attr{description}            = $item->part->description     if ! $item->description;
 
1726   $new_attr{qty}                    = 1.0                          if ! $item->qty;
 
1727   $new_attr{price_factor_id}        = $item->part->price_factor_id if ! $item->price_factor_id;
 
1728   $new_attr{sellprice}              = $price_src->price;
 
1729   $new_attr{discount}               = $discount_src->discount;
 
1730   $new_attr{active_price_source}    = $price_src;
 
1731   $new_attr{active_discount_source} = $discount_src;
 
1732   $new_attr{longdescription}        = $item->part->notes           if ! defined $attr->{longdescription};
 
1733   $new_attr{project_id}             = $record->globalproject_id;
 
1734   $new_attr{lastcost}               = $record->is_sales ? $item->part->lastcost : 0;
 
1736   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1737   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1738   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1739   $new_attr{custom_variables} = [];
 
1741   my $texts = get_part_texts($item->part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1743   $item->assign_attributes(%new_attr, %{ $texts });
 
1748 sub setup_order_from_cv {
 
1751   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id language_id));
 
1753   $order->intnotes($order->customervendor->notes);
 
1755   return if !$order->is_sales;
 
1757   $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1758   $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1759                       ? $order->customer->taxincluded_checked
 
1760                       : $::myconfig{taxincluded_checked});
 
1762   my $address = $order->customer->default_billing_address;;
 
1763   $order->billing_address_id($address ? $address->id : undef);
 
1766 # setup custom shipto from form
 
1768 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1769 # with 'shiptocvar_'.
 
1770 # Mark it to be deleted if a shipto from master data is selected
 
1771 # (i.e. order has a shipto).
 
1772 # Else, update or create a new custom shipto. If the fields are empty, it
 
1773 # will not be saved on save.
 
1774 sub setup_custom_shipto_from_form {
 
1775   my ($self, $order, $form) = @_;
 
1777   if ($order->shipto) {
 
1778     $self->is_custom_shipto_to_delete(1);
 
1780     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1782     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1783     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1785     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1786     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1790 # recalculate prices and taxes
 
1792 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1796   my %pat = $self->order->calculate_prices_and_taxes();
 
1798   $self->{taxes} = [];
 
1799   foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
 
1800     my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
1802     push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
 
1803                                 netamount => $netamount,
 
1804                                 tax       => SL::DB::Tax->new(id => $tax_id)->load });
 
1806   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1809 # get data for saving, printing, ..., that is not changed in the form
 
1811 # Only cvars for now.
 
1812 sub get_unalterable_data {
 
1815   foreach my $item (@{ $self->order->items }) {
 
1816     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1817     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1818     foreach my $var (@{ $item->cvars_by_config }) {
 
1819       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1821     $item->parse_custom_variable_values;
 
1827 # And remove related files in the spool directory
 
1832   my $db     = $self->order->db;
 
1834   $db->with_transaction(
 
1836       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1837       $self->order->delete;
 
1838       my $spool = $::lx_office_conf{paths}->{spool};
 
1839       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1841       $self->save_history('DELETED');
 
1844   }) || push(@{$errors}, $db->error);
 
1851 # And delete items that are deleted in the form.
 
1856   my $db     = $self->order->db;
 
1858   # check for new or updated phone note
 
1859   if ($::form->{phone_note}->{subject} || $::form->{phone_note}->{body}) {
 
1860     if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
 
1861       return [t8('Phone note needs a subject and a body.')];
 
1866     if ($::form->{phone_note}->{id}) {
 
1867       $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
 
1868       return [t8('Phone note not found for this order.')] if !$phone_note;
 
1871     $phone_note = SL::DB::Note->new() if !$phone_note;
 
1872     my $is_new  = !$phone_note->id;
 
1874     $phone_note->assign_attributes(%{ $::form->{phone_note} },
 
1875                                    trans_id     => $self->order->id,
 
1876                                    trans_module => 'oe',
 
1877                                    employee     => SL::DB::Manager::Employee->current);
 
1879     $self->order->add_phone_notes($phone_note) if $is_new;
 
1882   $db->with_transaction(sub {
 
1883     # delete custom shipto if it is to be deleted or if it is empty
 
1884     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1885       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1886       $self->order->custom_shipto(undef);
 
1889     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1890     $self->order->save(cascade => 1);
 
1893     if ($::form->{converted_from_oe_id}) {
 
1894       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1896       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1897         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1898         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1899         $src->link_to_record($self->order);
 
1901       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1903         foreach (@{ $self->order->items_sorted }) {
 
1904           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1906           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1907                                   from_id    => $from_id,
 
1908                                   to_table   => 'orderitems',
 
1915       $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
 
1918     $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
 
1920     $self->save_history('SAVED');
 
1923   }) || push(@{$errors}, $db->error);
 
1928 sub workflow_sales_or_request_for_quotation {
 
1932   my $errors = $self->save();
 
1934   if (scalar @{ $errors }) {
 
1935     $self->js->flash('error', $_) for @{ $errors };
 
1936     return $self->js->render();
 
1939   my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
 
1941   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1942   delete $::form->{id};
 
1944   # no linked records from order to quotations
 
1945   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
1947   # set item ids to new fake id, to identify them as new items
 
1948   foreach my $item (@{$self->order->items_sorted}) {
 
1949     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1953   $::form->{type} = $destination_type;
 
1954   $self->type($self->init_type);
 
1955   $self->cv  ($self->init_cv);
 
1959   $self->get_unalterable_data();
 
1960   $self->pre_render();
 
1962   # trigger rendering values for second row as hidden, because they
 
1963   # are loaded only on demand. So we need to keep the values from the
 
1965   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1969     title => $self->get_title_for('edit'),
 
1970     %{$self->{template_args}}
 
1974 sub workflow_sales_or_purchase_order {
 
1978   my $errors = $self->save();
 
1980   if (scalar @{ $errors }) {
 
1981     $self->js->flash('error', $_) foreach @{ $errors };
 
1982     return $self->js->render();
 
1985   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1986                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1987                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1988                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1991   # check for direct delivery
 
1992   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1994   if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
 
1995       && $::form->{use_shipto} && $self->order->shipto) {
 
1996     $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
 
1999   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
2000   $self->{converted_from_oe_id} = delete $::form->{id};
 
2002   # set item ids to new fake id, to identify them as new items
 
2003   foreach my $item (@{$self->order->items_sorted}) {
 
2004     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
2007   if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
 
2008     if ($::form->{use_shipto}) {
 
2009       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
2011       # remove any custom shipto if not wanted
 
2012       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
2017   $::form->{type} = $destination_type;
 
2018   $self->type($self->init_type);
 
2019   $self->cv  ($self->init_cv);
 
2023   $self->get_unalterable_data();
 
2024   $self->pre_render();
 
2026   # trigger rendering values for second row as hidden, because they
 
2027   # are loaded only on demand. So we need to keep the values from the
 
2029   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
2033     title => $self->get_title_for('edit'),
 
2034     %{$self->{template_args}}
 
2042   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
2043   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
2044   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
2045   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
 
2046   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
2049   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
2052   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
2054   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
2055   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
2056   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
2057   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
2058   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
2060   my $print_form = Form->new('');
 
2061   $print_form->{type}        = $self->type;
 
2062   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
2063   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
2064     form => $print_form,
 
2065     options => {dialog_name_prefix => 'print_options.',
 
2069                 no_opendocument    => 0,
 
2073   foreach my $item (@{$self->order->orderitems}) {
 
2074     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
2075     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
2076     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
2079   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
2080     # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
 
2081     # Do not use write_to_objects to prevent order->delivered to be set, because this should be
 
2082     # the value from db, which can be set manually or is set when linked delivery orders are saved.
 
2083     SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
 
2086   if ($self->order->number && $::instance_conf->get_webdav) {
 
2087     my $webdav = SL::Webdav->new(
 
2088       type     => $self->type,
 
2089       number   => $self->order->number,
 
2091     my @all_objects = $webdav->get_all_objects;
 
2092     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
2094                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
2098   if (   (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
 
2099       && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
 
2100     $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
 
2102   $self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
 
2104   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
2106   $self->{template_args}->{num_phone_notes} = scalar @{ $self->order->phone_notes || [] };
 
2108   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
2109                                                          edit_periodic_invoices_config calculate_qty follow_up show_history);
 
2110   $self->setup_edit_action_bar;
 
2113 sub setup_edit_action_bar {
 
2114   my ($self, %params) = @_;
 
2116   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
2117                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
2118                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
2120   my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
 
2121   my @req_cusordnumber   = qw(kivi.Order.check_cusordnumber_presence)           x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
 
2123   my $has_invoice_for_advance_payment;
 
2124   if ($self->order->id && $self->type eq sales_order_type()) {
 
2125     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2126     $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
 
2129   my $has_final_invoice;
 
2130   if ($self->order->id && $self->type eq sales_order_type()) {
 
2131     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2132     $has_final_invoice               = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
 
2135   my $right_for         = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
2136   my $right             = $right_for->{ $self->type };
 
2137   $right              ||= 'DOES_NOT_EXIST';
 
2138   my $may_edit_create   = $::auth->assert($right, 'may fail');
 
2140   for my $bar ($::request->layout->get('actionbar')) {
 
2145           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
2146                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
2148           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
 
2149                          @req_trans_cost_art, @req_cusordnumber,
 
2151           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2154           t8('Save and Close'),
 
2155           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
2156                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
2159           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
 
2160                          @req_trans_cost_art, @req_cusordnumber,
 
2162           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2166           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
2167           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2168                          @req_trans_cost_art, @req_cusordnumber,
 
2170           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.')
 
2171                      : !$self->order->id ? t8('This object has not been saved yet.')
 
2174       ], # end of combobox "Save"
 
2181           t8('Save and Quotation'),
 
2182           submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
 
2183           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2184           only_if  => (any { $self->type eq $_ } (sales_order_type())),
 
2185           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2189           submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
 
2190           only_if  => (any { $self->type eq $_ } (purchase_order_type())),
 
2191           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2194           t8('Save and Sales Order'),
 
2195           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
2196           checks   => [ @req_trans_cost_art ],
 
2197           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
2198           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2201           t8('Save and Purchase Order'),
 
2202           call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
 
2203           checks    => [ @req_trans_cost_art, @req_cusordnumber ],
 
2204           only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
2205           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2208           t8('Save and Delivery Order'),
 
2209           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2210                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2212           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2213                          @req_trans_cost_art, @req_cusordnumber,
 
2215           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())),
 
2216           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2219           t8('Save and Supplier Delivery Order'),
 
2220           call      => [ 'kivi.Order.save', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2221                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2223           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2224                          @req_trans_cost_art, @req_cusordnumber,
 
2226           only_if   => (any { $self->type eq $_ } (purchase_order_type())),
 
2227           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2230           t8('Save and Invoice'),
 
2231           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2232           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2233                          @req_trans_cost_art, @req_cusordnumber,
 
2235           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2238           ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
 
2239           call      => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::instance_conf->get_order_warn_duplicate_parts ],
 
2240           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2241                          @req_trans_cost_art, @req_cusordnumber,
 
2243           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2244                      : $has_final_invoice ? t8('This order has already a final invoice.')
 
2246           only_if   => (any { $self->type eq $_ } (sales_order_type())),
 
2249           t8('Save and Final Invoice'),
 
2250           call      => [ 'kivi.Order.save', 'save_and_final_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2251           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2252                          @req_trans_cost_art, @req_cusordnumber,
 
2254           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2255                      : $has_final_invoice ? t8('This order has already a final invoice.')
 
2257           only_if   => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
 
2260           t8('Save and AP Transaction'),
 
2261           call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
2262           only_if   => (any { $self->type eq $_ } (purchase_order_type())),
 
2263           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2266       ], # end of combobox "Workflow"
 
2273           t8('Save and preview PDF'),
 
2274           call     => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
2275                                                           $::instance_conf->get_order_warn_no_deliverydate,
 
2277           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2278           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2281           t8('Save and print'),
 
2282           call     => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
2283                                                          $::instance_conf->get_order_warn_no_deliverydate,
 
2285           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2286           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2289           t8('Save and E-mail'),
 
2290           id       => 'save_and_email_action',
 
2291           call     => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
2292                                                                          $::instance_conf->get_order_warn_no_deliverydate,
 
2294           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2295                     : !$self->order->id  ? t8('This object has not been saved yet.')
 
2299           t8('Download attachments of all parts'),
 
2300           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
2301           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2302           only_if  => $::instance_conf->get_doc_storage,
 
2304       ], # end of combobox "Export"
 
2308         call     => [ 'kivi.Order.delete_order' ],
 
2309         confirm  => $::locale->text('Do you really want to delete this object?'),
 
2310         disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2311                   : !$self->order->id  ? t8('This object has not been saved yet.')
 
2313         only_if  => $deletion_allowed,
 
2322           call     => [ 'set_history_window', $self->order->id, 'id' ],
 
2323           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
 
2327           call     => [ 'kivi.Order.follow_up_window' ],
 
2328           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2329           only_if  => $::auth->assert('productivity', 1),
 
2331       ], # end of combobox "more"
 
2337   my ($self, $doc_ref, $params) = @_;
 
2339   my $order  = $self->order;
 
2342   my $print_form = Form->new('');
 
2343   $print_form->{type}        = $order->type;
 
2344   $print_form->{formname}    = $params->{formname} || $order->type;
 
2345   $print_form->{format}      = $params->{format}   || 'pdf';
 
2346   $print_form->{media}       = $params->{media}    || 'file';
 
2347   $print_form->{groupitems}  = $params->{groupitems};
 
2348   $print_form->{printer_id}  = $params->{printer_id};
 
2349   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
2351   $order->language($params->{language});
 
2352   $order->flatten_to_form($print_form, format_amounts => 1);
 
2356   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
2357     $template_ext  = 'odt';
 
2358     $template_type = 'OpenDocument';
 
2359   } elsif ($print_form->{format} =~ m{html}i) {
 
2360     $template_ext  = 'html';
 
2361     $template_type = 'HTML';
 
2364   # search for the template
 
2365   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
2366     name        => $print_form->{formname},
 
2367     extension   => $template_ext,
 
2368     email       => $print_form->{media} eq 'email',
 
2369     language    => $params->{language},
 
2370     printer_id  => $print_form->{printer_id},
 
2373   if (!defined $template_file) {
 
2374     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);
 
2377   return @errors if scalar @errors;
 
2379   $print_form->throw_on_error(sub {
 
2381       $print_form->prepare_for_printing;
 
2383       $$doc_ref = SL::Helper::CreatePDF->create_pdf(
 
2384         format        => $print_form->{format},
 
2385         template_type => $template_type,
 
2386         template      => $template_file,
 
2387         variables     => $print_form,
 
2388         variable_content_types => {
 
2389           longdescription => 'html',
 
2390           partnotes       => 'html',
 
2392           $::form->get_variable_content_types_for_cvars,
 
2396     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2402 sub get_files_for_email_dialog {
 
2405   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2407   return %files if !$::instance_conf->get_doc_storage;
 
2409   if ($self->order->id) {
 
2410     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2411     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2412     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2413     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2417     uniq_by { $_->{id} }
 
2419       +{ id         => $_->part->id,
 
2420          partnumber => $_->part->partnumber }
 
2421     } @{$self->order->items_sorted};
 
2423   foreach my $part (@parts) {
 
2424     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2425     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2428   foreach my $key (keys %files) {
 
2429     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2435 sub make_periodic_invoices_config_from_yaml {
 
2436   my ($yaml_config) = @_;
 
2438   return if !$yaml_config;
 
2439   my $attr = SL::YAML::Load($yaml_config);
 
2440   return if 'HASH' ne ref $attr;
 
2441   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
2445 sub get_periodic_invoices_status {
 
2446   my ($self, $config) = @_;
 
2448   return                      if $self->type ne sales_order_type();
 
2449   return t8('not configured') if !$config;
 
2451   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
2452              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
2453              :                                                     die "Cannot get status of periodic invoices config";
 
2455   return $active ? t8('active') : t8('inactive');
 
2459   my ($self, $action) = @_;
 
2461   return '' if none { lc($action)} qw(add edit);
 
2464   # $::locale->text("Add Sales Order");
 
2465   # $::locale->text("Add Purchase Order");
 
2466   # $::locale->text("Add Quotation");
 
2467   # $::locale->text("Add Request for Quotation");
 
2468   # $::locale->text("Edit Sales Order");
 
2469   # $::locale->text("Edit Purchase Order");
 
2470   # $::locale->text("Edit Quotation");
 
2471   # $::locale->text("Edit Request for Quotation");
 
2473   $action = ucfirst(lc($action));
 
2474   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
2475        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
2476        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
2477        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
2481 sub get_item_cvpartnumber {
 
2482   my ($self, $item) = @_;
 
2484   return if !$self->search_cvpartnumber;
 
2485   return if !$self->order->customervendor;
 
2487   if ($self->cv eq 'vendor') {
 
2488     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2489     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2490   } elsif ($self->cv eq 'customer') {
 
2491     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2492     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2496 sub get_part_texts {
 
2497   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2499   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2500   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2502     description     => $defaults{description}     // $part->description,
 
2503     longdescription => $defaults{longdescription} // $part->notes,
 
2506   return $texts unless $language_id;
 
2508   my $translation = SL::DB::Manager::Translation->get_first(
 
2510       parts_id    => $part->id,
 
2511       language_id => $language_id,
 
2514   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2515   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2520 sub get_best_price_and_discount_source {
 
2521   my ($record, $item, $ignore_given) = @_;
 
2523   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
2526   if ( $item->part->is_assortment ) {
 
2527     # add assortment items with price 0, as the components carry the price
 
2528     $price_src = $price_source->price_from_source("");
 
2529     $price_src->price(0);
 
2530   } elsif (!$ignore_given && defined $item->sellprice) {
 
2531     $price_src = $price_source->price_from_source("");
 
2532     $price_src->price($item->sellprice);
 
2534     $price_src = $price_source->best_price
 
2535                ? $price_source->best_price
 
2536                : $price_source->price_from_source("");
 
2537     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
2538     $price_src->price(0) if !$price_source->best_price;
 
2542   if (!$ignore_given && defined $item->discount) {
 
2543     $discount_src = $price_source->discount_from_source("");
 
2544     $discount_src->discount($item->discount);
 
2546     $discount_src = $price_source->best_discount
 
2547                   ? $price_source->best_discount
 
2548                   : $price_source->discount_from_source("");
 
2549     $discount_src->discount(0) if !$price_source->best_discount;
 
2552   return ($price_src, $discount_src);
 
2555 sub sales_order_type {
 
2559 sub purchase_order_type {
 
2563 sub sales_quotation_type {
 
2567 sub request_quotation_type {
 
2568   'request_quotation';
 
2572   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
2573        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
2574        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
2575        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
2579 sub save_and_redirect_to {
 
2580   my ($self, %params) = @_;
 
2582   my $errors = $self->save();
 
2584   if (scalar @{ $errors }) {
 
2585     $self->js->flash('error', $_) foreach @{ $errors };
 
2586     return $self->js->render();
 
2589   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
2590            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
2591            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
2592            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
2594   flash_later('info', $text);
 
2596   $self->redirect_to(%params, id => $self->order->id);
 
2600   my ($self, $addition) = @_;
 
2602   my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
 
2603   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2605   SL::DB::History->new(
 
2606     trans_id    => $self->order->id,
 
2607     employee_id => SL::DB::Manager::Employee->current->id,
 
2608     what_done   => $self->order->type,
 
2609     snumbers    => $snumbers,
 
2610     addition    => $addition,
 
2614 sub store_doc_to_webdav_and_filemanagement {
 
2615   my ($self, $content, $filename, $variant) = @_;
 
2617   my $order = $self->order;
 
2620   # copy file to webdav folder
 
2621   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2622     my $webdav = SL::Webdav->new(
 
2623       type     => $order->type,
 
2624       number   => $order->number,
 
2626     my $webdav_file = SL::Webdav::File->new(
 
2628       filename => $filename,
 
2631       $webdav_file->store(data => \$content);
 
2634       push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
 
2637   if ($order->id && $::instance_conf->get_doc_storage) {
 
2639       SL::File->save(object_id     => $order->id,
 
2640                      object_type   => $order->type,
 
2641                      mime_type     => SL::MIME->mime_type_from_ext($filename),
 
2642                      source        => 'created',
 
2643                      file_type     => 'document',
 
2644                      file_name     => $filename,
 
2645                      file_contents => $content,
 
2646                      print_variant => $variant);
 
2649       push @errors, t8('Storing the document in the storage backend failed: #1', $@);
 
2656 sub link_requirement_specs_linking_to_created_from_objects {
 
2657   my ($self, @converted_from_oe_ids) = @_;
 
2659   return unless @converted_from_oe_ids;
 
2661   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
 
2662   foreach my $rs_order (@{ $rs_orders }) {
 
2663     SL::DB::RequirementSpecOrder->new(
 
2664       order_id            => $self->order->id,
 
2665       requirement_spec_id => $rs_order->requirement_spec_id,
 
2666       version_id          => $rs_order->version_id,
 
2671 sub set_project_in_linked_requirement_specs {
 
2674   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
 
2675   foreach my $rs_order (@{ $rs_orders }) {
 
2676     next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
 
2678     $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
 
2690 SL::Controller::Order - controller for orders
 
2694 This is a new form to enter orders, completely rewritten with the use
 
2695 of controller and java script techniques.
 
2697 The aim is to provide the user a better experience and a faster workflow. Also
 
2698 the code should be more readable, more reliable and better to maintain.
 
2706 One input row, so that input happens every time at the same place.
 
2710 Use of pickers where possible.
 
2714 Possibility to enter more than one item at once.
 
2718 Item list in a scrollable area, so that the workflow buttons stay at
 
2723 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2724 possible (by partnumber, description, qty, sellprice and discount for now).
 
2728 No C<update> is necessary. All entries and calculations are managed
 
2729 with ajax-calls and the page only reloads on C<save>.
 
2733 User can see changes immediately, because of the use of java script
 
2744 =item * C<SL/Controller/Order.pm>
 
2748 =item * C<template/webpages/order/form.html>
 
2752 =item * C<template/webpages/order/tabs/basic_data.html>
 
2754 Main tab for basic_data.
 
2756 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2757 reused from generic code.
 
2761 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
2763 For displaying information on business type
 
2765 =item * C<template/webpages/order/tabs/_item_input.html>
 
2767 The input line for items
 
2769 =item * C<template/webpages/order/tabs/_row.html>
 
2771 One row for already entered items
 
2773 =item * C<template/webpages/order/tabs/_tax_row.html>
 
2775 Displaying tax information
 
2777 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
2779 Dialog for selecting price and discount sources
 
2783 =item * C<js/kivi.Order.js>
 
2785 java script functions
 
2795 =item * price sources: little symbols showing better price / better discount
 
2797 =item * select units in input row?
 
2799 =item * check for direct delivery (workflow sales order -> purchase order)
 
2801 =item * access rights
 
2803 =item * display weights
 
2807 =item * optional client/user behaviour
 
2809 (transactions has to be set - department has to be set -
 
2810  force project if enabled in client config)
 
2814 =head1 KNOWN BUGS AND CAVEATS
 
2820 No indication that <shift>-up/down expands/collapses second row.
 
2824 Table header is not sticky in the scrolling area.
 
2828 Sorting does not include C<position>, neither does reordering.
 
2830 This behavior was implemented intentionally. But we can discuss, which behavior
 
2831 should be implemented.
 
2835 =head1 To discuss / Nice to have
 
2841 How to expand/collapse second row. Now it can be done clicking the icon or
 
2846 This controller uses a (changed) copy of the template for the PriceSource
 
2847 dialog. Maybe there could be used one code source.
 
2851 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2852 form. This is not only a problem here, but also in all parts using the PTC.
 
2853 There exists a ticket and a patch. This patch should be testet.
 
2857 An indicator, if the actual inputs are saved (like in an
 
2858 editor or on text processing application).
 
2862 A warning when leaving the page without saveing unchanged inputs.
 
2869 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>