1 package SL::Controller::Order;
 
   4 use parent qw(SL::Controller::Base);
 
   6 use SL::Helper::Flash qw(flash_later);
 
   8 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
 
   9 use SL::Locale::String qw(t8);
 
  10 use SL::SessionFile::Random;
 
  15 use SL::Util qw(trim);
 
  17 use SL::DB::AdditionalBillingAddress;
 
  24 use SL::DB::PartClassification;
 
  25 use SL::DB::PartsGroup;
 
  28 use SL::DB::RecordLink;
 
  29 use SL::DB::RequirementSpec;
 
  31 use SL::DB::Translation;
 
  33 use SL::Helper::CreatePDF qw(:all);
 
  34 use SL::Helper::PrintOptions;
 
  35 use SL::Helper::ShippedQty;
 
  36 use SL::Helper::UserPreferences::PositionsScrollbar;
 
  37 use SL::Helper::UserPreferences::UpdatePositions;
 
  39 use SL::Controller::Helper::GetModels;
 
  41 use List::Util qw(first sum0);
 
  42 use List::UtilsBy qw(sort_by uniq_by);
 
  43 use List::MoreUtils qw(any none pairwise first_index);
 
  44 use English qw(-no_match_vars);
 
  49 use Rose::Object::MakeMethods::Generic
 
  51  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 
  52  'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
 
  57 __PACKAGE__->run_before('check_auth');
 
  59 __PACKAGE__->run_before('check_auth_for_edit',
 
  60                         except => [ qw(edit show_customer_vendor_details_dialog price_popup load_second_rows) ]);
 
  62 __PACKAGE__->run_before('recalc',
 
  63                         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
 
  66 __PACKAGE__->run_before('get_unalterable_data',
 
  67                         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
 
  78   $self->order->transdate(DateTime->now_local());
 
  79   my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
  80                    $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
  82   if (   ($self->type eq sales_order_type()     &&  $::instance_conf->get_deliverydate_on)
 
  83       || ($self->type eq sales_quotation_type() &&  $::instance_conf->get_reqdate_on)
 
  84       && (!$self->order->reqdate)) {
 
  85     $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
 
  92     title => $self->get_title_for('add'),
 
  93     %{$self->{template_args}}
 
  97 # edit an existing order
 
 105     # this is to edit an order from an unsaved order object
 
 107     # set item ids to new fake id, to identify them as new items
 
 108     foreach my $item (@{$self->order->items_sorted}) {
 
 109       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 111     # trigger rendering values for second row as hidden, because they
 
 112     # are loaded only on demand. So we need to keep the values from
 
 114     $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
 121     title => $self->get_title_for('edit'),
 
 122     %{$self->{template_args}}
 
 126 # edit a collective order (consisting of one or more existing orders)
 
 127 sub action_edit_collective {
 
 131   my @multi_ids = map {
 
 132     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 133   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 135   # fall back to add if no ids are given
 
 136   if (scalar @multi_ids == 0) {
 
 141   # fall back to save as new if only one id is given
 
 142   if (scalar @multi_ids == 1) {
 
 143     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 144     $self->action_save_as_new();
 
 148   # make new order from given orders
 
 149   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 150   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 151   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 153   $self->action_edit();
 
 160   my $errors = $self->delete();
 
 162   if (scalar @{ $errors }) {
 
 163     $self->js->flash('error', $_) foreach @{ $errors };
 
 164     return $self->js->render();
 
 167   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 168            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 169            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 170            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 172   flash_later('info', $text);
 
 174   my @redirect_params = (
 
 179   $self->redirect_to(@redirect_params);
 
 186   my $errors = $self->save();
 
 188   if (scalar @{ $errors }) {
 
 189     $self->js->flash('error', $_) foreach @{ $errors };
 
 190     return $self->js->render();
 
 193   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 194            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 195            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 196            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 198   flash_later('info', $text);
 
 201   if ($::form->{back_to_caller}) {
 
 202     @redirect_params = $::form->{callback} ? ($::form->{callback})
 
 203                                            : (controller => 'LoginScreen', action => 'user_login');
 
 209       id       => $self->order->id,
 
 210       callback => $::form->{callback},
 
 214   $self->redirect_to(@redirect_params);
 
 217 # save the order as new document an open it for edit
 
 218 sub action_save_as_new {
 
 221   my $order = $self->order;
 
 224     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 225     return $self->js->render();
 
 228   # load order from db to check if values changed
 
 229   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 232   # Lets assign a new number if the user hasn't changed the previous one.
 
 233   # If it has been changed manually then use it as-is.
 
 234   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 236                         : trim($order->number);
 
 238   # Clear transdate unless changed
 
 239   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 240                         ? DateTime->today_local
 
 243   # Set new reqdate unless changed if it is enabled in client config
 
 244   if ($order->reqdate == $saved_order->reqdate) {
 
 245     my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
 246                      $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
 248     if (   ($self->type eq sales_order_type()     &&  !$::instance_conf->get_deliverydate_on)
 
 249         || ($self->type eq sales_quotation_type() &&  !$::instance_conf->get_reqdate_on)) {
 
 250       $new_attrs{reqdate} = '';
 
 252       $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 255     $new_attrs{reqdate} = $order->reqdate;
 
 259   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 261   # Create new record from current one
 
 262   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 264   # no linked records on save as new
 
 265   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 268   $self->action_save();
 
 273 # This is called if "print" is pressed in the print dialog.
 
 274 # If PDF creation was requested and succeeded, the pdf is offered for download
 
 275 # via send_file (which uses ajax in this case).
 
 279   my $errors = $self->save();
 
 281   if (scalar @{ $errors }) {
 
 282     $self->js->flash('error', $_) foreach @{ $errors };
 
 283     return $self->js->render();
 
 286   $self->js_reset_order_and_item_ids_after_save;
 
 288   my $format      = $::form->{print_options}->{format};
 
 289   my $media       = $::form->{print_options}->{media};
 
 290   my $formname    = $::form->{print_options}->{formname};
 
 291   my $copies      = $::form->{print_options}->{copies};
 
 292   my $groupitems  = $::form->{print_options}->{groupitems};
 
 293   my $printer_id  = $::form->{print_options}->{printer_id};
 
 295   # only PDF, OpenDocument & HTML for now
 
 296   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
 
 297     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 300   # only screen or printer by now
 
 301   if (none { $media eq $_ } qw(screen printer)) {
 
 302     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 305   # create a form for generate_attachment_filename
 
 306   my $form   = Form->new;
 
 307   $form->{$self->nr_key()}  = $self->order->number;
 
 308   $form->{type}             = $self->type;
 
 309   $form->{format}           = $format;
 
 310   $form->{formname}         = $formname;
 
 311   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 312   my $doc_filename          = $form->generate_attachment_filename();
 
 315   my @errors = $self->generate_doc(\$doc, { media      => $media,
 
 317                                             formname   => $formname,
 
 318                                             language   => $self->order->language,
 
 319                                             printer_id => $printer_id,
 
 320                                             groupitems => $groupitems });
 
 321   if (scalar @errors) {
 
 322     return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
 
 325   if ($media eq 'screen') {
 
 327     $self->js->flash('info', t8('The document has been created.'));
 
 330       type         => SL::MIME->mime_type_from_ext($doc_filename),
 
 331       name         => $doc_filename,
 
 335   } elsif ($media eq 'printer') {
 
 337     my $printer_id = $::form->{print_options}->{printer_id};
 
 338     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 343     $self->js->flash('info', t8('The document has been printed.'));
 
 346   my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
 
 347   if (scalar @warnings) {
 
 348     $self->js->flash('warning', $_) for @warnings;
 
 351   $self->save_history('PRINTED');
 
 354     ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
 
 357 sub action_preview_pdf {
 
 360   my $errors = $self->save();
 
 361   if (scalar @{ $errors }) {
 
 362     $self->js->flash('error', $_) foreach @{ $errors };
 
 363     return $self->js->render();
 
 366   $self->js_reset_order_and_item_ids_after_save;
 
 369   my $media       = 'screen';
 
 370   my $formname    = $self->type;
 
 373   # create a form for generate_attachment_filename
 
 374   my $form   = Form->new;
 
 375   $form->{$self->nr_key()}  = $self->order->number;
 
 376   $form->{type}             = $self->type;
 
 377   $form->{format}           = $format;
 
 378   $form->{formname}         = $formname;
 
 379   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 380   my $pdf_filename          = $form->generate_attachment_filename();
 
 383   my @errors = $self->generate_doc(\$pdf, { media      => $media,
 
 385                                             formname   => $formname,
 
 386                                             language   => $self->order->language,
 
 388   if (scalar @errors) {
 
 389     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 391   $self->save_history('PREVIEWED');
 
 392   $self->js->flash('info', t8('The PDF has been previewed'));
 
 396     type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 397     name         => $pdf_filename,
 
 402 # open the email dialog
 
 403 sub action_save_and_show_email_dialog {
 
 406   my $errors = $self->save();
 
 408   if (scalar @{ $errors }) {
 
 409     $self->js->flash('error', $_) foreach @{ $errors };
 
 410     return $self->js->render();
 
 413   my $cv_method = $self->cv;
 
 415   if (!$self->order->$cv_method) {
 
 416     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'))
 
 421   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 422   $email_form->{to} ||= $self->order->$cv_method->email;
 
 423   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 424   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 425   # Todo: get addresses from shipto, if any
 
 427   my $form = Form->new;
 
 428   $form->{$self->nr_key()}  = $self->order->number;
 
 429   $form->{cusordnumber}     = $self->order->cusordnumber;
 
 430   $form->{formname}         = $self->type;
 
 431   $form->{type}             = $self->type;
 
 432   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 433   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
 
 434   $form->{format}           = 'pdf';
 
 435   $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
 437   $email_form->{subject}             = $form->generate_email_subject();
 
 438   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 439   $email_form->{message}             = $form->generate_email_body();
 
 440   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 442   my %files = $self->get_files_for_email_dialog();
 
 444   my @employees_with_email = grep {
 
 445     my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
 
 446     $user && !!trim($user->get_config_value('email'));
 
 447   } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
 
 449   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 450                                   email_form    => $email_form,
 
 451                                   show_bcc      => $::auth->assert('email_bcc', 'may fail'),
 
 453                                   is_customer   => $self->cv eq 'customer',
 
 454                                   ALL_EMPLOYEES => \@employees_with_email,
 
 458       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 465 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 466 sub action_send_email {
 
 469   my $errors = $self->save();
 
 471   if (scalar @{ $errors }) {
 
 472     $self->js->run('kivi.Order.close_email_dialog');
 
 473     $self->js->flash('error', $_) foreach @{ $errors };
 
 474     return $self->js->render();
 
 477   $self->js_reset_order_and_item_ids_after_save;
 
 479   my $email_form  = delete $::form->{email_form};
 
 480   my %field_names = (to => 'email');
 
 482   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 484   # for Form::cleanup which may be called in Form::send_email
 
 485   $::form->{cwd}    = getcwd();
 
 486   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 488   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 489   $::form->{media}  = 'email';
 
 491   $::form->{attachment_policy} //= '';
 
 493   # Is an old file version available?
 
 495   if ($::form->{attachment_policy} eq 'old_file') {
 
 496     $attfile = SL::File->get_all(object_id     => $self->order->id,
 
 497                                  object_type   => $self->type,
 
 498                                  file_type     => 'document',
 
 499                                  print_variant => $::form->{formname});
 
 502   if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
 
 504     my @errors = $self->generate_doc(\$doc, {media      => $::form->{media},
 
 505                                              format     => $::form->{print_options}->{format},
 
 506                                              formname   => $::form->{print_options}->{formname},
 
 507                                              language   => $self->order->language,
 
 508                                              printer_id => $::form->{print_options}->{printer_id},
 
 509                                              groupitems => $::form->{print_options}->{groupitems}});
 
 510     if (scalar @errors) {
 
 511       return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
 
 514     my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename}, $::form->{formname});
 
 515     if (scalar @warnings) {
 
 516       flash_later('warning', $_) for @warnings;
 
 519     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 520     $sfile->fh->print($doc);
 
 523     $::form->{tmpfile} = $sfile->file_name;
 
 524     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 527   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 528   $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
 
 530   # internal notes unless no email journal
 
 531   unless ($::instance_conf->get_email_journal) {
 
 532     my $intnotes = $self->order->intnotes;
 
 533     $intnotes   .= "\n\n" if $self->order->intnotes;
 
 534     $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 535     $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 536     $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 537     $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 538     $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 539     $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 540     $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
 
 542     $self->order->update_attributes(intnotes => $intnotes);
 
 545   $self->save_history('MAILED');
 
 547   flash_later('info', t8('The email has been sent.'));
 
 549   my @redirect_params = (
 
 552     id     => $self->order->id,
 
 555   $self->redirect_to(@redirect_params);
 
 558 # open the periodic invoices config dialog
 
 560 # If there are values in the form (i.e. dialog was opened before),
 
 561 # then use this values. Create new ones, else.
 
 562 sub action_show_periodic_invoices_config_dialog {
 
 565   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 566   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 567   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 568                                                    order_value_periodicity => 'p', # = same as periodicity
 
 569                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 570                                                    extend_automatically_by => 12,
 
 572                                                    email_subject           => GenericTranslations->get(
 
 573                                                                                 language_id      => $::form->{language_id},
 
 574                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 575                                                    email_body              => GenericTranslations->get(
 
 576                                                                                 language_id      => $::form->{language_id},
 
 577                                                                                 translation_type => "salutation_general")
 
 578                                                                             . GenericTranslations->get(
 
 579                                                                                 language_id      => $::form->{language_id},
 
 580                                                                                 translation_type => "salutation_punctuation_mark") . "\n\n"
 
 581                                                                             . GenericTranslations->get(
 
 582                                                                                 language_id      => $::form->{language_id},
 
 583                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 585   # for older configs, replace email preset text if not yet set.
 
 586   $config->email_subject(GenericTranslations->get(
 
 587                                               language_id      => $::form->{language_id},
 
 588                                               translation_type =>"preset_text_periodic_invoices_email_subject")
 
 589                         ) unless $config->email_subject;
 
 591   $config->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")
 
 600                      ) unless $config->email_body;
 
 602   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 603   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 605   $::form->get_lists(printers => "ALL_PRINTERS",
 
 606                      charts   => { key       => 'ALL_CHARTS',
 
 607                                    transdate => 'current_date' });
 
 609   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 611   if ($::form->{customer_id}) {
 
 612     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 613     my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
 
 614     $::form->{postal_invoice}                  = $customer_object->postal_invoice;
 
 615     $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
 
 616     $config->send_email(0) if $::form->{postal_invoice};
 
 619   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 621                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 622                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 627 # assign the values of the periodic invoices config dialog
 
 628 # as yaml in the hidden tag and set the status.
 
 629 sub action_assign_periodic_invoices_config {
 
 632   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 634   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 635                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 636                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 637                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 638                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 639                  start_date_as_date         => $::form->{start_date_as_date},
 
 640                  end_date_as_date           => $::form->{end_date_as_date},
 
 641                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 642                  print                      => $::form->{print}      ? 1                         : 0,
 
 643                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 644                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 645                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 646                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 647                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 648                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 649                  email_recipient_address    => $::form->{email_recipient_address},
 
 650                  email_sender               => $::form->{email_sender},
 
 651                  email_subject              => $::form->{email_subject},
 
 652                  email_body                 => $::form->{email_body},
 
 655   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 657   my $status = $self->get_periodic_invoices_status($config);
 
 660     ->remove('#order_periodic_invoices_config')
 
 661     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 662     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 663     ->html('#periodic_invoices_status', $status)
 
 664     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 668 sub action_get_has_active_periodic_invoices {
 
 671   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 672   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 674   my $has_active_periodic_invoices =
 
 675        $self->type eq sales_order_type()
 
 678     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 679     && $config->get_previous_billed_period_start_date;
 
 681   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 684 # save the order and redirect to the frontend subroutine for a new
 
 686 sub action_save_and_delivery_order {
 
 689   $self->save_and_redirect_to(
 
 690     controller => 'oe.pl',
 
 691     action     => 'oe_delivery_order_from_order',
 
 695 sub action_save_and_supplier_delivery_order {
 
 698   $self->save_and_redirect_to(
 
 699     controller => 'controller.pl',
 
 700     action     => 'DeliveryOrder/add_from_order',
 
 701     type       => 'supplier_delivery_order',
 
 705 # save the order and redirect to the frontend subroutine for a new
 
 707 sub action_save_and_invoice {
 
 710   $self->save_and_redirect_to(
 
 711     controller => 'oe.pl',
 
 712     action     => 'oe_invoice_from_order',
 
 716 sub action_save_and_invoice_for_advance_payment {
 
 719   $self->save_and_redirect_to(
 
 720     controller       => 'oe.pl',
 
 721     action           => 'oe_invoice_from_order',
 
 722     new_invoice_type => 'invoice_for_advance_payment',
 
 726 sub action_save_and_final_invoice {
 
 729   $self->save_and_redirect_to(
 
 730     controller       => 'oe.pl',
 
 731     action           => 'oe_invoice_from_order',
 
 732     new_invoice_type => 'final_invoice',
 
 736 # workflow from sales order to sales quotation
 
 737 sub action_sales_quotation {
 
 738   $_[0]->workflow_sales_or_request_for_quotation();
 
 741 # workflow from sales order to sales quotation
 
 742 sub action_request_for_quotation {
 
 743   $_[0]->workflow_sales_or_request_for_quotation();
 
 746 # workflow from sales quotation to sales order
 
 747 sub action_sales_order {
 
 748   $_[0]->workflow_sales_or_purchase_order();
 
 751 # workflow from rfq to purchase order
 
 752 sub action_purchase_order {
 
 753   $_[0]->workflow_sales_or_purchase_order();
 
 756 # workflow from purchase order to ap transaction
 
 757 sub action_save_and_ap_transaction {
 
 760   $self->save_and_redirect_to(
 
 761     controller => 'ap.pl',
 
 762     action     => 'add_from_purchase_order',
 
 766 # set form elements in respect to a changed customer or vendor
 
 768 # This action is called on an change of the customer/vendor picker.
 
 769 sub action_customer_vendor_changed {
 
 772   setup_order_from_cv($self->order);
 
 775   my $cv_method = $self->cv;
 
 777   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 778     $self->js->show('#cp_row');
 
 780     $self->js->hide('#cp_row');
 
 783   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 784     $self->js->show('#shipto_selection');
 
 786     $self->js->hide('#shipto_selection');
 
 789   if ($cv_method eq 'customer') {
 
 790     my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
 
 791     $self->js->$show_hide('#billing_address_row');
 
 794   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 797     ->replaceWith('#order_cp_id',              $self->build_contact_select)
 
 798     ->replaceWith('#order_shipto_id',          $self->build_shipto_select)
 
 799     ->replaceWith('#shipto_inputs  ',          $self->build_shipto_inputs)
 
 800     ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
 
 801     ->replaceWith('#business_info_row',        $self->build_business_info_row)
 
 802     ->val(        '#order_taxzone_id',         $self->order->taxzone_id)
 
 803     ->val(        '#order_taxincluded',        $self->order->taxincluded)
 
 804     ->val(        '#order_currency_id',        $self->order->currency_id)
 
 805     ->val(        '#order_payment_id',         $self->order->payment_id)
 
 806     ->val(        '#order_delivery_term_id',   $self->order->delivery_term_id)
 
 807     ->val(        '#order_intnotes',           $self->order->intnotes)
 
 808     ->val(        '#order_language_id',        $self->order->$cv_method->language_id)
 
 809     ->focus(      '#order_' . $self->cv . '_id')
 
 810     ->run('kivi.Order.update_exchangerate');
 
 812   $self->js_redisplay_amounts_and_taxes;
 
 813   $self->js_redisplay_cvpartnumbers;
 
 817 # open the dialog for customer/vendor details
 
 818 sub action_show_customer_vendor_details_dialog {
 
 821   my $is_customer = 'customer' eq $::form->{vc};
 
 824     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 826     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 829   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 830   $details{discount_as_percent} = $cv->discount_as_percent;
 
 831   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 832   $details{business}            = $cv->business->description      if $cv->business;
 
 833   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 834   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 835   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 836   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 839     foreach my $entry (@{ $cv->additional_billing_addresses }) {
 
 840       push @{ $details{ADDITIONAL_BILLING_ADDRESSES} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 843   foreach my $entry (@{ $cv->shipto }) {
 
 844     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 846   foreach my $entry (@{ $cv->contacts }) {
 
 847     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 850   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 851                 is_customer => $is_customer,
 
 856 # called if a unit in an existing item row is changed
 
 857 sub action_unit_changed {
 
 860   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 861   my $item = $self->order->items_sorted->[$idx];
 
 863   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 864   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 869     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 870   $self->js_redisplay_line_values;
 
 871   $self->js_redisplay_amounts_and_taxes;
 
 875 # add an item row for a new item entered in the input row
 
 876 sub action_add_item {
 
 879   delete $::form->{add_item}->{create_part_type};
 
 881   my $form_attr = $::form->{add_item};
 
 883   return unless $form_attr->{parts_id};
 
 885   my $item = new_item($self->order, $form_attr);
 
 887   $self->order->add_items($item);
 
 891   $self->get_item_cvpartnumber($item);
 
 893   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 894   my $row_as_html = $self->p->render('order/tabs/_row',
 
 900   if ($::form->{insert_before_item_id}) {
 
 902       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 905       ->append('#row_table_id', $row_as_html);
 
 908   if ( $item->part->is_assortment ) {
 
 909     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 910     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 911       my $attr = { parts_id => $assortment_item->parts_id,
 
 912                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 913                    unit     => $assortment_item->unit,
 
 914                    description => $assortment_item->part->description,
 
 916       my $item = new_item($self->order, $attr);
 
 918       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 919       $item->discount(1) unless $assortment_item->charge;
 
 921       $self->order->add_items( $item );
 
 923       $self->get_item_cvpartnumber($item);
 
 924       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 925       my $row_as_html = $self->p->render('order/tabs/_row',
 
 930       if ($::form->{insert_before_item_id}) {
 
 932           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 935           ->append('#row_table_id', $row_as_html);
 
 941     ->val('.add_item_input', '')
 
 942     ->run('kivi.Order.init_row_handlers')
 
 943     ->run('kivi.Order.renumber_positions')
 
 944     ->focus('#add_item_parts_id_name');
 
 946   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 948   $self->js_redisplay_amounts_and_taxes;
 
 952 # add item rows for multiple items at once
 
 953 sub action_add_multi_items {
 
 956   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
 
 957   return $self->js->render() unless scalar @form_attr;
 
 960   foreach my $attr (@form_attr) {
 
 961     my $item = new_item($self->order, $attr);
 
 963     if ( $item->part->is_assortment ) {
 
 964       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 965         my $attr = { parts_id => $assortment_item->parts_id,
 
 966                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 967                      unit     => $assortment_item->unit,
 
 968                      description => $assortment_item->part->description,
 
 970         my $item = new_item($self->order, $attr);
 
 972         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 973         $item->discount(1) unless $assortment_item->charge;
 
 978   $self->order->add_items(@items);
 
 982   foreach my $item (@items) {
 
 983     $self->get_item_cvpartnumber($item);
 
 984     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 985     my $row_as_html = $self->p->render('order/tabs/_row',
 
 991     if ($::form->{insert_before_item_id}) {
 
 993         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 996         ->append('#row_table_id', $row_as_html);
 
1001     ->run('kivi.Part.close_picker_dialogs')
 
1002     ->run('kivi.Order.init_row_handlers')
 
1003     ->run('kivi.Order.renumber_positions')
 
1004     ->focus('#add_item_parts_id_name');
 
1006   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
1008   $self->js_redisplay_amounts_and_taxes;
 
1009   $self->js->render();
 
1012 # recalculate all linetotals, amounts and taxes and redisplay them
 
1013 sub action_recalc_amounts_and_taxes {
 
1018   $self->js_redisplay_line_values;
 
1019   $self->js_redisplay_amounts_and_taxes;
 
1020   $self->js->render();
 
1023 sub action_update_exchangerate {
 
1027     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
1028     currency_name => $self->order->currency->name,
 
1029     exchangerate  => $self->order->daily_exchangerate_as_null_number,
 
1032   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
1035 # redisplay item rows if they are sorted by an attribute
 
1036 sub action_reorder_items {
 
1040     partnumber   => sub { $_[0]->part->partnumber },
 
1041     description  => sub { $_[0]->description },
 
1042     qty          => sub { $_[0]->qty },
 
1043     sellprice    => sub { $_[0]->sellprice },
 
1044     discount     => sub { $_[0]->discount },
 
1045     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
1048   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1050   my $method = $sort_keys{$::form->{order_by}};
 
1051   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
1052   if ($::form->{sort_dir}) {
 
1053     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1054       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
1056       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
1059     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1060       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
1062       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
1066     ->run('kivi.Order.redisplay_items', \@to_sort)
 
1070 # show the popup to choose a price/discount source
 
1071 sub action_price_popup {
 
1074   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
1075   my $item = $self->order->items_sorted->[$idx];
 
1077   $self->render_price_dialog($item);
 
1080 # save the order in a session variable and redirect to the part controller
 
1081 sub action_create_part {
 
1084   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
 
1086   my $callback     = $self->url_for(
 
1087     action       => 'return_from_create_part',
 
1088     type         => $self->type, # type is needed for check_auth on return
 
1089     previousform => $previousform,
 
1092   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.'));
 
1094   my @redirect_params = (
 
1095     controller => 'Part',
 
1097     part_type  => $::form->{add_item}->{create_part_type},
 
1098     callback   => $callback,
 
1102   $self->redirect_to(@redirect_params);
 
1105 sub action_return_from_create_part {
 
1108   $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
 
1110   $::auth->restore_form_from_session(delete $::form->{previousform});
 
1112   # set item ids to new fake id, to identify them as new items
 
1113   foreach my $item (@{$self->order->items_sorted}) {
 
1114     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1118   $self->get_unalterable_data();
 
1119   $self->pre_render();
 
1121   # trigger rendering values for second row/longdescription as hidden,
 
1122   # because they are loaded only on demand. So we need to keep the values
 
1124   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1125   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1129     title => $self->get_title_for('edit'),
 
1130     %{$self->{template_args}}
 
1135 # load the second row for one or more items
 
1137 # This action gets the html code for all items second rows by rendering a template for
 
1138 # the second row and sets the html code via client js.
 
1139 sub action_load_second_rows {
 
1142   $self->recalc() if $self->order->is_sales; # for margin calculation
 
1144   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1145     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1146     my $item = $self->order->items_sorted->[$idx];
 
1148     $self->js_load_second_row($item, $item_id, 0);
 
1151   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
1153   $self->js->render();
 
1156 # update description, notes and sellprice from master data
 
1157 sub action_update_row_from_master_data {
 
1160   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1161     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1162     my $item  = $self->order->items_sorted->[$idx];
 
1163     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1165     $item->description($texts->{description});
 
1166     $item->longdescription($texts->{longdescription});
 
1168     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1171     if ($item->part->is_assortment) {
 
1172     # add assortment items with price 0, as the components carry the price
 
1173       $price_src = $price_source->price_from_source("");
 
1174       $price_src->price(0);
 
1176       $price_src = $price_source->best_price
 
1177                  ? $price_source->best_price
 
1178                  : $price_source->price_from_source("");
 
1179       $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
 
1180       $price_src->price(0) if !$price_source->best_price;
 
1184     $item->sellprice($price_src->price);
 
1185     $item->active_price_source($price_src);
 
1188       ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
 
1189       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1190       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1191       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1193     if ($self->search_cvpartnumber) {
 
1194       $self->get_item_cvpartnumber($item);
 
1195       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1200   $self->js_redisplay_line_values;
 
1201   $self->js_redisplay_amounts_and_taxes;
 
1203   $self->js->render();
 
1206 sub js_load_second_row {
 
1207   my ($self, $item, $item_id, $do_parse) = @_;
 
1210     # Parse values from form (they are formated while rendering (template)).
 
1211     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1212     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1213     foreach my $var (@{ $item->cvars_by_config }) {
 
1214       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1216     $item->parse_custom_variable_values;
 
1219   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1222     ->html('#second_row_' . $item_id, $row_as_html)
 
1223     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1226 sub js_redisplay_line_values {
 
1229   my $is_sales = $self->order->is_sales;
 
1231   # sales orders with margins
 
1236        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1237        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1238        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1239       ]} @{ $self->order->items_sorted };
 
1243        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1244       ]} @{ $self->order->items_sorted };
 
1248     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1251 sub js_redisplay_amounts_and_taxes {
 
1254   if (scalar @{ $self->{taxes} }) {
 
1255     $self->js->show('#taxincluded_row_id');
 
1257     $self->js->hide('#taxincluded_row_id');
 
1260   if ($self->order->taxincluded) {
 
1261     $self->js->hide('#subtotal_row_id');
 
1263     $self->js->show('#subtotal_row_id');
 
1266   if ($self->order->is_sales) {
 
1267     my $is_neg = $self->order->marge_total < 0;
 
1269       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1270       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1271       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1272       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1273       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1274       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1275       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1276       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1280     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1281     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1282     ->remove('.tax_row')
 
1283     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1286 sub js_redisplay_cvpartnumbers {
 
1289   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1291   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1294     ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
 
1297 sub js_reset_order_and_item_ids_after_save {
 
1301     ->val('#id', $self->order->id)
 
1302     ->val('#converted_from_oe_id', '')
 
1303     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1306   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1307     next if !$self->order->items_sorted->[$idx]->id;
 
1308     next if $form_item_id !~ m{^new};
 
1310       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1311       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1312       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1316   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1323 sub init_valid_types {
 
1324   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1330   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1331     die "Not a valid type for order";
 
1334   $self->type($::form->{type});
 
1340   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1341          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1342          : die "Not a valid type for order";
 
1347 sub init_search_cvpartnumber {
 
1350   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1351   my $search_cvpartnumber;
 
1352   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1353   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1355   return $search_cvpartnumber;
 
1358 sub init_show_update_button {
 
1361   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1372 sub init_all_price_factors {
 
1373   SL::DB::Manager::PriceFactor->get_all;
 
1376 sub init_part_picker_classification_ids {
 
1378   my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
 
1380   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
 
1386   my $right_for = { map { $_ => $_.'_edit' . ' | ' . $_.'_view' } @{$self->valid_types} };
 
1388   my $right   = $right_for->{ $self->type };
 
1389   $right    ||= 'DOES_NOT_EXIST';
 
1391   $::auth->assert($right);
 
1394 sub check_auth_for_edit {
 
1397   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1399   my $right   = $right_for->{ $self->type };
 
1400   $right    ||= 'DOES_NOT_EXIST';
 
1402   $::auth->assert($right);
 
1405 # build the selection box for contacts
 
1407 # Needed, if customer/vendor changed.
 
1408 sub build_contact_select {
 
1411   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1412     value_key  => 'cp_id',
 
1413     title_key  => 'full_name_dep',
 
1414     default    => $self->order->cp_id,
 
1416     style      => 'width: 300px',
 
1420 # build the selection box for the additional billing address
 
1422 # Needed, if customer/vendor changed.
 
1423 sub build_billing_address_select {
 
1426   return '' if $self->cv ne 'customer';
 
1428   select_tag('order.billing_address_id',
 
1429              [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
 
1431              title_key  => 'displayable_id',
 
1432              default    => $self->order->billing_address_id,
 
1434              style      => 'width: 300px',
 
1438 # build the selection box for shiptos
 
1440 # Needed, if customer/vendor changed.
 
1441 sub build_shipto_select {
 
1444   select_tag('order.shipto_id',
 
1445              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1446              value_key  => 'shipto_id',
 
1447              title_key  => 'displayable_id',
 
1448              default    => $self->order->shipto_id,
 
1450              style      => 'width: 300px',
 
1454 # build the inputs for the cusom shipto dialog
 
1456 # Needed, if customer/vendor changed.
 
1457 sub build_shipto_inputs {
 
1460   my $content = $self->p->render('common/_ship_to_dialog',
 
1461                                  vc_obj      => $self->order->customervendor,
 
1462                                  cs_obj      => $self->order->custom_shipto,
 
1463                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1464                                  id_selector => '#order_shipto_id');
 
1466   div_tag($content, id => 'shipto_inputs');
 
1469 # render the info line for business
 
1471 # Needed, if customer/vendor changed.
 
1472 sub build_business_info_row
 
1474   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1477 # build the rows for displaying taxes
 
1479 # Called if amounts where recalculated and redisplayed.
 
1480 sub build_tax_rows {
 
1484   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1485     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1487   return $rows_as_html;
 
1491 sub render_price_dialog {
 
1492   my ($self, $record_item) = @_;
 
1494   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1498       'kivi.io.price_chooser_dialog',
 
1499       t8('Available Prices'),
 
1500       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1505 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1506 #     $self->js->show('#dialog_flash_error');
 
1515   return if !$::form->{id};
 
1517   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1519   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1520   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1521   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1523   return $self->order;
 
1526 # load or create a new order object
 
1528 # And assign changes from the form to this object.
 
1529 # If the order is loaded from db, check if items are deleted in the form,
 
1530 # remove them form the object and collect them for removing from db on saving.
 
1531 # Then create/update items from form (via make_item) and add them.
 
1535   # add_items adds items to an order with no items for saving, but they cannot
 
1536   # be retrieved via items until the order is saved. Adding empty items to new
 
1537   # order here solves this problem.
 
1539   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1540   $order ||= SL::DB::Order->new(orderitems  => [],
 
1541                                 quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
 
1542                                 currency_id => $::instance_conf->get_currency_id(),);
 
1544   my $cv_id_method = $self->cv . '_id';
 
1545   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1546     $order->$cv_id_method($::form->{$cv_id_method});
 
1547     setup_order_from_cv($order);
 
1550   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1551   my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
 
1553   $order->assign_attributes(%{$::form->{order}});
 
1555   $self->setup_custom_shipto_from_form($order, $::form);
 
1557   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1558     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1559     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1562   # remove deleted items
 
1563   $self->item_ids_to_delete([]);
 
1564   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1565     my $item = $order->orderitems->[$idx];
 
1566     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1567       splice @{$order->orderitems}, $idx, 1;
 
1568       push @{$self->item_ids_to_delete}, $item->id;
 
1574   foreach my $form_attr (@{$form_orderitems}) {
 
1575     my $item = make_item($order, $form_attr);
 
1576     $item->position($pos);
 
1580   $order->add_items(grep {!$_->id} @items);
 
1585 # create or update items from form
 
1587 # Make item objects from form values. For items already existing read from db.
 
1588 # Create a new item else. And assign attributes.
 
1590   my ($record, $attr) = @_;
 
1593   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1595   my $is_new = !$item;
 
1597   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1598   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1599   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1600   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1602   $item->assign_attributes(%$attr);
 
1605     my $texts = get_part_texts($item->part, $record->language_id);
 
1606     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1607     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1608     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1616 # This is used to add one item
 
1618   my ($record, $attr) = @_;
 
1620   my $item = SL::DB::OrderItem->new;
 
1622   # Remove attributes where the user left or set the inputs empty.
 
1623   # So these attributes will be undefined and we can distinguish them
 
1624   # from zero later on.
 
1625   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1626     delete $attr->{$_} if $attr->{$_} eq '';
 
1629   $item->assign_attributes(%$attr);
 
1631   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1632   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1634   $item->unit($part->unit) if !$item->unit;
 
1637   if ( $part->is_assortment ) {
 
1638     # add assortment items with price 0, as the components carry the price
 
1639     $price_src = $price_source->price_from_source("");
 
1640     $price_src->price(0);
 
1641   } elsif (defined $item->sellprice) {
 
1642     $price_src = $price_source->price_from_source("");
 
1643     $price_src->price($item->sellprice);
 
1645     $price_src = $price_source->best_price
 
1646                ? $price_source->best_price
 
1647                : $price_source->price_from_source("");
 
1648     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
1649     $price_src->price(0) if !$price_source->best_price;
 
1653   if (defined $item->discount) {
 
1654     $discount_src = $price_source->discount_from_source("");
 
1655     $discount_src->discount($item->discount);
 
1657     $discount_src = $price_source->best_discount
 
1658                   ? $price_source->best_discount
 
1659                   : $price_source->discount_from_source("");
 
1660     $discount_src->discount(0) if !$price_source->best_discount;
 
1664   $new_attr{part}                   = $part;
 
1665   $new_attr{description}            = $part->description     if ! $item->description;
 
1666   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1667   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1668   $new_attr{sellprice}              = $price_src->price;
 
1669   $new_attr{discount}               = $discount_src->discount;
 
1670   $new_attr{active_price_source}    = $price_src;
 
1671   $new_attr{active_discount_source} = $discount_src;
 
1672   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1673   $new_attr{project_id}             = $record->globalproject_id;
 
1674   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1676   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1677   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1678   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1679   $new_attr{custom_variables} = [];
 
1681   my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1683   $item->assign_attributes(%new_attr, %{ $texts });
 
1688 sub setup_order_from_cv {
 
1691   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id language_id));
 
1693   $order->intnotes($order->customervendor->notes);
 
1695   return if !$order->is_sales;
 
1697   $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1698   $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1699                       ? $order->customer->taxincluded_checked
 
1700                       : $::myconfig{taxincluded_checked});
 
1702   my $address = $order->customer->default_billing_address;;
 
1703   $order->billing_address_id($address ? $address->id : undef);
 
1706 # setup custom shipto from form
 
1708 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1709 # with 'shiptocvar_'.
 
1710 # Mark it to be deleted if a shipto from master data is selected
 
1711 # (i.e. order has a shipto).
 
1712 # Else, update or create a new custom shipto. If the fields are empty, it
 
1713 # will not be saved on save.
 
1714 sub setup_custom_shipto_from_form {
 
1715   my ($self, $order, $form) = @_;
 
1717   if ($order->shipto) {
 
1718     $self->is_custom_shipto_to_delete(1);
 
1720     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1722     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1723     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1725     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1726     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1730 # recalculate prices and taxes
 
1732 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1736   my %pat = $self->order->calculate_prices_and_taxes();
 
1738   $self->{taxes} = [];
 
1739   foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
 
1740     my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
1742     push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
 
1743                                 netamount => $netamount,
 
1744                                 tax       => SL::DB::Tax->new(id => $tax_id)->load });
 
1746   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1749 # get data for saving, printing, ..., that is not changed in the form
 
1751 # Only cvars for now.
 
1752 sub get_unalterable_data {
 
1755   foreach my $item (@{ $self->order->items }) {
 
1756     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1757     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1758     foreach my $var (@{ $item->cvars_by_config }) {
 
1759       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1761     $item->parse_custom_variable_values;
 
1767 # And remove related files in the spool directory
 
1772   my $db     = $self->order->db;
 
1774   $db->with_transaction(
 
1776       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1777       $self->order->delete;
 
1778       my $spool = $::lx_office_conf{paths}->{spool};
 
1779       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1781       $self->save_history('DELETED');
 
1784   }) || push(@{$errors}, $db->error);
 
1791 # And delete items that are deleted in the form.
 
1796   my $db     = $self->order->db;
 
1798   $db->with_transaction(sub {
 
1799     # delete custom shipto if it is to be deleted or if it is empty
 
1800     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1801       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1802       $self->order->custom_shipto(undef);
 
1805     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1806     $self->order->save(cascade => 1);
 
1809     if ($::form->{converted_from_oe_id}) {
 
1810       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1812       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1813         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1814         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1815         $src->link_to_record($self->order);
 
1817       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1819         foreach (@{ $self->order->items_sorted }) {
 
1820           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1822           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1823                                   from_id    => $from_id,
 
1824                                   to_table   => 'orderitems',
 
1831       $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
 
1834     $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
 
1836     $self->save_history('SAVED');
 
1839   }) || push(@{$errors}, $db->error);
 
1844 sub workflow_sales_or_request_for_quotation {
 
1848   my $errors = $self->save();
 
1850   if (scalar @{ $errors }) {
 
1851     $self->js->flash('error', $_) for @{ $errors };
 
1852     return $self->js->render();
 
1855   my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
 
1857   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1858   delete $::form->{id};
 
1860   # no linked records from order to quotations
 
1861   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
1863   # set item ids to new fake id, to identify them as new items
 
1864   foreach my $item (@{$self->order->items_sorted}) {
 
1865     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1869   $::form->{type} = $destination_type;
 
1870   $self->type($self->init_type);
 
1871   $self->cv  ($self->init_cv);
 
1875   $self->get_unalterable_data();
 
1876   $self->pre_render();
 
1878   # trigger rendering values for second row as hidden, because they
 
1879   # are loaded only on demand. So we need to keep the values from the
 
1881   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1885     title => $self->get_title_for('edit'),
 
1886     %{$self->{template_args}}
 
1890 sub workflow_sales_or_purchase_order {
 
1894   my $errors = $self->save();
 
1896   if (scalar @{ $errors }) {
 
1897     $self->js->flash('error', $_) foreach @{ $errors };
 
1898     return $self->js->render();
 
1901   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1902                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1903                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1904                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1907   # check for direct delivery
 
1908   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1910   if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
 
1911       && $::form->{use_shipto} && $self->order->shipto) {
 
1912     $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
 
1915   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1916   $self->{converted_from_oe_id} = delete $::form->{id};
 
1918   # set item ids to new fake id, to identify them as new items
 
1919   foreach my $item (@{$self->order->items_sorted}) {
 
1920     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1923   if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
 
1924     if ($::form->{use_shipto}) {
 
1925       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
1927       # remove any custom shipto if not wanted
 
1928       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1933   $::form->{type} = $destination_type;
 
1934   $self->type($self->init_type);
 
1935   $self->cv  ($self->init_cv);
 
1939   $self->get_unalterable_data();
 
1940   $self->pre_render();
 
1942   # trigger rendering values for second row as hidden, because they
 
1943   # are loaded only on demand. So we need to keep the values from the
 
1945   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1949     title => $self->get_title_for('edit'),
 
1950     %{$self->{template_args}}
 
1958   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1959   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
1960   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1961   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
 
1962   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1965   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1968   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1970   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1971   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1972   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1973   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1974   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1976   my $print_form = Form->new('');
 
1977   $print_form->{type}        = $self->type;
 
1978   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
1979   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
1980     form => $print_form,
 
1981     options => {dialog_name_prefix => 'print_options.',
 
1985                 no_opendocument    => 0,
 
1989   foreach my $item (@{$self->order->orderitems}) {
 
1990     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1991     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1992     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1995   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1996     # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
 
1997     # Do not use write_to_objects to prevent order->delivered to be set, because this should be
 
1998     # the value from db, which can be set manually or is set when linked delivery orders are saved.
 
1999     SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
 
2002   if ($self->order->number && $::instance_conf->get_webdav) {
 
2003     my $webdav = SL::Webdav->new(
 
2004       type     => $self->type,
 
2005       number   => $self->order->number,
 
2007     my @all_objects = $webdav->get_all_objects;
 
2008     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
2010                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
2014   if (   (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
 
2015       && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
 
2016     $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
 
2019   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
2021   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
2022                                                          edit_periodic_invoices_config calculate_qty follow_up show_history);
 
2023   $self->setup_edit_action_bar;
 
2026 sub setup_edit_action_bar {
 
2027   my ($self, %params) = @_;
 
2029   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
2030                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
2031                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
2033   my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
 
2034   my @req_cusordnumber   = qw(kivi.Order.check_cusordnumber_presence)           x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
 
2036   my $has_invoice_for_advance_payment;
 
2037   if ($self->order->id && $self->type eq sales_order_type()) {
 
2038     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2039     $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
 
2042   my $has_final_invoice;
 
2043   if ($self->order->id && $self->type eq sales_order_type()) {
 
2044     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2045     $has_final_invoice               = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
 
2048   my $right_for         = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
2049   my $right             = $right_for->{ $self->type };
 
2050   $right              ||= 'DOES_NOT_EXIST';
 
2051   my $may_edit_create   = $::auth->assert($right, 'may fail');
 
2053   for my $bar ($::request->layout->get('actionbar')) {
 
2058           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
2059                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
2061           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
 
2062                          @req_trans_cost_art, @req_cusordnumber,
 
2064           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2067           t8('Save and Close'),
 
2068           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
2069                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
2072           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
 
2073                          @req_trans_cost_art, @req_cusordnumber,
 
2075           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2079           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
2080           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2081                          @req_trans_cost_art, @req_cusordnumber,
 
2083           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.')
 
2084                      : !$self->order->id ? t8('This object has not been saved yet.')
 
2087       ], # end of combobox "Save"
 
2094           t8('Save and Quotation'),
 
2095           submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
 
2096           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2097           only_if  => (any { $self->type eq $_ } (sales_order_type())),
 
2098           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2102           submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
 
2103           only_if  => (any { $self->type eq $_ } (purchase_order_type())),
 
2104           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2107           t8('Save and Sales Order'),
 
2108           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
2109           checks   => [ @req_trans_cost_art ],
 
2110           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
2111           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2114           t8('Save and Purchase Order'),
 
2115           call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
 
2116           checks    => [ @req_trans_cost_art, @req_cusordnumber ],
 
2117           only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
2118           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2121           t8('Save and Delivery Order'),
 
2122           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2123                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2125           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2126                          @req_trans_cost_art, @req_cusordnumber,
 
2128           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())),
 
2129           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2132           t8('Save and Supplier Delivery Order'),
 
2133           call      => [ 'kivi.Order.save', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2134                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2136           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2137                          @req_trans_cost_art, @req_cusordnumber,
 
2139           only_if   => (any { $self->type eq $_ } (purchase_order_type())),
 
2140           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2143           t8('Save and Invoice'),
 
2144           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2145           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2146                          @req_trans_cost_art, @req_cusordnumber,
 
2148           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2151           ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
 
2152           call      => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::instance_conf->get_order_warn_duplicate_parts ],
 
2153           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2154                          @req_trans_cost_art, @req_cusordnumber,
 
2156           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2157                      : $has_final_invoice ? t8('This order has already a final invoice.')
 
2159           only_if   => (any { $self->type eq $_ } (sales_order_type())),
 
2162           t8('Save and Final Invoice'),
 
2163           call      => [ 'kivi.Order.save', 'save_and_final_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2164           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2165                          @req_trans_cost_art, @req_cusordnumber,
 
2167           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2168                      : $has_final_invoice ? t8('This order has already a final invoice.')
 
2170           only_if   => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
 
2173           t8('Save and AP Transaction'),
 
2174           call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
2175           only_if   => (any { $self->type eq $_ } (purchase_order_type())),
 
2176           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2179       ], # end of combobox "Workflow"
 
2186           t8('Save and preview PDF'),
 
2187           call     => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
2188                                                           $::instance_conf->get_order_warn_no_deliverydate,
 
2190           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2191           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2194           t8('Save and print'),
 
2195           call     => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
2196                                                          $::instance_conf->get_order_warn_no_deliverydate,
 
2198           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2199           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2202           t8('Save and E-mail'),
 
2203           id       => 'save_and_email_action',
 
2204           call     => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
2205                                                                          $::instance_conf->get_order_warn_no_deliverydate,
 
2207           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2208                     : !$self->order->id  ? t8('This object has not been saved yet.')
 
2212           t8('Download attachments of all parts'),
 
2213           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
2214           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2215           only_if  => $::instance_conf->get_doc_storage,
 
2217       ], # end of combobox "Export"
 
2221         call     => [ 'kivi.Order.delete_order' ],
 
2222         confirm  => $::locale->text('Do you really want to delete this object?'),
 
2223         disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2224                   : !$self->order->id  ? t8('This object has not been saved yet.')
 
2226         only_if  => $deletion_allowed,
 
2235           call     => [ 'set_history_window', $self->order->id, 'id' ],
 
2236           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
 
2240           call     => [ 'kivi.Order.follow_up_window' ],
 
2241           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2242           only_if  => $::auth->assert('productivity', 1),
 
2244       ], # end of combobox "more"
 
2250   my ($self, $doc_ref, $params) = @_;
 
2252   my $order  = $self->order;
 
2255   my $print_form = Form->new('');
 
2256   $print_form->{type}        = $order->type;
 
2257   $print_form->{formname}    = $params->{formname} || $order->type;
 
2258   $print_form->{format}      = $params->{format}   || 'pdf';
 
2259   $print_form->{media}       = $params->{media}    || 'file';
 
2260   $print_form->{groupitems}  = $params->{groupitems};
 
2261   $print_form->{printer_id}  = $params->{printer_id};
 
2262   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
2264   $order->language($params->{language});
 
2265   $order->flatten_to_form($print_form, format_amounts => 1);
 
2269   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
2270     $template_ext  = 'odt';
 
2271     $template_type = 'OpenDocument';
 
2272   } elsif ($print_form->{format} =~ m{html}i) {
 
2273     $template_ext  = 'html';
 
2274     $template_type = 'HTML';
 
2277   # search for the template
 
2278   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
2279     name        => $print_form->{formname},
 
2280     extension   => $template_ext,
 
2281     email       => $print_form->{media} eq 'email',
 
2282     language    => $params->{language},
 
2283     printer_id  => $print_form->{printer_id},
 
2286   if (!defined $template_file) {
 
2287     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);
 
2290   return @errors if scalar @errors;
 
2292   $print_form->throw_on_error(sub {
 
2294       $print_form->prepare_for_printing;
 
2296       $$doc_ref = SL::Helper::CreatePDF->create_pdf(
 
2297         format        => $print_form->{format},
 
2298         template_type => $template_type,
 
2299         template      => $template_file,
 
2300         variables     => $print_form,
 
2301         variable_content_types => {
 
2302           longdescription => 'html',
 
2303           partnotes       => 'html',
 
2305           $::form->get_variable_content_types_for_cvars,
 
2309     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2315 sub get_files_for_email_dialog {
 
2318   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2320   return %files if !$::instance_conf->get_doc_storage;
 
2322   if ($self->order->id) {
 
2323     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2324     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2325     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2326     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2330     uniq_by { $_->{id} }
 
2332       +{ id         => $_->part->id,
 
2333          partnumber => $_->part->partnumber }
 
2334     } @{$self->order->items_sorted};
 
2336   foreach my $part (@parts) {
 
2337     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2338     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2341   foreach my $key (keys %files) {
 
2342     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2348 sub make_periodic_invoices_config_from_yaml {
 
2349   my ($yaml_config) = @_;
 
2351   return if !$yaml_config;
 
2352   my $attr = SL::YAML::Load($yaml_config);
 
2353   return if 'HASH' ne ref $attr;
 
2354   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
2358 sub get_periodic_invoices_status {
 
2359   my ($self, $config) = @_;
 
2361   return                      if $self->type ne sales_order_type();
 
2362   return t8('not configured') if !$config;
 
2364   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
2365              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
2366              :                                                     die "Cannot get status of periodic invoices config";
 
2368   return $active ? t8('active') : t8('inactive');
 
2372   my ($self, $action) = @_;
 
2374   return '' if none { lc($action)} qw(add edit);
 
2377   # $::locale->text("Add Sales Order");
 
2378   # $::locale->text("Add Purchase Order");
 
2379   # $::locale->text("Add Quotation");
 
2380   # $::locale->text("Add Request for Quotation");
 
2381   # $::locale->text("Edit Sales Order");
 
2382   # $::locale->text("Edit Purchase Order");
 
2383   # $::locale->text("Edit Quotation");
 
2384   # $::locale->text("Edit Request for Quotation");
 
2386   $action = ucfirst(lc($action));
 
2387   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
2388        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
2389        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
2390        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
2394 sub get_item_cvpartnumber {
 
2395   my ($self, $item) = @_;
 
2397   return if !$self->search_cvpartnumber;
 
2398   return if !$self->order->customervendor;
 
2400   if ($self->cv eq 'vendor') {
 
2401     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2402     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2403   } elsif ($self->cv eq 'customer') {
 
2404     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2405     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2409 sub get_part_texts {
 
2410   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2412   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2413   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2415     description     => $defaults{description}     // $part->description,
 
2416     longdescription => $defaults{longdescription} // $part->notes,
 
2419   return $texts unless $language_id;
 
2421   my $translation = SL::DB::Manager::Translation->get_first(
 
2423       parts_id    => $part->id,
 
2424       language_id => $language_id,
 
2427   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2428   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2433 sub sales_order_type {
 
2437 sub purchase_order_type {
 
2441 sub sales_quotation_type {
 
2445 sub request_quotation_type {
 
2446   'request_quotation';
 
2450   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
2451        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
2452        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
2453        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
2457 sub save_and_redirect_to {
 
2458   my ($self, %params) = @_;
 
2460   my $errors = $self->save();
 
2462   if (scalar @{ $errors }) {
 
2463     $self->js->flash('error', $_) foreach @{ $errors };
 
2464     return $self->js->render();
 
2467   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
2468            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
2469            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
2470            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
2472   flash_later('info', $text);
 
2474   $self->redirect_to(%params, id => $self->order->id);
 
2478   my ($self, $addition) = @_;
 
2480   my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
 
2481   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2483   SL::DB::History->new(
 
2484     trans_id    => $self->order->id,
 
2485     employee_id => SL::DB::Manager::Employee->current->id,
 
2486     what_done   => $self->order->type,
 
2487     snumbers    => $snumbers,
 
2488     addition    => $addition,
 
2492 sub store_doc_to_webdav_and_filemanagement {
 
2493   my ($self, $content, $filename, $variant) = @_;
 
2495   my $order = $self->order;
 
2498   # copy file to webdav folder
 
2499   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2500     my $webdav = SL::Webdav->new(
 
2501       type     => $order->type,
 
2502       number   => $order->number,
 
2504     my $webdav_file = SL::Webdav::File->new(
 
2506       filename => $filename,
 
2509       $webdav_file->store(data => \$content);
 
2512       push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
 
2515   if ($order->id && $::instance_conf->get_doc_storage) {
 
2517       SL::File->save(object_id     => $order->id,
 
2518                      object_type   => $order->type,
 
2519                      mime_type     => SL::MIME->mime_type_from_ext($filename),
 
2520                      source        => 'created',
 
2521                      file_type     => 'document',
 
2522                      file_name     => $filename,
 
2523                      file_contents => $content,
 
2524                      print_variant => $variant);
 
2527       push @errors, t8('Storing the document in the storage backend failed: #1', $@);
 
2534 sub link_requirement_specs_linking_to_created_from_objects {
 
2535   my ($self, @converted_from_oe_ids) = @_;
 
2537   return unless @converted_from_oe_ids;
 
2539   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
 
2540   foreach my $rs_order (@{ $rs_orders }) {
 
2541     SL::DB::RequirementSpecOrder->new(
 
2542       order_id            => $self->order->id,
 
2543       requirement_spec_id => $rs_order->requirement_spec_id,
 
2544       version_id          => $rs_order->version_id,
 
2549 sub set_project_in_linked_requirement_specs {
 
2552   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
 
2553   foreach my $rs_order (@{ $rs_orders }) {
 
2554     next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
 
2556     $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
 
2568 SL::Controller::Order - controller for orders
 
2572 This is a new form to enter orders, completely rewritten with the use
 
2573 of controller and java script techniques.
 
2575 The aim is to provide the user a better experience and a faster workflow. Also
 
2576 the code should be more readable, more reliable and better to maintain.
 
2584 One input row, so that input happens every time at the same place.
 
2588 Use of pickers where possible.
 
2592 Possibility to enter more than one item at once.
 
2596 Item list in a scrollable area, so that the workflow buttons stay at
 
2601 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2602 possible (by partnumber, description, qty, sellprice and discount for now).
 
2606 No C<update> is necessary. All entries and calculations are managed
 
2607 with ajax-calls and the page only reloads on C<save>.
 
2611 User can see changes immediately, because of the use of java script
 
2622 =item * C<SL/Controller/Order.pm>
 
2626 =item * C<template/webpages/order/form.html>
 
2630 =item * C<template/webpages/order/tabs/basic_data.html>
 
2632 Main tab for basic_data.
 
2634 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2635 reused from generic code.
 
2639 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
2641 For displaying information on business type
 
2643 =item * C<template/webpages/order/tabs/_item_input.html>
 
2645 The input line for items
 
2647 =item * C<template/webpages/order/tabs/_row.html>
 
2649 One row for already entered items
 
2651 =item * C<template/webpages/order/tabs/_tax_row.html>
 
2653 Displaying tax information
 
2655 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
2657 Dialog for selecting price and discount sources
 
2661 =item * C<js/kivi.Order.js>
 
2663 java script functions
 
2673 =item * price sources: little symbols showing better price / better discount
 
2675 =item * select units in input row?
 
2677 =item * check for direct delivery (workflow sales order -> purchase order)
 
2679 =item * access rights
 
2681 =item * display weights
 
2685 =item * optional client/user behaviour
 
2687 (transactions has to be set - department has to be set -
 
2688  force project if enabled in client config)
 
2692 =head1 KNOWN BUGS AND CAVEATS
 
2698 Customer discount is not displayed as a valid discount in price source popup
 
2699 (this might be a bug in price sources)
 
2701 (I cannot reproduce this (Bernd))
 
2705 No indication that <shift>-up/down expands/collapses second row.
 
2709 Inline creation of parts is not currently supported
 
2713 Table header is not sticky in the scrolling area.
 
2717 Sorting does not include C<position>, neither does reordering.
 
2719 This behavior was implemented intentionally. But we can discuss, which behavior
 
2720 should be implemented.
 
2724 =head1 To discuss / Nice to have
 
2730 How to expand/collapse second row. Now it can be done clicking the icon or
 
2735 Possibility to select PriceSources in input row?
 
2739 This controller uses a (changed) copy of the template for the PriceSource
 
2740 dialog. Maybe there could be used one code source.
 
2744 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2745 form. This is not only a problem here, but also in all parts using the PTC.
 
2746 There exists a ticket and a patch. This patch should be testet.
 
2750 An indicator, if the actual inputs are saved (like in an
 
2751 editor or on text processing application).
 
2755 A warning when leaving the page without saveing unchanged inputs.
 
2762 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>