1 package SL::Controller::Order;
 
   4 use parent qw(SL::Controller::Base);
 
   6 use SL::Helper::Flash qw(flash_later);
 
   7 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
 
   8 use SL::Locale::String qw(t8);
 
   9 use SL::SessionFile::Random;
 
  14 use SL::Util qw(trim);
 
  16 use SL::DB::AdditionalBillingAddress;
 
  23 use SL::DB::PartClassification;
 
  24 use SL::DB::PartsGroup;
 
  27 use SL::DB::RecordLink;
 
  28 use SL::DB::RequirementSpec;
 
  30 use SL::DB::Translation;
 
  32 use SL::Helper::CreatePDF qw(:all);
 
  33 use SL::Helper::PrintOptions;
 
  34 use SL::Helper::ShippedQty;
 
  35 use SL::Helper::UserPreferences::PositionsScrollbar;
 
  36 use SL::Helper::UserPreferences::UpdatePositions;
 
  38 use SL::Controller::Helper::GetModels;
 
  40 use List::Util qw(first sum0);
 
  41 use List::UtilsBy qw(sort_by uniq_by);
 
  42 use List::MoreUtils qw(any none pairwise first_index);
 
  43 use English qw(-no_match_vars);
 
  48 use Rose::Object::MakeMethods::Generic
 
  50  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 
  51  'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
 
  56 __PACKAGE__->run_before('check_auth');
 
  58 __PACKAGE__->run_before('recalc',
 
  59                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
 
  62 __PACKAGE__->run_before('get_unalterable_data',
 
  63                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
 
  74   $self->order->transdate(DateTime->now_local());
 
  75   my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
  76                    $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
  78   if (   ($self->type eq sales_order_type()     &&  $::instance_conf->get_deliverydate_on)
 
  79       || ($self->type eq sales_quotation_type() &&  $::instance_conf->get_reqdate_on)
 
  80       && (!$self->order->reqdate)) {
 
  81     $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
 
  88     title => $self->get_title_for('add'),
 
  89     %{$self->{template_args}}
 
  93 # edit an existing order
 
 101     # this is to edit an order from an unsaved order object
 
 103     # set item ids to new fake id, to identify them as new items
 
 104     foreach my $item (@{$self->order->items_sorted}) {
 
 105       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 107     # trigger rendering values for second row as hidden, because they
 
 108     # are loaded only on demand. So we need to keep the values from
 
 110     $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
 117     title => $self->get_title_for('edit'),
 
 118     %{$self->{template_args}}
 
 122 # edit a collective order (consisting of one or more existing orders)
 
 123 sub action_edit_collective {
 
 127   my @multi_ids = map {
 
 128     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 129   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 131   # fall back to add if no ids are given
 
 132   if (scalar @multi_ids == 0) {
 
 137   # fall back to save as new if only one id is given
 
 138   if (scalar @multi_ids == 1) {
 
 139     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 140     $self->action_save_as_new();
 
 144   # make new order from given orders
 
 145   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 146   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 147   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 149   $self->action_edit();
 
 156   my $errors = $self->delete();
 
 158   if (scalar @{ $errors }) {
 
 159     $self->js->flash('error', $_) foreach @{ $errors };
 
 160     return $self->js->render();
 
 163   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 164            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 165            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 166            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 168   flash_later('info', $text);
 
 170   my @redirect_params = (
 
 175   $self->redirect_to(@redirect_params);
 
 182   my $errors = $self->save();
 
 184   if (scalar @{ $errors }) {
 
 185     $self->js->flash('error', $_) foreach @{ $errors };
 
 186     return $self->js->render();
 
 189   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 190            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 191            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 192            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 194   flash_later('info', $text);
 
 196   my @redirect_params = (
 
 199     id     => $self->order->id,
 
 202   $self->redirect_to(@redirect_params);
 
 205 # save the order as new document an open it for edit
 
 206 sub action_save_as_new {
 
 209   my $order = $self->order;
 
 212     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 213     return $self->js->render();
 
 216   # load order from db to check if values changed
 
 217   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 220   # Lets assign a new number if the user hasn't changed the previous one.
 
 221   # If it has been changed manually then use it as-is.
 
 222   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 224                         : trim($order->number);
 
 226   # Clear transdate unless changed
 
 227   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 228                         ? DateTime->today_local
 
 231   # Set new reqdate unless changed if it is enabled in client config
 
 232   if ($order->reqdate == $saved_order->reqdate) {
 
 233     my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
 234                      $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
 236     if (   ($self->type eq sales_order_type()     &&  !$::instance_conf->get_deliverydate_on)
 
 237         || ($self->type eq sales_quotation_type() &&  !$::instance_conf->get_reqdate_on)) {
 
 238       $new_attrs{reqdate} = '';
 
 240       $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 243     $new_attrs{reqdate} = $order->reqdate;
 
 247   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 249   # Create new record from current one
 
 250   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 252   # no linked records on save as new
 
 253   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 256   $self->action_save();
 
 261 # This is called if "print" is pressed in the print dialog.
 
 262 # If PDF creation was requested and succeeded, the pdf is offered for download
 
 263 # via send_file (which uses ajax in this case).
 
 267   my $errors = $self->save();
 
 269   if (scalar @{ $errors }) {
 
 270     $self->js->flash('error', $_) foreach @{ $errors };
 
 271     return $self->js->render();
 
 274   $self->js_reset_order_and_item_ids_after_save;
 
 276   my $format      = $::form->{print_options}->{format};
 
 277   my $media       = $::form->{print_options}->{media};
 
 278   my $formname    = $::form->{print_options}->{formname};
 
 279   my $copies      = $::form->{print_options}->{copies};
 
 280   my $groupitems  = $::form->{print_options}->{groupitems};
 
 281   my $printer_id  = $::form->{print_options}->{printer_id};
 
 283   # only PDF, OpenDocument & HTML for now
 
 284   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
 
 285     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 288   # only screen or printer by now
 
 289   if (none { $media eq $_ } qw(screen printer)) {
 
 290     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 293   # create a form for generate_attachment_filename
 
 294   my $form   = Form->new;
 
 295   $form->{$self->nr_key()}  = $self->order->number;
 
 296   $form->{type}             = $self->type;
 
 297   $form->{format}           = $format;
 
 298   $form->{formname}         = $formname;
 
 299   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 300   my $doc_filename          = $form->generate_attachment_filename();
 
 303   my @errors = $self->generate_doc(\$doc, { format     => $format,
 
 304                                             formname   => $formname,
 
 305                                             language   => $self->order->language,
 
 306                                             printer_id => $printer_id,
 
 307                                             groupitems => $groupitems });
 
 308   if (scalar @errors) {
 
 309     return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
 
 312   if ($media eq 'screen') {
 
 314     $self->js->flash('info', t8('The document has been created.'));
 
 317       type         => SL::MIME->mime_type_from_ext($doc_filename),
 
 318       name         => $doc_filename,
 
 322   } elsif ($media eq 'printer') {
 
 324     my $printer_id = $::form->{print_options}->{printer_id};
 
 325     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 330     $self->js->flash('info', t8('The document has been printed.'));
 
 333   my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename);
 
 334   if (scalar @warnings) {
 
 335     $self->js->flash('warning', $_) for @warnings;
 
 338   $self->save_history('PRINTED');
 
 341     ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
 
 344 sub action_preview_pdf {
 
 347   my $errors = $self->save();
 
 348   if (scalar @{ $errors }) {
 
 349     $self->js->flash('error', $_) foreach @{ $errors };
 
 350     return $self->js->render();
 
 353   $self->js_reset_order_and_item_ids_after_save;
 
 356   my $media       = 'screen';
 
 357   my $formname    = $self->type;
 
 360   # create a form for generate_attachment_filename
 
 361   my $form   = Form->new;
 
 362   $form->{$self->nr_key()}  = $self->order->number;
 
 363   $form->{type}             = $self->type;
 
 364   $form->{format}           = $format;
 
 365   $form->{formname}         = $formname;
 
 366   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 367   my $pdf_filename          = $form->generate_attachment_filename();
 
 370   my @errors = $self->generate_doc(\$pdf, { format     => $format,
 
 371                                             formname   => $formname,
 
 372                                             language   => $self->order->language,
 
 374   if (scalar @errors) {
 
 375     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 377   $self->save_history('PREVIEWED');
 
 378   $self->js->flash('info', t8('The PDF has been previewed'));
 
 382     type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 383     name         => $pdf_filename,
 
 388 # open the email dialog
 
 389 sub action_save_and_show_email_dialog {
 
 392   my $errors = $self->save();
 
 394   if (scalar @{ $errors }) {
 
 395     $self->js->flash('error', $_) foreach @{ $errors };
 
 396     return $self->js->render();
 
 399   my $cv_method = $self->cv;
 
 401   if (!$self->order->$cv_method) {
 
 402     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'))
 
 407   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 408   $email_form->{to} ||= $self->order->$cv_method->email;
 
 409   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 410   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 411   # Todo: get addresses from shipto, if any
 
 413   my $form = Form->new;
 
 414   $form->{$self->nr_key()}  = $self->order->number;
 
 415   $form->{cusordnumber}     = $self->order->cusordnumber;
 
 416   $form->{formname}         = $self->type;
 
 417   $form->{type}             = $self->type;
 
 418   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 419   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
 
 420   $form->{format}           = 'pdf';
 
 421   $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
 423   $email_form->{subject}             = $form->generate_email_subject();
 
 424   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 425   $email_form->{message}             = $form->generate_email_body();
 
 426   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 428   my %files = $self->get_files_for_email_dialog();
 
 430   my @employees_with_email = grep {
 
 431     my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
 
 432     $user && !!trim($user->get_config_value('email'));
 
 433   } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
 
 435   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 436                                   email_form    => $email_form,
 
 437                                   show_bcc      => $::auth->assert('email_bcc', 'may fail'),
 
 439                                   is_customer   => $self->cv eq 'customer',
 
 440                                   ALL_EMPLOYEES => \@employees_with_email,
 
 444       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 451 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 452 sub action_send_email {
 
 455   my $errors = $self->save();
 
 457   if (scalar @{ $errors }) {
 
 458     $self->js->run('kivi.Order.close_email_dialog');
 
 459     $self->js->flash('error', $_) foreach @{ $errors };
 
 460     return $self->js->render();
 
 463   $self->js_reset_order_and_item_ids_after_save;
 
 465   my $email_form  = delete $::form->{email_form};
 
 466   my %field_names = (to => 'email');
 
 468   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 470   # for Form::cleanup which may be called in Form::send_email
 
 471   $::form->{cwd}    = getcwd();
 
 472   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 474   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 475   $::form->{media}  = 'email';
 
 477   $::form->{attachment_policy} //= '';
 
 479   # Is an old file version available?
 
 481   if ($::form->{attachment_policy} eq 'old_file') {
 
 482     $attfile = SL::File->get_all(object_id   => $self->order->id,
 
 483                                  object_type => $::form->{formname},
 
 484                                  file_type   => 'document');
 
 487   if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
 
 489     my @errors = $self->generate_doc(\$doc, {media      => $::form->{media},
 
 490                                              format     => $::form->{print_options}->{format},
 
 491                                              formname   => $::form->{print_options}->{formname},
 
 492                                              language   => $self->order->language,
 
 493                                              printer_id => $::form->{print_options}->{printer_id},
 
 494                                              groupitems => $::form->{print_options}->{groupitems}});
 
 495     if (scalar @errors) {
 
 496       return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
 
 499     my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename});
 
 500     if (scalar @warnings) {
 
 501       flash_later('warning', $_) for @warnings;
 
 504     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 505     $sfile->fh->print($doc);
 
 508     $::form->{tmpfile} = $sfile->file_name;
 
 509     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 512   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 513   $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
 
 516   my $intnotes = $self->order->intnotes;
 
 517   $intnotes   .= "\n\n" if $self->order->intnotes;
 
 518   $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 519   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 520   $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 521   $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 522   $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 523   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 524   $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
 526   $self->order->update_attributes(intnotes => $intnotes);
 
 528   $self->save_history('MAILED');
 
 530   flash_later('info', t8('The email has been sent.'));
 
 532   my @redirect_params = (
 
 535     id     => $self->order->id,
 
 538   $self->redirect_to(@redirect_params);
 
 541 # open the periodic invoices config dialog
 
 543 # If there are values in the form (i.e. dialog was opened before),
 
 544 # then use this values. Create new ones, else.
 
 545 sub action_show_periodic_invoices_config_dialog {
 
 548   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 549   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 550   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 551                                                    order_value_periodicity => 'p', # = same as periodicity
 
 552                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 553                                                    extend_automatically_by => 12,
 
 555                                                    email_subject           => GenericTranslations->get(
 
 556                                                                                 language_id      => $::form->{language_id},
 
 557                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 558                                                    email_body              => GenericTranslations->get(
 
 559                                                                                 language_id      => $::form->{language_id},
 
 560                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 562   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 563   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 565   $::form->get_lists(printers => "ALL_PRINTERS",
 
 566                      charts   => { key       => 'ALL_CHARTS',
 
 567                                    transdate => 'current_date' });
 
 569   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 571   if ($::form->{customer_id}) {
 
 572     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 573     my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
 
 574     $::form->{postal_invoice}                  = $customer_object->postal_invoice;
 
 575     $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
 
 576     $config->send_email(0) if $::form->{postal_invoice};
 
 579   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 581                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 582                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 587 # assign the values of the periodic invoices config dialog
 
 588 # as yaml in the hidden tag and set the status.
 
 589 sub action_assign_periodic_invoices_config {
 
 592   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 594   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 595                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 596                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 597                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 598                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 599                  start_date_as_date         => $::form->{start_date_as_date},
 
 600                  end_date_as_date           => $::form->{end_date_as_date},
 
 601                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 602                  print                      => $::form->{print}      ? 1                         : 0,
 
 603                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 604                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 605                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 606                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 607                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 608                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 609                  email_recipient_address    => $::form->{email_recipient_address},
 
 610                  email_sender               => $::form->{email_sender},
 
 611                  email_subject              => $::form->{email_subject},
 
 612                  email_body                 => $::form->{email_body},
 
 615   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 617   my $status = $self->get_periodic_invoices_status($config);
 
 620     ->remove('#order_periodic_invoices_config')
 
 621     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 622     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 623     ->html('#periodic_invoices_status', $status)
 
 624     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 628 sub action_get_has_active_periodic_invoices {
 
 631   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 632   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 634   my $has_active_periodic_invoices =
 
 635        $self->type eq sales_order_type()
 
 638     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 639     && $config->get_previous_billed_period_start_date;
 
 641   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 644 # save the order and redirect to the frontend subroutine for a new
 
 646 sub action_save_and_delivery_order {
 
 649   $self->save_and_redirect_to(
 
 650     controller => 'oe.pl',
 
 651     action     => 'oe_delivery_order_from_order',
 
 655 # save the order and redirect to the frontend subroutine for a new
 
 657 sub action_save_and_invoice {
 
 660   $self->save_and_redirect_to(
 
 661     controller => 'oe.pl',
 
 662     action     => 'oe_invoice_from_order',
 
 666 # workflow from sales order to sales quotation
 
 667 sub action_sales_quotation {
 
 668   $_[0]->workflow_sales_or_request_for_quotation();
 
 671 # workflow from sales order to sales quotation
 
 672 sub action_request_for_quotation {
 
 673   $_[0]->workflow_sales_or_request_for_quotation();
 
 676 # workflow from sales quotation to sales order
 
 677 sub action_sales_order {
 
 678   $_[0]->workflow_sales_or_purchase_order();
 
 681 # workflow from rfq to purchase order
 
 682 sub action_purchase_order {
 
 683   $_[0]->workflow_sales_or_purchase_order();
 
 686 # workflow from purchase order to ap transaction
 
 687 sub action_save_and_ap_transaction {
 
 690   $self->save_and_redirect_to(
 
 691     controller => 'ap.pl',
 
 692     action     => 'add_from_purchase_order',
 
 696 # set form elements in respect to a changed customer or vendor
 
 698 # This action is called on an change of the customer/vendor picker.
 
 699 sub action_customer_vendor_changed {
 
 702   setup_order_from_cv($self->order);
 
 705   my $cv_method = $self->cv;
 
 707   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 708     $self->js->show('#cp_row');
 
 710     $self->js->hide('#cp_row');
 
 713   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 714     $self->js->show('#shipto_selection');
 
 716     $self->js->hide('#shipto_selection');
 
 719   if ($cv_method eq 'customer') {
 
 720     my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
 
 721     $self->js->$show_hide('#billing_address_row');
 
 724   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 727     ->replaceWith('#order_cp_id',              $self->build_contact_select)
 
 728     ->replaceWith('#order_shipto_id',          $self->build_shipto_select)
 
 729     ->replaceWith('#shipto_inputs  ',          $self->build_shipto_inputs)
 
 730     ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
 
 731     ->replaceWith('#business_info_row',        $self->build_business_info_row)
 
 732     ->val(        '#order_taxzone_id',         $self->order->taxzone_id)
 
 733     ->val(        '#order_taxincluded',        $self->order->taxincluded)
 
 734     ->val(        '#order_currency_id',        $self->order->currency_id)
 
 735     ->val(        '#order_payment_id',         $self->order->payment_id)
 
 736     ->val(        '#order_delivery_term_id',   $self->order->delivery_term_id)
 
 737     ->val(        '#order_intnotes',           $self->order->intnotes)
 
 738     ->val(        '#order_language_id',        $self->order->$cv_method->language_id)
 
 739     ->focus(      '#order_' . $self->cv . '_id')
 
 740     ->run('kivi.Order.update_exchangerate');
 
 742   $self->js_redisplay_amounts_and_taxes;
 
 743   $self->js_redisplay_cvpartnumbers;
 
 747 # open the dialog for customer/vendor details
 
 748 sub action_show_customer_vendor_details_dialog {
 
 751   my $is_customer = 'customer' eq $::form->{vc};
 
 754     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 756     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 759   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 760   $details{discount_as_percent} = $cv->discount_as_percent;
 
 761   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 762   $details{business}            = $cv->business->description      if $cv->business;
 
 763   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 764   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 765   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 766   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 768   foreach my $entry (@{ $cv->additional_billing_addresses }) {
 
 769     push @{ $details{ADDITIONAL_BILLING_ADDRESSES} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 771   foreach my $entry (@{ $cv->shipto }) {
 
 772     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 774   foreach my $entry (@{ $cv->contacts }) {
 
 775     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 778   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 779                 is_customer => $is_customer,
 
 784 # called if a unit in an existing item row is changed
 
 785 sub action_unit_changed {
 
 788   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 789   my $item = $self->order->items_sorted->[$idx];
 
 791   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 792   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 797     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 798   $self->js_redisplay_line_values;
 
 799   $self->js_redisplay_amounts_and_taxes;
 
 803 # add an item row for a new item entered in the input row
 
 804 sub action_add_item {
 
 807   delete $::form->{add_item}->{create_part_type};
 
 809   my $form_attr = $::form->{add_item};
 
 811   return unless $form_attr->{parts_id};
 
 813   my $item = new_item($self->order, $form_attr);
 
 815   $self->order->add_items($item);
 
 819   $self->get_item_cvpartnumber($item);
 
 821   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 822   my $row_as_html = $self->p->render('order/tabs/_row',
 
 828   if ($::form->{insert_before_item_id}) {
 
 830       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 833       ->append('#row_table_id', $row_as_html);
 
 836   if ( $item->part->is_assortment ) {
 
 837     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 838     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 839       my $attr = { parts_id => $assortment_item->parts_id,
 
 840                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 841                    unit     => $assortment_item->unit,
 
 842                    description => $assortment_item->part->description,
 
 844       my $item = new_item($self->order, $attr);
 
 846       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 847       $item->discount(1) unless $assortment_item->charge;
 
 849       $self->order->add_items( $item );
 
 851       $self->get_item_cvpartnumber($item);
 
 852       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 853       my $row_as_html = $self->p->render('order/tabs/_row',
 
 858       if ($::form->{insert_before_item_id}) {
 
 860           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 863           ->append('#row_table_id', $row_as_html);
 
 869     ->val('.add_item_input', '')
 
 870     ->run('kivi.Order.init_row_handlers')
 
 871     ->run('kivi.Order.renumber_positions')
 
 872     ->focus('#add_item_parts_id_name');
 
 874   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 876   $self->js_redisplay_amounts_and_taxes;
 
 880 # add item rows for multiple items at once
 
 881 sub action_add_multi_items {
 
 884   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
 
 885   return $self->js->render() unless scalar @form_attr;
 
 888   foreach my $attr (@form_attr) {
 
 889     my $item = new_item($self->order, $attr);
 
 891     if ( $item->part->is_assortment ) {
 
 892       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 893         my $attr = { parts_id => $assortment_item->parts_id,
 
 894                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 895                      unit     => $assortment_item->unit,
 
 896                      description => $assortment_item->part->description,
 
 898         my $item = new_item($self->order, $attr);
 
 900         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 901         $item->discount(1) unless $assortment_item->charge;
 
 906   $self->order->add_items(@items);
 
 910   foreach my $item (@items) {
 
 911     $self->get_item_cvpartnumber($item);
 
 912     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 913     my $row_as_html = $self->p->render('order/tabs/_row',
 
 919     if ($::form->{insert_before_item_id}) {
 
 921         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 924         ->append('#row_table_id', $row_as_html);
 
 929     ->run('kivi.Part.close_picker_dialogs')
 
 930     ->run('kivi.Order.init_row_handlers')
 
 931     ->run('kivi.Order.renumber_positions')
 
 932     ->focus('#add_item_parts_id_name');
 
 934   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 936   $self->js_redisplay_amounts_and_taxes;
 
 940 # recalculate all linetotals, amounts and taxes and redisplay them
 
 941 sub action_recalc_amounts_and_taxes {
 
 946   $self->js_redisplay_line_values;
 
 947   $self->js_redisplay_amounts_and_taxes;
 
 951 sub action_update_exchangerate {
 
 955     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
 956     currency_name => $self->order->currency->name,
 
 957     exchangerate  => $self->order->daily_exchangerate_as_null_number,
 
 960   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
 963 # redisplay item rows if they are sorted by an attribute
 
 964 sub action_reorder_items {
 
 968     partnumber   => sub { $_[0]->part->partnumber },
 
 969     description  => sub { $_[0]->description },
 
 970     qty          => sub { $_[0]->qty },
 
 971     sellprice    => sub { $_[0]->sellprice },
 
 972     discount     => sub { $_[0]->discount },
 
 973     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
 976   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
 978   my $method = $sort_keys{$::form->{order_by}};
 
 979   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
 980   if ($::form->{sort_dir}) {
 
 981     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 982       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
 984       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 987     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 988       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
 990       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 994     ->run('kivi.Order.redisplay_items', \@to_sort)
 
 998 # show the popup to choose a price/discount source
 
 999 sub action_price_popup {
 
1002   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
1003   my $item = $self->order->items_sorted->[$idx];
 
1005   $self->render_price_dialog($item);
 
1008 # save the order in a session variable and redirect to the part controller
 
1009 sub action_create_part {
 
1012   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
 
1014   my $callback     = $self->url_for(
 
1015     action       => 'return_from_create_part',
 
1016     type         => $self->type, # type is needed for check_auth on return
 
1017     previousform => $previousform,
 
1020   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.'));
 
1022   my @redirect_params = (
 
1023     controller => 'Part',
 
1025     part_type  => $::form->{add_item}->{create_part_type},
 
1026     callback   => $callback,
 
1030   $self->redirect_to(@redirect_params);
 
1033 sub action_return_from_create_part {
 
1036   $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
 
1038   $::auth->restore_form_from_session(delete $::form->{previousform});
 
1040   # set item ids to new fake id, to identify them as new items
 
1041   foreach my $item (@{$self->order->items_sorted}) {
 
1042     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1046   $self->get_unalterable_data();
 
1047   $self->pre_render();
 
1049   # trigger rendering values for second row/longdescription as hidden,
 
1050   # because they are loaded only on demand. So we need to keep the values
 
1052   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1053   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1057     title => $self->get_title_for('edit'),
 
1058     %{$self->{template_args}}
 
1063 # load the second row for one or more items
 
1065 # This action gets the html code for all items second rows by rendering a template for
 
1066 # the second row and sets the html code via client js.
 
1067 sub action_load_second_rows {
 
1070   $self->recalc() if $self->order->is_sales; # for margin calculation
 
1072   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1073     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1074     my $item = $self->order->items_sorted->[$idx];
 
1076     $self->js_load_second_row($item, $item_id, 0);
 
1079   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
1081   $self->js->render();
 
1084 # update description, notes and sellprice from master data
 
1085 sub action_update_row_from_master_data {
 
1088   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1089     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1090     my $item  = $self->order->items_sorted->[$idx];
 
1091     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1093     $item->description($texts->{description});
 
1094     $item->longdescription($texts->{longdescription});
 
1096     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1099     if ($item->part->is_assortment) {
 
1100     # add assortment items with price 0, as the components carry the price
 
1101       $price_src = $price_source->price_from_source("");
 
1102       $price_src->price(0);
 
1104       $price_src = $price_source->best_price
 
1105                  ? $price_source->best_price
 
1106                  : $price_source->price_from_source("");
 
1107       $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
 
1108       $price_src->price(0) if !$price_source->best_price;
 
1112     $item->sellprice($price_src->price);
 
1113     $item->active_price_source($price_src);
 
1116       ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
 
1117       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1118       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1119       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1121     if ($self->search_cvpartnumber) {
 
1122       $self->get_item_cvpartnumber($item);
 
1123       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1128   $self->js_redisplay_line_values;
 
1129   $self->js_redisplay_amounts_and_taxes;
 
1131   $self->js->render();
 
1134 sub js_load_second_row {
 
1135   my ($self, $item, $item_id, $do_parse) = @_;
 
1138     # Parse values from form (they are formated while rendering (template)).
 
1139     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1140     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1141     foreach my $var (@{ $item->cvars_by_config }) {
 
1142       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1144     $item->parse_custom_variable_values;
 
1147   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1150     ->html('#second_row_' . $item_id, $row_as_html)
 
1151     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1154 sub js_redisplay_line_values {
 
1157   my $is_sales = $self->order->is_sales;
 
1159   # sales orders with margins
 
1164        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1165        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1166        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1167       ]} @{ $self->order->items_sorted };
 
1171        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1172       ]} @{ $self->order->items_sorted };
 
1176     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1179 sub js_redisplay_amounts_and_taxes {
 
1182   if (scalar @{ $self->{taxes} }) {
 
1183     $self->js->show('#taxincluded_row_id');
 
1185     $self->js->hide('#taxincluded_row_id');
 
1188   if ($self->order->taxincluded) {
 
1189     $self->js->hide('#subtotal_row_id');
 
1191     $self->js->show('#subtotal_row_id');
 
1194   if ($self->order->is_sales) {
 
1195     my $is_neg = $self->order->marge_total < 0;
 
1197       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1198       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1199       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1200       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1201       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1202       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1203       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1204       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1208     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1209     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1210     ->remove('.tax_row')
 
1211     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1214 sub js_redisplay_cvpartnumbers {
 
1217   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1219   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1222     ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
 
1225 sub js_reset_order_and_item_ids_after_save {
 
1229     ->val('#id', $self->order->id)
 
1230     ->val('#converted_from_oe_id', '')
 
1231     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1234   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1235     next if !$self->order->items_sorted->[$idx]->id;
 
1236     next if $form_item_id !~ m{^new};
 
1238       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1239       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1240       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1244   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1251 sub init_valid_types {
 
1252   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1258   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1259     die "Not a valid type for order";
 
1262   $self->type($::form->{type});
 
1268   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1269          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1270          : die "Not a valid type for order";
 
1275 sub init_search_cvpartnumber {
 
1278   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1279   my $search_cvpartnumber;
 
1280   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1281   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1283   return $search_cvpartnumber;
 
1286 sub init_show_update_button {
 
1289   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1300 sub init_all_price_factors {
 
1301   SL::DB::Manager::PriceFactor->get_all;
 
1304 sub init_part_picker_classification_ids {
 
1306   my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
 
1308   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
 
1314   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1316   my $right   = $right_for->{ $self->type };
 
1317   $right    ||= 'DOES_NOT_EXIST';
 
1319   $::auth->assert($right);
 
1322 # build the selection box for contacts
 
1324 # Needed, if customer/vendor changed.
 
1325 sub build_contact_select {
 
1328   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1329     value_key  => 'cp_id',
 
1330     title_key  => 'full_name_dep',
 
1331     default    => $self->order->cp_id,
 
1333     style      => 'width: 300px',
 
1337 # build the selection box for the additional billing address
 
1339 # Needed, if customer/vendor changed.
 
1340 sub build_billing_address_select {
 
1343   select_tag('order.billing_address_id',
 
1344              [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
 
1346              title_key  => 'displayable_id',
 
1347              default    => $self->order->billing_address_id,
 
1349              style      => 'width: 300px',
 
1353 # build the selection box for shiptos
 
1355 # Needed, if customer/vendor changed.
 
1356 sub build_shipto_select {
 
1359   select_tag('order.shipto_id',
 
1360              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1361              value_key  => 'shipto_id',
 
1362              title_key  => 'displayable_id',
 
1363              default    => $self->order->shipto_id,
 
1365              style      => 'width: 300px',
 
1369 # build the inputs for the cusom shipto dialog
 
1371 # Needed, if customer/vendor changed.
 
1372 sub build_shipto_inputs {
 
1375   my $content = $self->p->render('common/_ship_to_dialog',
 
1376                                  vc_obj      => $self->order->customervendor,
 
1377                                  cs_obj      => $self->order->custom_shipto,
 
1378                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1379                                  id_selector => '#order_shipto_id');
 
1381   div_tag($content, id => 'shipto_inputs');
 
1384 # render the info line for business
 
1386 # Needed, if customer/vendor changed.
 
1387 sub build_business_info_row
 
1389   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1392 # build the rows for displaying taxes
 
1394 # Called if amounts where recalculated and redisplayed.
 
1395 sub build_tax_rows {
 
1399   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1400     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1402   return $rows_as_html;
 
1406 sub render_price_dialog {
 
1407   my ($self, $record_item) = @_;
 
1409   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1413       'kivi.io.price_chooser_dialog',
 
1414       t8('Available Prices'),
 
1415       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1420 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1421 #     $self->js->show('#dialog_flash_error');
 
1430   return if !$::form->{id};
 
1432   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1434   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1435   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1436   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1438   return $self->order;
 
1441 # load or create a new order object
 
1443 # And assign changes from the form to this object.
 
1444 # If the order is loaded from db, check if items are deleted in the form,
 
1445 # remove them form the object and collect them for removing from db on saving.
 
1446 # Then create/update items from form (via make_item) and add them.
 
1450   # add_items adds items to an order with no items for saving, but they cannot
 
1451   # be retrieved via items until the order is saved. Adding empty items to new
 
1452   # order here solves this problem.
 
1454   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1455   $order ||= SL::DB::Order->new(orderitems  => [],
 
1456                                 quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
 
1457                                 currency_id => $::instance_conf->get_currency_id(),);
 
1459   my $cv_id_method = $self->cv . '_id';
 
1460   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1461     $order->$cv_id_method($::form->{$cv_id_method});
 
1462     setup_order_from_cv($order);
 
1465   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1466   my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
 
1468   $order->assign_attributes(%{$::form->{order}});
 
1470   $self->setup_custom_shipto_from_form($order, $::form);
 
1472   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1473     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1474     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1477   # remove deleted items
 
1478   $self->item_ids_to_delete([]);
 
1479   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1480     my $item = $order->orderitems->[$idx];
 
1481     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1482       splice @{$order->orderitems}, $idx, 1;
 
1483       push @{$self->item_ids_to_delete}, $item->id;
 
1489   foreach my $form_attr (@{$form_orderitems}) {
 
1490     my $item = make_item($order, $form_attr);
 
1491     $item->position($pos);
 
1495   $order->add_items(grep {!$_->id} @items);
 
1500 # create or update items from form
 
1502 # Make item objects from form values. For items already existing read from db.
 
1503 # Create a new item else. And assign attributes.
 
1505   my ($record, $attr) = @_;
 
1508   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1510   my $is_new = !$item;
 
1512   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1513   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1514   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1515   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1517   $item->assign_attributes(%$attr);
 
1520     my $texts = get_part_texts($item->part, $record->language_id);
 
1521     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1522     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1523     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1531 # This is used to add one item
 
1533   my ($record, $attr) = @_;
 
1535   my $item = SL::DB::OrderItem->new;
 
1537   # Remove attributes where the user left or set the inputs empty.
 
1538   # So these attributes will be undefined and we can distinguish them
 
1539   # from zero later on.
 
1540   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1541     delete $attr->{$_} if $attr->{$_} eq '';
 
1544   $item->assign_attributes(%$attr);
 
1546   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1547   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1549   $item->unit($part->unit) if !$item->unit;
 
1552   if ( $part->is_assortment ) {
 
1553     # add assortment items with price 0, as the components carry the price
 
1554     $price_src = $price_source->price_from_source("");
 
1555     $price_src->price(0);
 
1556   } elsif (defined $item->sellprice) {
 
1557     $price_src = $price_source->price_from_source("");
 
1558     $price_src->price($item->sellprice);
 
1560     $price_src = $price_source->best_price
 
1561                ? $price_source->best_price
 
1562                : $price_source->price_from_source("");
 
1563     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
1564     $price_src->price(0) if !$price_source->best_price;
 
1568   if (defined $item->discount) {
 
1569     $discount_src = $price_source->discount_from_source("");
 
1570     $discount_src->discount($item->discount);
 
1572     $discount_src = $price_source->best_discount
 
1573                   ? $price_source->best_discount
 
1574                   : $price_source->discount_from_source("");
 
1575     $discount_src->discount(0) if !$price_source->best_discount;
 
1579   $new_attr{part}                   = $part;
 
1580   $new_attr{description}            = $part->description     if ! $item->description;
 
1581   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1582   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1583   $new_attr{sellprice}              = $price_src->price;
 
1584   $new_attr{discount}               = $discount_src->discount;
 
1585   $new_attr{active_price_source}    = $price_src;
 
1586   $new_attr{active_discount_source} = $discount_src;
 
1587   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1588   $new_attr{project_id}             = $record->globalproject_id;
 
1589   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1591   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1592   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1593   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1594   $new_attr{custom_variables} = [];
 
1596   my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1598   $item->assign_attributes(%new_attr, %{ $texts });
 
1603 sub setup_order_from_cv {
 
1606   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
 
1608   $order->intnotes($order->customervendor->notes);
 
1610   return if !$order->is_sales;
 
1612   $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1613   $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1614                       ? $order->customer->taxincluded_checked
 
1615                       : $::myconfig{taxincluded_checked});
 
1617   my $address = $order->customer->default_billing_address;;
 
1618   $order->billing_address_id($address ? $address->id : undef);
 
1621 # setup custom shipto from form
 
1623 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1624 # with 'shiptocvar_'.
 
1625 # Mark it to be deleted if a shipto from master data is selected
 
1626 # (i.e. order has a shipto).
 
1627 # Else, update or create a new custom shipto. If the fields are empty, it
 
1628 # will not be saved on save.
 
1629 sub setup_custom_shipto_from_form {
 
1630   my ($self, $order, $form) = @_;
 
1632   if ($order->shipto) {
 
1633     $self->is_custom_shipto_to_delete(1);
 
1635     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1637     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1638     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1640     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1641     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1645 # recalculate prices and taxes
 
1647 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1651   my %pat = $self->order->calculate_prices_and_taxes();
 
1653   $self->{taxes} = [];
 
1654   foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
 
1655     my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
1657     push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
 
1658                                 netamount => $netamount,
 
1659                                 tax       => SL::DB::Tax->new(id => $tax_id)->load });
 
1661   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1664 # get data for saving, printing, ..., that is not changed in the form
 
1666 # Only cvars for now.
 
1667 sub get_unalterable_data {
 
1670   foreach my $item (@{ $self->order->items }) {
 
1671     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1672     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1673     foreach my $var (@{ $item->cvars_by_config }) {
 
1674       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1676     $item->parse_custom_variable_values;
 
1682 # And remove related files in the spool directory
 
1687   my $db     = $self->order->db;
 
1689   $db->with_transaction(
 
1691       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1692       $self->order->delete;
 
1693       my $spool = $::lx_office_conf{paths}->{spool};
 
1694       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1696       $self->save_history('DELETED');
 
1699   }) || push(@{$errors}, $db->error);
 
1706 # And delete items that are deleted in the form.
 
1711   my $db     = $self->order->db;
 
1713   $db->with_transaction(sub {
 
1714     # delete custom shipto if it is to be deleted or if it is empty
 
1715     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1716       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1717       $self->order->custom_shipto(undef);
 
1720     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1721     $self->order->save(cascade => 1);
 
1724     if ($::form->{converted_from_oe_id}) {
 
1725       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1727       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1728         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1729         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1730         $src->link_to_record($self->order);
 
1732       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1734         foreach (@{ $self->order->items_sorted }) {
 
1735           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1737           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1738                                   from_id    => $from_id,
 
1739                                   to_table   => 'orderitems',
 
1746       $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
 
1749     $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
 
1751     $self->save_history('SAVED');
 
1754   }) || push(@{$errors}, $db->error);
 
1759 sub workflow_sales_or_request_for_quotation {
 
1763   my $errors = $self->save();
 
1765   if (scalar @{ $errors }) {
 
1766     $self->js->flash('error', $_) for @{ $errors };
 
1767     return $self->js->render();
 
1770   my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
 
1772   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1773   $self->{converted_from_oe_id} = delete $::form->{id};
 
1775   # set item ids to new fake id, to identify them as new items
 
1776   foreach my $item (@{$self->order->items_sorted}) {
 
1777     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1781   $::form->{type} = $destination_type;
 
1782   $self->type($self->init_type);
 
1783   $self->cv  ($self->init_cv);
 
1787   $self->get_unalterable_data();
 
1788   $self->pre_render();
 
1790   # trigger rendering values for second row as hidden, because they
 
1791   # are loaded only on demand. So we need to keep the values from the
 
1793   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1797     title => $self->get_title_for('edit'),
 
1798     %{$self->{template_args}}
 
1802 sub workflow_sales_or_purchase_order {
 
1806   my $errors = $self->save();
 
1808   if (scalar @{ $errors }) {
 
1809     $self->js->flash('error', $_) foreach @{ $errors };
 
1810     return $self->js->render();
 
1813   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1814                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1815                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1816                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1819   # check for direct delivery
 
1820   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1822   if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
 
1823       && $::form->{use_shipto} && $self->order->shipto) {
 
1824     $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
 
1827   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1828   $self->{converted_from_oe_id} = delete $::form->{id};
 
1830   # set item ids to new fake id, to identify them as new items
 
1831   foreach my $item (@{$self->order->items_sorted}) {
 
1832     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1835   if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
 
1836     if ($::form->{use_shipto}) {
 
1837       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
1839       # remove any custom shipto if not wanted
 
1840       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1845   $::form->{type} = $destination_type;
 
1846   $self->type($self->init_type);
 
1847   $self->cv  ($self->init_cv);
 
1851   $self->get_unalterable_data();
 
1852   $self->pre_render();
 
1854   # trigger rendering values for second row as hidden, because they
 
1855   # are loaded only on demand. So we need to keep the values from the
 
1857   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1861     title => $self->get_title_for('edit'),
 
1862     %{$self->{template_args}}
 
1870   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1871   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
1872   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1873   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted();
 
1874   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1877   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1880   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1882   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1883   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1884   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1885   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1886   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1888   my $print_form = Form->new('');
 
1889   $print_form->{type}        = $self->type;
 
1890   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
1891   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
1892     form => $print_form,
 
1893     options => {dialog_name_prefix => 'print_options.',
 
1897                 no_opendocument    => 0,
 
1901   foreach my $item (@{$self->order->orderitems}) {
 
1902     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1903     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1904     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1907   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1908     # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
 
1909     # Do not use write_to_objects to prevent order->delivered to be set, because this should be
 
1910     # the value from db, which can be set manually or is set when linked delivery orders are saved.
 
1911     SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
 
1914   if ($self->order->number && $::instance_conf->get_webdav) {
 
1915     my $webdav = SL::Webdav->new(
 
1916       type     => $self->type,
 
1917       number   => $self->order->number,
 
1919     my @all_objects = $webdav->get_all_objects;
 
1920     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1922                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1926   if (   (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
 
1927       && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
 
1928     $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
 
1931   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1933   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
1934                                                          edit_periodic_invoices_config calculate_qty follow_up show_history);
 
1935   $self->setup_edit_action_bar;
 
1938 sub setup_edit_action_bar {
 
1939   my ($self, %params) = @_;
 
1941   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
1942                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1943                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1945   my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
 
1946   my @req_cusordnumber   = qw(kivi.Order.check_cusordnumber_presence)           x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
 
1948   for my $bar ($::request->layout->get('actionbar')) {
 
1953           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1954                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
1956           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
 
1957                          @req_trans_cost_art, @req_cusordnumber,
 
1962           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1963           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
1964                          @req_trans_cost_art, @req_cusordnumber,
 
1966           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1968       ], # end of combobox "Save"
 
1975           t8('Save and Quotation'),
 
1976           submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
 
1977           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
1978           only_if  => (any { $self->type eq $_ } (sales_order_type())),
 
1982           submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
 
1983           only_if  => (any { $self->type eq $_ } (purchase_order_type())),
 
1986           t8('Save and Sales Order'),
 
1987           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
1988           checks   => [ @req_trans_cost_art ],
 
1989           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
1992           t8('Save and Purchase Order'),
 
1993           call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
 
1994           checks    => [ @req_trans_cost_art, @req_cusordnumber ],
 
1995           only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
1998           t8('Save and Delivery Order'),
 
1999           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2000                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2002           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2003                          @req_trans_cost_art, @req_cusordnumber,
 
2005           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
 
2008           t8('Save and Invoice'),
 
2009           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2010           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2011                          @req_trans_cost_art, @req_cusordnumber,
 
2015           t8('Save and AP Transaction'),
 
2016           call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
2017           only_if   => (any { $self->type eq $_ } (purchase_order_type()))
 
2020       ], # end of combobox "Workflow"
 
2027           t8('Save and preview PDF'),
 
2028           call   => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
2029                                                         $::instance_conf->get_order_warn_no_deliverydate,
 
2031           checks => [ @req_trans_cost_art, @req_cusordnumber ],
 
2034           t8('Save and print'),
 
2035           call   => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
2036                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2038           checks => [ @req_trans_cost_art, @req_cusordnumber ],
 
2041           t8('Save and E-mail'),
 
2042           id   => 'save_and_email_action',
 
2043           call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
2044                                                                      $::instance_conf->get_order_warn_no_deliverydate,
 
2046           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2049           t8('Download attachments of all parts'),
 
2050           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
2051           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2052           only_if  => $::instance_conf->get_doc_storage,
 
2054       ], # end of combobox "Export"
 
2058         call     => [ 'kivi.Order.delete_order' ],
 
2059         confirm  => $::locale->text('Do you really want to delete this object?'),
 
2060         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2061         only_if  => $deletion_allowed,
 
2070           call     => [ 'set_history_window', $self->order->id, 'id' ],
 
2071           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
 
2075           call     => [ 'kivi.Order.follow_up_window' ],
 
2076           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2077           only_if  => $::auth->assert('productivity', 1),
 
2079       ], # end of combobox "more"
 
2085   my ($self, $doc_ref, $params) = @_;
 
2087   my $order  = $self->order;
 
2090   my $print_form = Form->new('');
 
2091   $print_form->{type}        = $order->type;
 
2092   $print_form->{formname}    = $params->{formname} || $order->type;
 
2093   $print_form->{format}      = $params->{format}   || 'pdf';
 
2094   $print_form->{media}       = $params->{media}    || 'file';
 
2095   $print_form->{groupitems}  = $params->{groupitems};
 
2096   $print_form->{printer_id}  = $params->{printer_id};
 
2097   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
2099   $order->language($params->{language});
 
2100   $order->flatten_to_form($print_form, format_amounts => 1);
 
2104   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
2105     $template_ext  = 'odt';
 
2106     $template_type = 'OpenDocument';
 
2107   } elsif ($print_form->{format} =~ m{html}i) {
 
2108     $template_ext  = 'html';
 
2109     $template_type = 'HTML';
 
2112   # search for the template
 
2113   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
2114     name        => $print_form->{formname},
 
2115     extension   => $template_ext,
 
2116     email       => $print_form->{media} eq 'email',
 
2117     language    => $params->{language},
 
2118     printer_id  => $print_form->{printer_id},
 
2121   if (!defined $template_file) {
 
2122     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);
 
2125   return @errors if scalar @errors;
 
2127   $print_form->throw_on_error(sub {
 
2129       $print_form->prepare_for_printing;
 
2131       $$doc_ref = SL::Helper::CreatePDF->create_pdf(
 
2132         format        => $print_form->{format},
 
2133         template_type => $template_type,
 
2134         template      => $template_file,
 
2135         variables     => $print_form,
 
2136         variable_content_types => {
 
2137           longdescription => 'html',
 
2138           partnotes       => 'html',
 
2140           $::form->get_variable_content_types_for_cvars,
 
2144     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2150 sub get_files_for_email_dialog {
 
2153   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2155   return %files if !$::instance_conf->get_doc_storage;
 
2157   if ($self->order->id) {
 
2158     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2159     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2160     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2161     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2165     uniq_by { $_->{id} }
 
2167       +{ id         => $_->part->id,
 
2168          partnumber => $_->part->partnumber }
 
2169     } @{$self->order->items_sorted};
 
2171   foreach my $part (@parts) {
 
2172     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2173     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2176   foreach my $key (keys %files) {
 
2177     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2183 sub make_periodic_invoices_config_from_yaml {
 
2184   my ($yaml_config) = @_;
 
2186   return if !$yaml_config;
 
2187   my $attr = SL::YAML::Load($yaml_config);
 
2188   return if 'HASH' ne ref $attr;
 
2189   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
2193 sub get_periodic_invoices_status {
 
2194   my ($self, $config) = @_;
 
2196   return                      if $self->type ne sales_order_type();
 
2197   return t8('not configured') if !$config;
 
2199   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
2200              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
2201              :                                                     die "Cannot get status of periodic invoices config";
 
2203   return $active ? t8('active') : t8('inactive');
 
2207   my ($self, $action) = @_;
 
2209   return '' if none { lc($action)} qw(add edit);
 
2212   # $::locale->text("Add Sales Order");
 
2213   # $::locale->text("Add Purchase Order");
 
2214   # $::locale->text("Add Quotation");
 
2215   # $::locale->text("Add Request for Quotation");
 
2216   # $::locale->text("Edit Sales Order");
 
2217   # $::locale->text("Edit Purchase Order");
 
2218   # $::locale->text("Edit Quotation");
 
2219   # $::locale->text("Edit Request for Quotation");
 
2221   $action = ucfirst(lc($action));
 
2222   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
2223        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
2224        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
2225        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
2229 sub get_item_cvpartnumber {
 
2230   my ($self, $item) = @_;
 
2232   return if !$self->search_cvpartnumber;
 
2233   return if !$self->order->customervendor;
 
2235   if ($self->cv eq 'vendor') {
 
2236     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2237     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2238   } elsif ($self->cv eq 'customer') {
 
2239     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2240     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2244 sub get_part_texts {
 
2245   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2247   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2248   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2250     description     => $defaults{description}     // $part->description,
 
2251     longdescription => $defaults{longdescription} // $part->notes,
 
2254   return $texts unless $language_id;
 
2256   my $translation = SL::DB::Manager::Translation->get_first(
 
2258       parts_id    => $part->id,
 
2259       language_id => $language_id,
 
2262   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2263   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2268 sub sales_order_type {
 
2272 sub purchase_order_type {
 
2276 sub sales_quotation_type {
 
2280 sub request_quotation_type {
 
2281   'request_quotation';
 
2285   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
2286        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
2287        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
2288        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
2292 sub save_and_redirect_to {
 
2293   my ($self, %params) = @_;
 
2295   my $errors = $self->save();
 
2297   if (scalar @{ $errors }) {
 
2298     $self->js->flash('error', $_) foreach @{ $errors };
 
2299     return $self->js->render();
 
2302   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
2303            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
2304            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
2305            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
2307   flash_later('info', $text);
 
2309   $self->redirect_to(%params, id => $self->order->id);
 
2313   my ($self, $addition) = @_;
 
2315   my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
 
2316   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2318   SL::DB::History->new(
 
2319     trans_id    => $self->order->id,
 
2320     employee_id => SL::DB::Manager::Employee->current->id,
 
2321     what_done   => $self->order->type,
 
2322     snumbers    => $snumbers,
 
2323     addition    => $addition,
 
2327 sub store_doc_to_webdav_and_filemanagement {
 
2328   my ($self, $content, $filename) = @_;
 
2330   my $order = $self->order;
 
2333   # copy file to webdav folder
 
2334   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2335     my $webdav = SL::Webdav->new(
 
2336       type     => $order->type,
 
2337       number   => $order->number,
 
2339     my $webdav_file = SL::Webdav::File->new(
 
2341       filename => $filename,
 
2344       $webdav_file->store(data => \$content);
 
2347       push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
 
2350   if ($order->id && $::instance_conf->get_doc_storage) {
 
2352       SL::File->save(object_id     => $order->id,
 
2353                      object_type   => $order->type,
 
2354                      mime_type     => SL::MIME->mime_type_from_ext($filename),
 
2355                      source        => 'created',
 
2356                      file_type     => 'document',
 
2357                      file_name     => $filename,
 
2358                      file_contents => $content);
 
2361       push @errors, t8('Storing the document in the storage backend failed: #1', $@);
 
2368 sub link_requirement_specs_linking_to_created_from_objects {
 
2369   my ($self, @converted_from_oe_ids) = @_;
 
2371   return unless @converted_from_oe_ids;
 
2373   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
 
2374   foreach my $rs_order (@{ $rs_orders }) {
 
2375     SL::DB::RequirementSpecOrder->new(
 
2376       order_id            => $self->order->id,
 
2377       requirement_spec_id => $rs_order->requirement_spec_id,
 
2378       version_id          => $rs_order->version_id,
 
2383 sub set_project_in_linked_requirement_specs {
 
2386   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
 
2387   foreach my $rs_order (@{ $rs_orders }) {
 
2388     next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
 
2390     $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
 
2402 SL::Controller::Order - controller for orders
 
2406 This is a new form to enter orders, completely rewritten with the use
 
2407 of controller and java script techniques.
 
2409 The aim is to provide the user a better experience and a faster workflow. Also
 
2410 the code should be more readable, more reliable and better to maintain.
 
2418 One input row, so that input happens every time at the same place.
 
2422 Use of pickers where possible.
 
2426 Possibility to enter more than one item at once.
 
2430 Item list in a scrollable area, so that the workflow buttons stay at
 
2435 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2436 possible (by partnumber, description, qty, sellprice and discount for now).
 
2440 No C<update> is necessary. All entries and calculations are managed
 
2441 with ajax-calls and the page only reloads on C<save>.
 
2445 User can see changes immediately, because of the use of java script
 
2456 =item * C<SL/Controller/Order.pm>
 
2460 =item * C<template/webpages/order/form.html>
 
2464 =item * C<template/webpages/order/tabs/basic_data.html>
 
2466 Main tab for basic_data.
 
2468 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2469 reused from generic code.
 
2473 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
2475 For displaying information on business type
 
2477 =item * C<template/webpages/order/tabs/_item_input.html>
 
2479 The input line for items
 
2481 =item * C<template/webpages/order/tabs/_row.html>
 
2483 One row for already entered items
 
2485 =item * C<template/webpages/order/tabs/_tax_row.html>
 
2487 Displaying tax information
 
2489 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
2491 Dialog for selecting price and discount sources
 
2495 =item * C<js/kivi.Order.js>
 
2497 java script functions
 
2507 =item * price sources: little symbols showing better price / better discount
 
2509 =item * select units in input row?
 
2511 =item * check for direct delivery (workflow sales order -> purchase order)
 
2513 =item * access rights
 
2515 =item * display weights
 
2519 =item * optional client/user behaviour
 
2521 (transactions has to be set - department has to be set -
 
2522  force project if enabled in client config)
 
2526 =head1 KNOWN BUGS AND CAVEATS
 
2532 Customer discount is not displayed as a valid discount in price source popup
 
2533 (this might be a bug in price sources)
 
2535 (I cannot reproduce this (Bernd))
 
2539 No indication that <shift>-up/down expands/collapses second row.
 
2543 Inline creation of parts is not currently supported
 
2547 Table header is not sticky in the scrolling area.
 
2551 Sorting does not include C<position>, neither does reordering.
 
2553 This behavior was implemented intentionally. But we can discuss, which behavior
 
2554 should be implemented.
 
2558 =head1 To discuss / Nice to have
 
2564 How to expand/collapse second row. Now it can be done clicking the icon or
 
2569 Possibility to select PriceSources in input row?
 
2573 This controller uses a (changed) copy of the template for the PriceSource
 
2574 dialog. Maybe there could be used one code source.
 
2578 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2579 form. This is not only a problem here, but also in all parts using the PTC.
 
2580 There exists a ticket and a patch. This patch should be testet.
 
2584 An indicator, if the actual inputs are saved (like in an
 
2585 editor or on text processing application).
 
2589 A warning when leaving the page without saveing unchanged inputs.
 
2596 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>