1 package SL::Controller::Order;
 
   4 use parent qw(SL::Controller::Base);
 
   6 use SL::Helper::Flash qw(flash_later);
 
   8 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
 
   9 use SL::Locale::String qw(t8);
 
  10 use SL::SessionFile::Random;
 
  15 use SL::Util qw(trim);
 
  17 use SL::DB::AdditionalBillingAddress;
 
  24 use SL::DB::PartClassification;
 
  25 use SL::DB::PartsGroup;
 
  28 use SL::DB::RecordLink;
 
  29 use SL::DB::RequirementSpec;
 
  31 use SL::DB::Translation;
 
  33 use SL::Helper::CreatePDF qw(:all);
 
  34 use SL::Helper::PrintOptions;
 
  35 use SL::Helper::ShippedQty;
 
  36 use SL::Helper::UserPreferences::PositionsScrollbar;
 
  37 use SL::Helper::UserPreferences::UpdatePositions;
 
  39 use SL::Controller::Helper::GetModels;
 
  41 use List::Util qw(first sum0);
 
  42 use List::UtilsBy qw(sort_by uniq_by);
 
  43 use List::MoreUtils qw(any none pairwise first_index);
 
  44 use English qw(-no_match_vars);
 
  49 use Rose::Object::MakeMethods::Generic
 
  51  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 
  52  'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
 
  57 __PACKAGE__->run_before('check_auth');
 
  59 __PACKAGE__->run_before('recalc',
 
  60                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
 
  63 __PACKAGE__->run_before('get_unalterable_data',
 
  64                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
 
  75   $self->order->transdate(DateTime->now_local());
 
  76   my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
  77                    $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
  79   if (   ($self->type eq sales_order_type()     &&  $::instance_conf->get_deliverydate_on)
 
  80       || ($self->type eq sales_quotation_type() &&  $::instance_conf->get_reqdate_on)
 
  81       && (!$self->order->reqdate)) {
 
  82     $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
 
  89     title => $self->get_title_for('add'),
 
  90     %{$self->{template_args}}
 
  94 # edit an existing order
 
 102     # this is to edit an order from an unsaved order object
 
 104     # set item ids to new fake id, to identify them as new items
 
 105     foreach my $item (@{$self->order->items_sorted}) {
 
 106       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 108     # trigger rendering values for second row as hidden, because they
 
 109     # are loaded only on demand. So we need to keep the values from
 
 111     $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
 118     title => $self->get_title_for('edit'),
 
 119     %{$self->{template_args}}
 
 123 # edit a collective order (consisting of one or more existing orders)
 
 124 sub action_edit_collective {
 
 128   my @multi_ids = map {
 
 129     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 130   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 132   # fall back to add if no ids are given
 
 133   if (scalar @multi_ids == 0) {
 
 138   # fall back to save as new if only one id is given
 
 139   if (scalar @multi_ids == 1) {
 
 140     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 141     $self->action_save_as_new();
 
 145   # make new order from given orders
 
 146   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 147   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 148   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 150   $self->action_edit();
 
 157   my $errors = $self->delete();
 
 159   if (scalar @{ $errors }) {
 
 160     $self->js->flash('error', $_) foreach @{ $errors };
 
 161     return $self->js->render();
 
 164   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 165            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 166            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 167            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 169   flash_later('info', $text);
 
 171   my @redirect_params = (
 
 176   $self->redirect_to(@redirect_params);
 
 183   my $errors = $self->save();
 
 185   if (scalar @{ $errors }) {
 
 186     $self->js->flash('error', $_) foreach @{ $errors };
 
 187     return $self->js->render();
 
 190   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 191            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 192            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 193            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 195   flash_later('info', $text);
 
 197   my @redirect_params = (
 
 200     id     => $self->order->id,
 
 203   $self->redirect_to(@redirect_params);
 
 206 # save the order as new document an open it for edit
 
 207 sub action_save_as_new {
 
 210   my $order = $self->order;
 
 213     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 214     return $self->js->render();
 
 217   # load order from db to check if values changed
 
 218   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 221   # Lets assign a new number if the user hasn't changed the previous one.
 
 222   # If it has been changed manually then use it as-is.
 
 223   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 225                         : trim($order->number);
 
 227   # Clear transdate unless changed
 
 228   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 229                         ? DateTime->today_local
 
 232   # Set new reqdate unless changed if it is enabled in client config
 
 233   if ($order->reqdate == $saved_order->reqdate) {
 
 234     my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
 235                      $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
 237     if (   ($self->type eq sales_order_type()     &&  !$::instance_conf->get_deliverydate_on)
 
 238         || ($self->type eq sales_quotation_type() &&  !$::instance_conf->get_reqdate_on)) {
 
 239       $new_attrs{reqdate} = '';
 
 241       $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 244     $new_attrs{reqdate} = $order->reqdate;
 
 248   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 250   # Create new record from current one
 
 251   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 253   # no linked records on save as new
 
 254   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 257   $self->action_save();
 
 262 # This is called if "print" is pressed in the print dialog.
 
 263 # If PDF creation was requested and succeeded, the pdf is offered for download
 
 264 # via send_file (which uses ajax in this case).
 
 268   my $errors = $self->save();
 
 270   if (scalar @{ $errors }) {
 
 271     $self->js->flash('error', $_) foreach @{ $errors };
 
 272     return $self->js->render();
 
 275   $self->js_reset_order_and_item_ids_after_save;
 
 277   my $format      = $::form->{print_options}->{format};
 
 278   my $media       = $::form->{print_options}->{media};
 
 279   my $formname    = $::form->{print_options}->{formname};
 
 280   my $copies      = $::form->{print_options}->{copies};
 
 281   my $groupitems  = $::form->{print_options}->{groupitems};
 
 282   my $printer_id  = $::form->{print_options}->{printer_id};
 
 284   # only PDF, OpenDocument & HTML for now
 
 285   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
 
 286     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 289   # only screen or printer by now
 
 290   if (none { $media eq $_ } qw(screen printer)) {
 
 291     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 294   # create a form for generate_attachment_filename
 
 295   my $form   = Form->new;
 
 296   $form->{$self->nr_key()}  = $self->order->number;
 
 297   $form->{type}             = $self->type;
 
 298   $form->{format}           = $format;
 
 299   $form->{formname}         = $formname;
 
 300   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 301   my $doc_filename          = $form->generate_attachment_filename();
 
 304   my @errors = $self->generate_doc(\$doc, { format     => $format,
 
 305                                             formname   => $formname,
 
 306                                             language   => $self->order->language,
 
 307                                             printer_id => $printer_id,
 
 308                                             groupitems => $groupitems });
 
 309   if (scalar @errors) {
 
 310     return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
 
 313   if ($media eq 'screen') {
 
 315     $self->js->flash('info', t8('The document has been created.'));
 
 318       type         => SL::MIME->mime_type_from_ext($doc_filename),
 
 319       name         => $doc_filename,
 
 323   } elsif ($media eq 'printer') {
 
 325     my $printer_id = $::form->{print_options}->{printer_id};
 
 326     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 331     $self->js->flash('info', t8('The document has been printed.'));
 
 334   my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
 
 335   if (scalar @warnings) {
 
 336     $self->js->flash('warning', $_) for @warnings;
 
 339   $self->save_history('PRINTED');
 
 342     ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
 
 345 sub action_preview_pdf {
 
 348   my $errors = $self->save();
 
 349   if (scalar @{ $errors }) {
 
 350     $self->js->flash('error', $_) foreach @{ $errors };
 
 351     return $self->js->render();
 
 354   $self->js_reset_order_and_item_ids_after_save;
 
 357   my $media       = 'screen';
 
 358   my $formname    = $self->type;
 
 361   # create a form for generate_attachment_filename
 
 362   my $form   = Form->new;
 
 363   $form->{$self->nr_key()}  = $self->order->number;
 
 364   $form->{type}             = $self->type;
 
 365   $form->{format}           = $format;
 
 366   $form->{formname}         = $formname;
 
 367   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 368   my $pdf_filename          = $form->generate_attachment_filename();
 
 371   my @errors = $self->generate_doc(\$pdf, { format     => $format,
 
 372                                             formname   => $formname,
 
 373                                             language   => $self->order->language,
 
 375   if (scalar @errors) {
 
 376     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 378   $self->save_history('PREVIEWED');
 
 379   $self->js->flash('info', t8('The PDF has been previewed'));
 
 383     type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 384     name         => $pdf_filename,
 
 389 # open the email dialog
 
 390 sub action_save_and_show_email_dialog {
 
 393   my $errors = $self->save();
 
 395   if (scalar @{ $errors }) {
 
 396     $self->js->flash('error', $_) foreach @{ $errors };
 
 397     return $self->js->render();
 
 400   my $cv_method = $self->cv;
 
 402   if (!$self->order->$cv_method) {
 
 403     return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
 
 408   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 409   $email_form->{to} ||= $self->order->$cv_method->email;
 
 410   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 411   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 412   # Todo: get addresses from shipto, if any
 
 414   my $form = Form->new;
 
 415   $form->{$self->nr_key()}  = $self->order->number;
 
 416   $form->{cusordnumber}     = $self->order->cusordnumber;
 
 417   $form->{formname}         = $self->type;
 
 418   $form->{type}             = $self->type;
 
 419   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 420   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
 
 421   $form->{format}           = 'pdf';
 
 422   $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
 424   $email_form->{subject}             = $form->generate_email_subject();
 
 425   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 426   $email_form->{message}             = $form->generate_email_body();
 
 427   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 429   my %files = $self->get_files_for_email_dialog();
 
 431   my @employees_with_email = grep {
 
 432     my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
 
 433     $user && !!trim($user->get_config_value('email'));
 
 434   } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
 
 436   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 437                                   email_form    => $email_form,
 
 438                                   show_bcc      => $::auth->assert('email_bcc', 'may fail'),
 
 440                                   is_customer   => $self->cv eq 'customer',
 
 441                                   ALL_EMPLOYEES => \@employees_with_email,
 
 445       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 452 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 453 sub action_send_email {
 
 456   my $errors = $self->save();
 
 458   if (scalar @{ $errors }) {
 
 459     $self->js->run('kivi.Order.close_email_dialog');
 
 460     $self->js->flash('error', $_) foreach @{ $errors };
 
 461     return $self->js->render();
 
 464   $self->js_reset_order_and_item_ids_after_save;
 
 466   my $email_form  = delete $::form->{email_form};
 
 467   my %field_names = (to => 'email');
 
 469   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 471   # for Form::cleanup which may be called in Form::send_email
 
 472   $::form->{cwd}    = getcwd();
 
 473   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 475   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 476   $::form->{media}  = 'email';
 
 478   $::form->{attachment_policy} //= '';
 
 480   # Is an old file version available?
 
 482   if ($::form->{attachment_policy} eq 'old_file') {
 
 483     $attfile = SL::File->get_all(object_id     => $self->order->id,
 
 484                                  object_type   => $self->type,
 
 485                                  file_type     => 'document',
 
 486                                  print_variant => $::form->{formname});
 
 489   if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
 
 491     my @errors = $self->generate_doc(\$doc, {media      => $::form->{media},
 
 492                                              format     => $::form->{print_options}->{format},
 
 493                                              formname   => $::form->{print_options}->{formname},
 
 494                                              language   => $self->order->language,
 
 495                                              printer_id => $::form->{print_options}->{printer_id},
 
 496                                              groupitems => $::form->{print_options}->{groupitems}});
 
 497     if (scalar @errors) {
 
 498       return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
 
 501     my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename}, $::form->{formname});
 
 502     if (scalar @warnings) {
 
 503       flash_later('warning', $_) for @warnings;
 
 506     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 507     $sfile->fh->print($doc);
 
 510     $::form->{tmpfile} = $sfile->file_name;
 
 511     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 514   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 515   $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
 
 518   my $intnotes = $self->order->intnotes;
 
 519   $intnotes   .= "\n\n" if $self->order->intnotes;
 
 520   $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 521   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 522   $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 523   $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 524   $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 525   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 526   $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
 
 528   $self->order->update_attributes(intnotes => $intnotes);
 
 530   $self->save_history('MAILED');
 
 532   flash_later('info', t8('The email has been sent.'));
 
 534   my @redirect_params = (
 
 537     id     => $self->order->id,
 
 540   $self->redirect_to(@redirect_params);
 
 543 # open the periodic invoices config dialog
 
 545 # If there are values in the form (i.e. dialog was opened before),
 
 546 # then use this values. Create new ones, else.
 
 547 sub action_show_periodic_invoices_config_dialog {
 
 550   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 551   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 552   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 553                                                    order_value_periodicity => 'p', # = same as periodicity
 
 554                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 555                                                    extend_automatically_by => 12,
 
 557                                                    email_subject           => GenericTranslations->get(
 
 558                                                                                 language_id      => $::form->{language_id},
 
 559                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 560                                                    email_body              => GenericTranslations->get(
 
 561                                                                                 language_id      => $::form->{language_id},
 
 562                                                                                 translation_type => "salutation_general")
 
 563                                                                             . GenericTranslations->get(
 
 564                                                                                 language_id      => $::form->{language_id},
 
 565                                                                                 translation_type => "salutation_punctuation_mark") . "\n\n"
 
 566                                                                             . GenericTranslations->get(
 
 567                                                                                 language_id      => $::form->{language_id},
 
 568                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 570   # for older configs, replace email preset text if not yet set.
 
 571   $config->email_subject(GenericTranslations->get(
 
 572                                               language_id      => $::form->{language_id},
 
 573                                               translation_type =>"preset_text_periodic_invoices_email_subject")
 
 574                         ) unless $config->email_subject;
 
 576   $config->email_body(GenericTranslations->get(
 
 577                                               language_id      => $::form->{language_id},
 
 578                                               translation_type => "salutation_general")
 
 579                     . GenericTranslations->get(
 
 580                                               language_id      => $::form->{language_id},
 
 581                                               translation_type => "salutation_punctuation_mark") . "\n\n"
 
 582                     . GenericTranslations->get(
 
 583                                               language_id      => $::form->{language_id},
 
 584                                               translation_type =>"preset_text_periodic_invoices_email_body")
 
 585                      ) unless $config->email_body;
 
 587   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 588   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 590   $::form->get_lists(printers => "ALL_PRINTERS",
 
 591                      charts   => { key       => 'ALL_CHARTS',
 
 592                                    transdate => 'current_date' });
 
 594   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 596   if ($::form->{customer_id}) {
 
 597     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 598     my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
 
 599     $::form->{postal_invoice}                  = $customer_object->postal_invoice;
 
 600     $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
 
 601     $config->send_email(0) if $::form->{postal_invoice};
 
 604   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 606                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 607                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 612 # assign the values of the periodic invoices config dialog
 
 613 # as yaml in the hidden tag and set the status.
 
 614 sub action_assign_periodic_invoices_config {
 
 617   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 619   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 620                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 621                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 622                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 623                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 624                  start_date_as_date         => $::form->{start_date_as_date},
 
 625                  end_date_as_date           => $::form->{end_date_as_date},
 
 626                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 627                  print                      => $::form->{print}      ? 1                         : 0,
 
 628                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 629                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 630                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 631                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 632                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 633                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 634                  email_recipient_address    => $::form->{email_recipient_address},
 
 635                  email_sender               => $::form->{email_sender},
 
 636                  email_subject              => $::form->{email_subject},
 
 637                  email_body                 => $::form->{email_body},
 
 640   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 642   my $status = $self->get_periodic_invoices_status($config);
 
 645     ->remove('#order_periodic_invoices_config')
 
 646     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 647     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 648     ->html('#periodic_invoices_status', $status)
 
 649     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 653 sub action_get_has_active_periodic_invoices {
 
 656   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 657   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 659   my $has_active_periodic_invoices =
 
 660        $self->type eq sales_order_type()
 
 663     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 664     && $config->get_previous_billed_period_start_date;
 
 666   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 669 # save the order and redirect to the frontend subroutine for a new
 
 671 sub action_save_and_delivery_order {
 
 674   $self->save_and_redirect_to(
 
 675     controller => 'oe.pl',
 
 676     action     => 'oe_delivery_order_from_order',
 
 680 sub action_save_and_supplier_delivery_order {
 
 683   $self->save_and_redirect_to(
 
 684     controller => 'controller.pl',
 
 685     action     => 'DeliveryOrder/add_from_order',
 
 686     type       => 'supplier_delivery_order',
 
 690 # save the order and redirect to the frontend subroutine for a new
 
 692 sub action_save_and_invoice {
 
 695   $self->save_and_redirect_to(
 
 696     controller => 'oe.pl',
 
 697     action     => 'oe_invoice_from_order',
 
 701 sub action_save_and_invoice_for_advance_payment {
 
 704   $self->save_and_redirect_to(
 
 705     controller       => 'oe.pl',
 
 706     action           => 'oe_invoice_from_order',
 
 707     new_invoice_type => 'invoice_for_advance_payment',
 
 711 sub action_save_and_final_invoice {
 
 714   $self->save_and_redirect_to(
 
 715     controller       => 'oe.pl',
 
 716     action           => 'oe_invoice_from_order',
 
 717     new_invoice_type => 'final_invoice',
 
 721 # workflow from sales order to sales quotation
 
 722 sub action_sales_quotation {
 
 723   $_[0]->workflow_sales_or_request_for_quotation();
 
 726 # workflow from sales order to sales quotation
 
 727 sub action_request_for_quotation {
 
 728   $_[0]->workflow_sales_or_request_for_quotation();
 
 731 # workflow from sales quotation to sales order
 
 732 sub action_sales_order {
 
 733   $_[0]->workflow_sales_or_purchase_order();
 
 736 # workflow from rfq to purchase order
 
 737 sub action_purchase_order {
 
 738   $_[0]->workflow_sales_or_purchase_order();
 
 741 # workflow from purchase order to ap transaction
 
 742 sub action_save_and_ap_transaction {
 
 745   $self->save_and_redirect_to(
 
 746     controller => 'ap.pl',
 
 747     action     => 'add_from_purchase_order',
 
 751 # set form elements in respect to a changed customer or vendor
 
 753 # This action is called on an change of the customer/vendor picker.
 
 754 sub action_customer_vendor_changed {
 
 757   setup_order_from_cv($self->order);
 
 760   my $cv_method = $self->cv;
 
 762   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 763     $self->js->show('#cp_row');
 
 765     $self->js->hide('#cp_row');
 
 768   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 769     $self->js->show('#shipto_selection');
 
 771     $self->js->hide('#shipto_selection');
 
 774   if ($cv_method eq 'customer') {
 
 775     my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
 
 776     $self->js->$show_hide('#billing_address_row');
 
 779   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 782     ->replaceWith('#order_cp_id',              $self->build_contact_select)
 
 783     ->replaceWith('#order_shipto_id',          $self->build_shipto_select)
 
 784     ->replaceWith('#shipto_inputs  ',          $self->build_shipto_inputs)
 
 785     ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
 
 786     ->replaceWith('#business_info_row',        $self->build_business_info_row)
 
 787     ->val(        '#order_taxzone_id',         $self->order->taxzone_id)
 
 788     ->val(        '#order_taxincluded',        $self->order->taxincluded)
 
 789     ->val(        '#order_currency_id',        $self->order->currency_id)
 
 790     ->val(        '#order_payment_id',         $self->order->payment_id)
 
 791     ->val(        '#order_delivery_term_id',   $self->order->delivery_term_id)
 
 792     ->val(        '#order_intnotes',           $self->order->intnotes)
 
 793     ->val(        '#order_language_id',        $self->order->$cv_method->language_id)
 
 794     ->focus(      '#order_' . $self->cv . '_id')
 
 795     ->run('kivi.Order.update_exchangerate');
 
 797   $self->js_redisplay_amounts_and_taxes;
 
 798   $self->js_redisplay_cvpartnumbers;
 
 802 # open the dialog for customer/vendor details
 
 803 sub action_show_customer_vendor_details_dialog {
 
 806   my $is_customer = 'customer' eq $::form->{vc};
 
 809     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 811     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 814   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 815   $details{discount_as_percent} = $cv->discount_as_percent;
 
 816   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 817   $details{business}            = $cv->business->description      if $cv->business;
 
 818   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 819   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 820   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 821   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 824     foreach my $entry (@{ $cv->additional_billing_addresses }) {
 
 825       push @{ $details{ADDITIONAL_BILLING_ADDRESSES} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 828   foreach my $entry (@{ $cv->shipto }) {
 
 829     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 831   foreach my $entry (@{ $cv->contacts }) {
 
 832     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 835   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 836                 is_customer => $is_customer,
 
 841 # called if a unit in an existing item row is changed
 
 842 sub action_unit_changed {
 
 845   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 846   my $item = $self->order->items_sorted->[$idx];
 
 848   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 849   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 854     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 855   $self->js_redisplay_line_values;
 
 856   $self->js_redisplay_amounts_and_taxes;
 
 860 # add an item row for a new item entered in the input row
 
 861 sub action_add_item {
 
 864   delete $::form->{add_item}->{create_part_type};
 
 866   my $form_attr = $::form->{add_item};
 
 868   return unless $form_attr->{parts_id};
 
 870   my $item = new_item($self->order, $form_attr);
 
 872   $self->order->add_items($item);
 
 876   $self->get_item_cvpartnumber($item);
 
 878   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 879   my $row_as_html = $self->p->render('order/tabs/_row',
 
 885   if ($::form->{insert_before_item_id}) {
 
 887       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 890       ->append('#row_table_id', $row_as_html);
 
 893   if ( $item->part->is_assortment ) {
 
 894     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 895     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 896       my $attr = { parts_id => $assortment_item->parts_id,
 
 897                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 898                    unit     => $assortment_item->unit,
 
 899                    description => $assortment_item->part->description,
 
 901       my $item = new_item($self->order, $attr);
 
 903       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 904       $item->discount(1) unless $assortment_item->charge;
 
 906       $self->order->add_items( $item );
 
 908       $self->get_item_cvpartnumber($item);
 
 909       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 910       my $row_as_html = $self->p->render('order/tabs/_row',
 
 915       if ($::form->{insert_before_item_id}) {
 
 917           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 920           ->append('#row_table_id', $row_as_html);
 
 926     ->val('.add_item_input', '')
 
 927     ->run('kivi.Order.init_row_handlers')
 
 928     ->run('kivi.Order.renumber_positions')
 
 929     ->focus('#add_item_parts_id_name');
 
 931   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 933   $self->js_redisplay_amounts_and_taxes;
 
 937 # add item rows for multiple items at once
 
 938 sub action_add_multi_items {
 
 941   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
 
 942   return $self->js->render() unless scalar @form_attr;
 
 945   foreach my $attr (@form_attr) {
 
 946     my $item = new_item($self->order, $attr);
 
 948     if ( $item->part->is_assortment ) {
 
 949       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 950         my $attr = { parts_id => $assortment_item->parts_id,
 
 951                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 952                      unit     => $assortment_item->unit,
 
 953                      description => $assortment_item->part->description,
 
 955         my $item = new_item($self->order, $attr);
 
 957         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 958         $item->discount(1) unless $assortment_item->charge;
 
 963   $self->order->add_items(@items);
 
 967   foreach my $item (@items) {
 
 968     $self->get_item_cvpartnumber($item);
 
 969     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 970     my $row_as_html = $self->p->render('order/tabs/_row',
 
 976     if ($::form->{insert_before_item_id}) {
 
 978         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 981         ->append('#row_table_id', $row_as_html);
 
 986     ->run('kivi.Part.close_picker_dialogs')
 
 987     ->run('kivi.Order.init_row_handlers')
 
 988     ->run('kivi.Order.renumber_positions')
 
 989     ->focus('#add_item_parts_id_name');
 
 991   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 993   $self->js_redisplay_amounts_and_taxes;
 
 997 # recalculate all linetotals, amounts and taxes and redisplay them
 
 998 sub action_recalc_amounts_and_taxes {
 
1003   $self->js_redisplay_line_values;
 
1004   $self->js_redisplay_amounts_and_taxes;
 
1005   $self->js->render();
 
1008 sub action_update_exchangerate {
 
1012     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
1013     currency_name => $self->order->currency->name,
 
1014     exchangerate  => $self->order->daily_exchangerate_as_null_number,
 
1017   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
1020 # redisplay item rows if they are sorted by an attribute
 
1021 sub action_reorder_items {
 
1025     partnumber   => sub { $_[0]->part->partnumber },
 
1026     description  => sub { $_[0]->description },
 
1027     qty          => sub { $_[0]->qty },
 
1028     sellprice    => sub { $_[0]->sellprice },
 
1029     discount     => sub { $_[0]->discount },
 
1030     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
1033   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1035   my $method = $sort_keys{$::form->{order_by}};
 
1036   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
1037   if ($::form->{sort_dir}) {
 
1038     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1039       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
1041       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
1044     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1045       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
1047       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
1051     ->run('kivi.Order.redisplay_items', \@to_sort)
 
1055 # show the popup to choose a price/discount source
 
1056 sub action_price_popup {
 
1059   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
1060   my $item = $self->order->items_sorted->[$idx];
 
1062   $self->render_price_dialog($item);
 
1065 # save the order in a session variable and redirect to the part controller
 
1066 sub action_create_part {
 
1069   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
 
1071   my $callback     = $self->url_for(
 
1072     action       => 'return_from_create_part',
 
1073     type         => $self->type, # type is needed for check_auth on return
 
1074     previousform => $previousform,
 
1077   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.'));
 
1079   my @redirect_params = (
 
1080     controller => 'Part',
 
1082     part_type  => $::form->{add_item}->{create_part_type},
 
1083     callback   => $callback,
 
1087   $self->redirect_to(@redirect_params);
 
1090 sub action_return_from_create_part {
 
1093   $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
 
1095   $::auth->restore_form_from_session(delete $::form->{previousform});
 
1097   # set item ids to new fake id, to identify them as new items
 
1098   foreach my $item (@{$self->order->items_sorted}) {
 
1099     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1103   $self->get_unalterable_data();
 
1104   $self->pre_render();
 
1106   # trigger rendering values for second row/longdescription as hidden,
 
1107   # because they are loaded only on demand. So we need to keep the values
 
1109   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1110   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1114     title => $self->get_title_for('edit'),
 
1115     %{$self->{template_args}}
 
1120 # load the second row for one or more items
 
1122 # This action gets the html code for all items second rows by rendering a template for
 
1123 # the second row and sets the html code via client js.
 
1124 sub action_load_second_rows {
 
1127   $self->recalc() if $self->order->is_sales; # for margin calculation
 
1129   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1130     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1131     my $item = $self->order->items_sorted->[$idx];
 
1133     $self->js_load_second_row($item, $item_id, 0);
 
1136   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
1138   $self->js->render();
 
1141 # update description, notes and sellprice from master data
 
1142 sub action_update_row_from_master_data {
 
1145   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1146     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1147     my $item  = $self->order->items_sorted->[$idx];
 
1148     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1150     $item->description($texts->{description});
 
1151     $item->longdescription($texts->{longdescription});
 
1153     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1156     if ($item->part->is_assortment) {
 
1157     # add assortment items with price 0, as the components carry the price
 
1158       $price_src = $price_source->price_from_source("");
 
1159       $price_src->price(0);
 
1161       $price_src = $price_source->best_price
 
1162                  ? $price_source->best_price
 
1163                  : $price_source->price_from_source("");
 
1164       $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
 
1165       $price_src->price(0) if !$price_source->best_price;
 
1169     $item->sellprice($price_src->price);
 
1170     $item->active_price_source($price_src);
 
1173       ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
 
1174       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1175       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1176       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1178     if ($self->search_cvpartnumber) {
 
1179       $self->get_item_cvpartnumber($item);
 
1180       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1185   $self->js_redisplay_line_values;
 
1186   $self->js_redisplay_amounts_and_taxes;
 
1188   $self->js->render();
 
1191 sub js_load_second_row {
 
1192   my ($self, $item, $item_id, $do_parse) = @_;
 
1195     # Parse values from form (they are formated while rendering (template)).
 
1196     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1197     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1198     foreach my $var (@{ $item->cvars_by_config }) {
 
1199       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1201     $item->parse_custom_variable_values;
 
1204   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1207     ->html('#second_row_' . $item_id, $row_as_html)
 
1208     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1211 sub js_redisplay_line_values {
 
1214   my $is_sales = $self->order->is_sales;
 
1216   # sales orders with margins
 
1221        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1222        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1223        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1224       ]} @{ $self->order->items_sorted };
 
1228        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1229       ]} @{ $self->order->items_sorted };
 
1233     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1236 sub js_redisplay_amounts_and_taxes {
 
1239   if (scalar @{ $self->{taxes} }) {
 
1240     $self->js->show('#taxincluded_row_id');
 
1242     $self->js->hide('#taxincluded_row_id');
 
1245   if ($self->order->taxincluded) {
 
1246     $self->js->hide('#subtotal_row_id');
 
1248     $self->js->show('#subtotal_row_id');
 
1251   if ($self->order->is_sales) {
 
1252     my $is_neg = $self->order->marge_total < 0;
 
1254       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1255       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1256       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1257       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1258       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1259       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1260       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1261       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1265     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1266     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1267     ->remove('.tax_row')
 
1268     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1271 sub js_redisplay_cvpartnumbers {
 
1274   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1276   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1279     ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
 
1282 sub js_reset_order_and_item_ids_after_save {
 
1286     ->val('#id', $self->order->id)
 
1287     ->val('#converted_from_oe_id', '')
 
1288     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1291   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1292     next if !$self->order->items_sorted->[$idx]->id;
 
1293     next if $form_item_id !~ m{^new};
 
1295       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1296       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1297       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1301   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1308 sub init_valid_types {
 
1309   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1315   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1316     die "Not a valid type for order";
 
1319   $self->type($::form->{type});
 
1325   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1326          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1327          : die "Not a valid type for order";
 
1332 sub init_search_cvpartnumber {
 
1335   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1336   my $search_cvpartnumber;
 
1337   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1338   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1340   return $search_cvpartnumber;
 
1343 sub init_show_update_button {
 
1346   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1357 sub init_all_price_factors {
 
1358   SL::DB::Manager::PriceFactor->get_all;
 
1361 sub init_part_picker_classification_ids {
 
1363   my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
 
1365   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
 
1371   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1373   my $right   = $right_for->{ $self->type };
 
1374   $right    ||= 'DOES_NOT_EXIST';
 
1376   $::auth->assert($right);
 
1379 # build the selection box for contacts
 
1381 # Needed, if customer/vendor changed.
 
1382 sub build_contact_select {
 
1385   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1386     value_key  => 'cp_id',
 
1387     title_key  => 'full_name_dep',
 
1388     default    => $self->order->cp_id,
 
1390     style      => 'width: 300px',
 
1394 # build the selection box for the additional billing address
 
1396 # Needed, if customer/vendor changed.
 
1397 sub build_billing_address_select {
 
1400   return '' if $self->cv ne 'customer';
 
1402   select_tag('order.billing_address_id',
 
1403              [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
 
1405              title_key  => 'displayable_id',
 
1406              default    => $self->order->billing_address_id,
 
1408              style      => 'width: 300px',
 
1412 # build the selection box for shiptos
 
1414 # Needed, if customer/vendor changed.
 
1415 sub build_shipto_select {
 
1418   select_tag('order.shipto_id',
 
1419              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1420              value_key  => 'shipto_id',
 
1421              title_key  => 'displayable_id',
 
1422              default    => $self->order->shipto_id,
 
1424              style      => 'width: 300px',
 
1428 # build the inputs for the cusom shipto dialog
 
1430 # Needed, if customer/vendor changed.
 
1431 sub build_shipto_inputs {
 
1434   my $content = $self->p->render('common/_ship_to_dialog',
 
1435                                  vc_obj      => $self->order->customervendor,
 
1436                                  cs_obj      => $self->order->custom_shipto,
 
1437                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1438                                  id_selector => '#order_shipto_id');
 
1440   div_tag($content, id => 'shipto_inputs');
 
1443 # render the info line for business
 
1445 # Needed, if customer/vendor changed.
 
1446 sub build_business_info_row
 
1448   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1451 # build the rows for displaying taxes
 
1453 # Called if amounts where recalculated and redisplayed.
 
1454 sub build_tax_rows {
 
1458   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1459     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1461   return $rows_as_html;
 
1465 sub render_price_dialog {
 
1466   my ($self, $record_item) = @_;
 
1468   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1472       'kivi.io.price_chooser_dialog',
 
1473       t8('Available Prices'),
 
1474       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1479 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1480 #     $self->js->show('#dialog_flash_error');
 
1489   return if !$::form->{id};
 
1491   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1493   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1494   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1495   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1497   return $self->order;
 
1500 # load or create a new order object
 
1502 # And assign changes from the form to this object.
 
1503 # If the order is loaded from db, check if items are deleted in the form,
 
1504 # remove them form the object and collect them for removing from db on saving.
 
1505 # Then create/update items from form (via make_item) and add them.
 
1509   # add_items adds items to an order with no items for saving, but they cannot
 
1510   # be retrieved via items until the order is saved. Adding empty items to new
 
1511   # order here solves this problem.
 
1513   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1514   $order ||= SL::DB::Order->new(orderitems  => [],
 
1515                                 quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
 
1516                                 currency_id => $::instance_conf->get_currency_id(),);
 
1518   my $cv_id_method = $self->cv . '_id';
 
1519   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1520     $order->$cv_id_method($::form->{$cv_id_method});
 
1521     setup_order_from_cv($order);
 
1524   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1525   my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
 
1527   $order->assign_attributes(%{$::form->{order}});
 
1529   $self->setup_custom_shipto_from_form($order, $::form);
 
1531   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1532     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1533     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1536   # remove deleted items
 
1537   $self->item_ids_to_delete([]);
 
1538   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1539     my $item = $order->orderitems->[$idx];
 
1540     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1541       splice @{$order->orderitems}, $idx, 1;
 
1542       push @{$self->item_ids_to_delete}, $item->id;
 
1548   foreach my $form_attr (@{$form_orderitems}) {
 
1549     my $item = make_item($order, $form_attr);
 
1550     $item->position($pos);
 
1554   $order->add_items(grep {!$_->id} @items);
 
1559 # create or update items from form
 
1561 # Make item objects from form values. For items already existing read from db.
 
1562 # Create a new item else. And assign attributes.
 
1564   my ($record, $attr) = @_;
 
1567   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1569   my $is_new = !$item;
 
1571   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1572   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1573   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1574   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1576   $item->assign_attributes(%$attr);
 
1579     my $texts = get_part_texts($item->part, $record->language_id);
 
1580     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1581     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1582     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1590 # This is used to add one item
 
1592   my ($record, $attr) = @_;
 
1594   my $item = SL::DB::OrderItem->new;
 
1596   # Remove attributes where the user left or set the inputs empty.
 
1597   # So these attributes will be undefined and we can distinguish them
 
1598   # from zero later on.
 
1599   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1600     delete $attr->{$_} if $attr->{$_} eq '';
 
1603   $item->assign_attributes(%$attr);
 
1605   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1606   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1608   $item->unit($part->unit) if !$item->unit;
 
1611   if ( $part->is_assortment ) {
 
1612     # add assortment items with price 0, as the components carry the price
 
1613     $price_src = $price_source->price_from_source("");
 
1614     $price_src->price(0);
 
1615   } elsif (defined $item->sellprice) {
 
1616     $price_src = $price_source->price_from_source("");
 
1617     $price_src->price($item->sellprice);
 
1619     $price_src = $price_source->best_price
 
1620                ? $price_source->best_price
 
1621                : $price_source->price_from_source("");
 
1622     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
1623     $price_src->price(0) if !$price_source->best_price;
 
1627   if (defined $item->discount) {
 
1628     $discount_src = $price_source->discount_from_source("");
 
1629     $discount_src->discount($item->discount);
 
1631     $discount_src = $price_source->best_discount
 
1632                   ? $price_source->best_discount
 
1633                   : $price_source->discount_from_source("");
 
1634     $discount_src->discount(0) if !$price_source->best_discount;
 
1638   $new_attr{part}                   = $part;
 
1639   $new_attr{description}            = $part->description     if ! $item->description;
 
1640   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1641   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1642   $new_attr{sellprice}              = $price_src->price;
 
1643   $new_attr{discount}               = $discount_src->discount;
 
1644   $new_attr{active_price_source}    = $price_src;
 
1645   $new_attr{active_discount_source} = $discount_src;
 
1646   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1647   $new_attr{project_id}             = $record->globalproject_id;
 
1648   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1650   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1651   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1652   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1653   $new_attr{custom_variables} = [];
 
1655   my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1657   $item->assign_attributes(%new_attr, %{ $texts });
 
1662 sub setup_order_from_cv {
 
1665   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
 
1667   $order->intnotes($order->customervendor->notes);
 
1669   return if !$order->is_sales;
 
1671   $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1672   $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1673                       ? $order->customer->taxincluded_checked
 
1674                       : $::myconfig{taxincluded_checked});
 
1676   my $address = $order->customer->default_billing_address;;
 
1677   $order->billing_address_id($address ? $address->id : undef);
 
1680 # setup custom shipto from form
 
1682 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1683 # with 'shiptocvar_'.
 
1684 # Mark it to be deleted if a shipto from master data is selected
 
1685 # (i.e. order has a shipto).
 
1686 # Else, update or create a new custom shipto. If the fields are empty, it
 
1687 # will not be saved on save.
 
1688 sub setup_custom_shipto_from_form {
 
1689   my ($self, $order, $form) = @_;
 
1691   if ($order->shipto) {
 
1692     $self->is_custom_shipto_to_delete(1);
 
1694     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1696     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1697     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1699     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1700     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1704 # recalculate prices and taxes
 
1706 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1710   my %pat = $self->order->calculate_prices_and_taxes();
 
1712   $self->{taxes} = [];
 
1713   foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
 
1714     my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
1716     push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
 
1717                                 netamount => $netamount,
 
1718                                 tax       => SL::DB::Tax->new(id => $tax_id)->load });
 
1720   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1723 # get data for saving, printing, ..., that is not changed in the form
 
1725 # Only cvars for now.
 
1726 sub get_unalterable_data {
 
1729   foreach my $item (@{ $self->order->items }) {
 
1730     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1731     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1732     foreach my $var (@{ $item->cvars_by_config }) {
 
1733       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1735     $item->parse_custom_variable_values;
 
1741 # And remove related files in the spool directory
 
1746   my $db     = $self->order->db;
 
1748   $db->with_transaction(
 
1750       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1751       $self->order->delete;
 
1752       my $spool = $::lx_office_conf{paths}->{spool};
 
1753       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1755       $self->save_history('DELETED');
 
1758   }) || push(@{$errors}, $db->error);
 
1765 # And delete items that are deleted in the form.
 
1770   my $db     = $self->order->db;
 
1772   $db->with_transaction(sub {
 
1773     # delete custom shipto if it is to be deleted or if it is empty
 
1774     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1775       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1776       $self->order->custom_shipto(undef);
 
1779     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1780     $self->order->save(cascade => 1);
 
1783     if ($::form->{converted_from_oe_id}) {
 
1784       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1786       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1787         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1788         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1789         $src->link_to_record($self->order);
 
1791       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1793         foreach (@{ $self->order->items_sorted }) {
 
1794           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1796           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1797                                   from_id    => $from_id,
 
1798                                   to_table   => 'orderitems',
 
1805       $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
 
1808     $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
 
1810     $self->save_history('SAVED');
 
1813   }) || push(@{$errors}, $db->error);
 
1818 sub workflow_sales_or_request_for_quotation {
 
1822   my $errors = $self->save();
 
1824   if (scalar @{ $errors }) {
 
1825     $self->js->flash('error', $_) for @{ $errors };
 
1826     return $self->js->render();
 
1829   my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
 
1831   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1832   $self->{converted_from_oe_id} = delete $::form->{id};
 
1834   # set item ids to new fake id, to identify them as new items
 
1835   foreach my $item (@{$self->order->items_sorted}) {
 
1836     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1840   $::form->{type} = $destination_type;
 
1841   $self->type($self->init_type);
 
1842   $self->cv  ($self->init_cv);
 
1846   $self->get_unalterable_data();
 
1847   $self->pre_render();
 
1849   # trigger rendering values for second row as hidden, because they
 
1850   # are loaded only on demand. So we need to keep the values from the
 
1852   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1856     title => $self->get_title_for('edit'),
 
1857     %{$self->{template_args}}
 
1861 sub workflow_sales_or_purchase_order {
 
1865   my $errors = $self->save();
 
1867   if (scalar @{ $errors }) {
 
1868     $self->js->flash('error', $_) foreach @{ $errors };
 
1869     return $self->js->render();
 
1872   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1873                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1874                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1875                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1878   # check for direct delivery
 
1879   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1881   if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
 
1882       && $::form->{use_shipto} && $self->order->shipto) {
 
1883     $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
 
1886   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1887   $self->{converted_from_oe_id} = delete $::form->{id};
 
1889   # set item ids to new fake id, to identify them as new items
 
1890   foreach my $item (@{$self->order->items_sorted}) {
 
1891     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1894   if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
 
1895     if ($::form->{use_shipto}) {
 
1896       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
1898       # remove any custom shipto if not wanted
 
1899       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1904   $::form->{type} = $destination_type;
 
1905   $self->type($self->init_type);
 
1906   $self->cv  ($self->init_cv);
 
1910   $self->get_unalterable_data();
 
1911   $self->pre_render();
 
1913   # trigger rendering values for second row as hidden, because they
 
1914   # are loaded only on demand. So we need to keep the values from the
 
1916   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1920     title => $self->get_title_for('edit'),
 
1921     %{$self->{template_args}}
 
1929   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1930   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
1931   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1932   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted();
 
1933   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1936   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1939   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1941   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1942   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1943   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1944   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1945   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1947   my $print_form = Form->new('');
 
1948   $print_form->{type}        = $self->type;
 
1949   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
1950   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
1951     form => $print_form,
 
1952     options => {dialog_name_prefix => 'print_options.',
 
1956                 no_opendocument    => 0,
 
1960   foreach my $item (@{$self->order->orderitems}) {
 
1961     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1962     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1963     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1966   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1967     # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
 
1968     # Do not use write_to_objects to prevent order->delivered to be set, because this should be
 
1969     # the value from db, which can be set manually or is set when linked delivery orders are saved.
 
1970     SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
 
1973   if ($self->order->number && $::instance_conf->get_webdav) {
 
1974     my $webdav = SL::Webdav->new(
 
1975       type     => $self->type,
 
1976       number   => $self->order->number,
 
1978     my @all_objects = $webdav->get_all_objects;
 
1979     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1981                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1985   if (   (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
 
1986       && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
 
1987     $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
 
1990   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1992   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
1993                                                          edit_periodic_invoices_config calculate_qty follow_up show_history);
 
1994   $self->setup_edit_action_bar;
 
1997 sub setup_edit_action_bar {
 
1998   my ($self, %params) = @_;
 
2000   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
2001                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
2002                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
2004   my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
 
2005   my @req_cusordnumber   = qw(kivi.Order.check_cusordnumber_presence)           x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
 
2007   my $has_invoice_for_advance_payment;
 
2008   if ($self->order->id && $self->type eq sales_order_type()) {
 
2009     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2010     $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
 
2013   my $has_final_invoice;
 
2014   if ($self->order->id && $self->type eq sales_order_type()) {
 
2015     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2016     $has_final_invoice               = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
 
2019   for my $bar ($::request->layout->get('actionbar')) {
 
2024           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
2025                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
2027           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
 
2028                          @req_trans_cost_art, @req_cusordnumber,
 
2033           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
2034           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2035                          @req_trans_cost_art, @req_cusordnumber,
 
2037           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2039       ], # end of combobox "Save"
 
2046           t8('Save and Quotation'),
 
2047           submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
 
2048           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2049           only_if  => (any { $self->type eq $_ } (sales_order_type())),
 
2053           submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
 
2054           only_if  => (any { $self->type eq $_ } (purchase_order_type())),
 
2057           t8('Save and Sales Order'),
 
2058           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
2059           checks   => [ @req_trans_cost_art ],
 
2060           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
2063           t8('Save and Purchase Order'),
 
2064           call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
 
2065           checks    => [ @req_trans_cost_art, @req_cusordnumber ],
 
2066           only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
2069           t8('Save and Delivery Order'),
 
2070           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2071                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2073           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2074                          @req_trans_cost_art, @req_cusordnumber,
 
2076           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
 
2079           t8('Save and Supplier Delivery Order'),
 
2080           call      => [ 'kivi.Order.save', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2081                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2083           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2084                          @req_trans_cost_art, @req_cusordnumber,
 
2086           only_if   => (any { $self->type eq $_ } (purchase_order_type()))
 
2089           t8('Save and Invoice'),
 
2090           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2091           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2092                          @req_trans_cost_art, @req_cusordnumber,
 
2096           ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
 
2097           call      => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::instance_conf->get_order_warn_duplicate_parts ],
 
2098           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2099                          @req_trans_cost_art, @req_cusordnumber,
 
2101           disabled  => $has_final_invoice ? t8('This order has already a final invoice.')
 
2103           only_if   => (any { $self->type eq $_ } (sales_order_type())),
 
2106           t8('Save and Final Invoice'),
 
2107           call      => [ 'kivi.Order.save', 'save_and_final_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2108           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2109                          @req_trans_cost_art, @req_cusordnumber,
 
2111           disabled  => $has_final_invoice ? t8('This order has already a final invoice.')
 
2113           only_if   => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
 
2116           t8('Save and AP Transaction'),
 
2117           call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
2118           only_if   => (any { $self->type eq $_ } (purchase_order_type()))
 
2121       ], # end of combobox "Workflow"
 
2128           t8('Save and preview PDF'),
 
2129           call   => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
2130                                                         $::instance_conf->get_order_warn_no_deliverydate,
 
2132           checks => [ @req_trans_cost_art, @req_cusordnumber ],
 
2135           t8('Save and print'),
 
2136           call   => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
2137                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2139           checks => [ @req_trans_cost_art, @req_cusordnumber ],
 
2142           t8('Save and E-mail'),
 
2143           id   => 'save_and_email_action',
 
2144           call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
2145                                                                      $::instance_conf->get_order_warn_no_deliverydate,
 
2147           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2150           t8('Download attachments of all parts'),
 
2151           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
2152           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2153           only_if  => $::instance_conf->get_doc_storage,
 
2155       ], # end of combobox "Export"
 
2159         call     => [ 'kivi.Order.delete_order' ],
 
2160         confirm  => $::locale->text('Do you really want to delete this object?'),
 
2161         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2162         only_if  => $deletion_allowed,
 
2171           call     => [ 'set_history_window', $self->order->id, 'id' ],
 
2172           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
 
2176           call     => [ 'kivi.Order.follow_up_window' ],
 
2177           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2178           only_if  => $::auth->assert('productivity', 1),
 
2180       ], # end of combobox "more"
 
2186   my ($self, $doc_ref, $params) = @_;
 
2188   my $order  = $self->order;
 
2191   my $print_form = Form->new('');
 
2192   $print_form->{type}        = $order->type;
 
2193   $print_form->{formname}    = $params->{formname} || $order->type;
 
2194   $print_form->{format}      = $params->{format}   || 'pdf';
 
2195   $print_form->{media}       = $params->{media}    || 'file';
 
2196   $print_form->{groupitems}  = $params->{groupitems};
 
2197   $print_form->{printer_id}  = $params->{printer_id};
 
2198   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
2200   $order->language($params->{language});
 
2201   $order->flatten_to_form($print_form, format_amounts => 1);
 
2205   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
2206     $template_ext  = 'odt';
 
2207     $template_type = 'OpenDocument';
 
2208   } elsif ($print_form->{format} =~ m{html}i) {
 
2209     $template_ext  = 'html';
 
2210     $template_type = 'HTML';
 
2213   # search for the template
 
2214   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
2215     name        => $print_form->{formname},
 
2216     extension   => $template_ext,
 
2217     email       => $print_form->{media} eq 'email',
 
2218     language    => $params->{language},
 
2219     printer_id  => $print_form->{printer_id},
 
2222   if (!defined $template_file) {
 
2223     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);
 
2226   return @errors if scalar @errors;
 
2228   $print_form->throw_on_error(sub {
 
2230       $print_form->prepare_for_printing;
 
2232       $$doc_ref = SL::Helper::CreatePDF->create_pdf(
 
2233         format        => $print_form->{format},
 
2234         template_type => $template_type,
 
2235         template      => $template_file,
 
2236         variables     => $print_form,
 
2237         variable_content_types => {
 
2238           longdescription => 'html',
 
2239           partnotes       => 'html',
 
2241           $::form->get_variable_content_types_for_cvars,
 
2245     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2251 sub get_files_for_email_dialog {
 
2254   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2256   return %files if !$::instance_conf->get_doc_storage;
 
2258   if ($self->order->id) {
 
2259     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2260     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2261     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2262     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2266     uniq_by { $_->{id} }
 
2268       +{ id         => $_->part->id,
 
2269          partnumber => $_->part->partnumber }
 
2270     } @{$self->order->items_sorted};
 
2272   foreach my $part (@parts) {
 
2273     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2274     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2277   foreach my $key (keys %files) {
 
2278     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2284 sub make_periodic_invoices_config_from_yaml {
 
2285   my ($yaml_config) = @_;
 
2287   return if !$yaml_config;
 
2288   my $attr = SL::YAML::Load($yaml_config);
 
2289   return if 'HASH' ne ref $attr;
 
2290   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
2294 sub get_periodic_invoices_status {
 
2295   my ($self, $config) = @_;
 
2297   return                      if $self->type ne sales_order_type();
 
2298   return t8('not configured') if !$config;
 
2300   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
2301              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
2302              :                                                     die "Cannot get status of periodic invoices config";
 
2304   return $active ? t8('active') : t8('inactive');
 
2308   my ($self, $action) = @_;
 
2310   return '' if none { lc($action)} qw(add edit);
 
2313   # $::locale->text("Add Sales Order");
 
2314   # $::locale->text("Add Purchase Order");
 
2315   # $::locale->text("Add Quotation");
 
2316   # $::locale->text("Add Request for Quotation");
 
2317   # $::locale->text("Edit Sales Order");
 
2318   # $::locale->text("Edit Purchase Order");
 
2319   # $::locale->text("Edit Quotation");
 
2320   # $::locale->text("Edit Request for Quotation");
 
2322   $action = ucfirst(lc($action));
 
2323   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
2324        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
2325        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
2326        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
2330 sub get_item_cvpartnumber {
 
2331   my ($self, $item) = @_;
 
2333   return if !$self->search_cvpartnumber;
 
2334   return if !$self->order->customervendor;
 
2336   if ($self->cv eq 'vendor') {
 
2337     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2338     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2339   } elsif ($self->cv eq 'customer') {
 
2340     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2341     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2345 sub get_part_texts {
 
2346   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2348   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2349   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2351     description     => $defaults{description}     // $part->description,
 
2352     longdescription => $defaults{longdescription} // $part->notes,
 
2355   return $texts unless $language_id;
 
2357   my $translation = SL::DB::Manager::Translation->get_first(
 
2359       parts_id    => $part->id,
 
2360       language_id => $language_id,
 
2363   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2364   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2369 sub sales_order_type {
 
2373 sub purchase_order_type {
 
2377 sub sales_quotation_type {
 
2381 sub request_quotation_type {
 
2382   'request_quotation';
 
2386   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
2387        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
2388        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
2389        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
2393 sub save_and_redirect_to {
 
2394   my ($self, %params) = @_;
 
2396   my $errors = $self->save();
 
2398   if (scalar @{ $errors }) {
 
2399     $self->js->flash('error', $_) foreach @{ $errors };
 
2400     return $self->js->render();
 
2403   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
2404            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
2405            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
2406            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
2408   flash_later('info', $text);
 
2410   $self->redirect_to(%params, id => $self->order->id);
 
2414   my ($self, $addition) = @_;
 
2416   my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
 
2417   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2419   SL::DB::History->new(
 
2420     trans_id    => $self->order->id,
 
2421     employee_id => SL::DB::Manager::Employee->current->id,
 
2422     what_done   => $self->order->type,
 
2423     snumbers    => $snumbers,
 
2424     addition    => $addition,
 
2428 sub store_doc_to_webdav_and_filemanagement {
 
2429   my ($self, $content, $filename, $variant) = @_;
 
2431   my $order = $self->order;
 
2434   # copy file to webdav folder
 
2435   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2436     my $webdav = SL::Webdav->new(
 
2437       type     => $order->type,
 
2438       number   => $order->number,
 
2440     my $webdav_file = SL::Webdav::File->new(
 
2442       filename => $filename,
 
2445       $webdav_file->store(data => \$content);
 
2448       push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
 
2451   if ($order->id && $::instance_conf->get_doc_storage) {
 
2453       SL::File->save(object_id     => $order->id,
 
2454                      object_type   => $order->type,
 
2455                      mime_type     => SL::MIME->mime_type_from_ext($filename),
 
2456                      source        => 'created',
 
2457                      file_type     => 'document',
 
2458                      file_name     => $filename,
 
2459                      file_contents => $content,
 
2460                      print_variant => $variant);
 
2463       push @errors, t8('Storing the document in the storage backend failed: #1', $@);
 
2470 sub link_requirement_specs_linking_to_created_from_objects {
 
2471   my ($self, @converted_from_oe_ids) = @_;
 
2473   return unless @converted_from_oe_ids;
 
2475   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
 
2476   foreach my $rs_order (@{ $rs_orders }) {
 
2477     SL::DB::RequirementSpecOrder->new(
 
2478       order_id            => $self->order->id,
 
2479       requirement_spec_id => $rs_order->requirement_spec_id,
 
2480       version_id          => $rs_order->version_id,
 
2485 sub set_project_in_linked_requirement_specs {
 
2488   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
 
2489   foreach my $rs_order (@{ $rs_orders }) {
 
2490     next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
 
2492     $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
 
2504 SL::Controller::Order - controller for orders
 
2508 This is a new form to enter orders, completely rewritten with the use
 
2509 of controller and java script techniques.
 
2511 The aim is to provide the user a better experience and a faster workflow. Also
 
2512 the code should be more readable, more reliable and better to maintain.
 
2520 One input row, so that input happens every time at the same place.
 
2524 Use of pickers where possible.
 
2528 Possibility to enter more than one item at once.
 
2532 Item list in a scrollable area, so that the workflow buttons stay at
 
2537 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2538 possible (by partnumber, description, qty, sellprice and discount for now).
 
2542 No C<update> is necessary. All entries and calculations are managed
 
2543 with ajax-calls and the page only reloads on C<save>.
 
2547 User can see changes immediately, because of the use of java script
 
2558 =item * C<SL/Controller/Order.pm>
 
2562 =item * C<template/webpages/order/form.html>
 
2566 =item * C<template/webpages/order/tabs/basic_data.html>
 
2568 Main tab for basic_data.
 
2570 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2571 reused from generic code.
 
2575 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
2577 For displaying information on business type
 
2579 =item * C<template/webpages/order/tabs/_item_input.html>
 
2581 The input line for items
 
2583 =item * C<template/webpages/order/tabs/_row.html>
 
2585 One row for already entered items
 
2587 =item * C<template/webpages/order/tabs/_tax_row.html>
 
2589 Displaying tax information
 
2591 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
2593 Dialog for selecting price and discount sources
 
2597 =item * C<js/kivi.Order.js>
 
2599 java script functions
 
2609 =item * price sources: little symbols showing better price / better discount
 
2611 =item * select units in input row?
 
2613 =item * check for direct delivery (workflow sales order -> purchase order)
 
2615 =item * access rights
 
2617 =item * display weights
 
2621 =item * optional client/user behaviour
 
2623 (transactions has to be set - department has to be set -
 
2624  force project if enabled in client config)
 
2628 =head1 KNOWN BUGS AND CAVEATS
 
2634 Customer discount is not displayed as a valid discount in price source popup
 
2635 (this might be a bug in price sources)
 
2637 (I cannot reproduce this (Bernd))
 
2641 No indication that <shift>-up/down expands/collapses second row.
 
2645 Inline creation of parts is not currently supported
 
2649 Table header is not sticky in the scrolling area.
 
2653 Sorting does not include C<position>, neither does reordering.
 
2655 This behavior was implemented intentionally. But we can discuss, which behavior
 
2656 should be implemented.
 
2660 =head1 To discuss / Nice to have
 
2666 How to expand/collapse second row. Now it can be done clicking the icon or
 
2671 Possibility to select PriceSources in input row?
 
2675 This controller uses a (changed) copy of the template for the PriceSource
 
2676 dialog. Maybe there could be used one code source.
 
2680 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2681 form. This is not only a problem here, but also in all parts using the PTC.
 
2682 There exists a ticket and a patch. This patch should be testet.
 
2686 An indicator, if the actual inputs are saved (like in an
 
2687 editor or on text processing application).
 
2691 A warning when leaving the page without saveing unchanged inputs.
 
2698 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>