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);
 
  20 use SL::DB::PartsGroup;
 
  23 use SL::DB::RecordLink;
 
  26 use SL::Helper::CreatePDF qw(:all);
 
  27 use SL::Helper::PrintOptions;
 
  28 use SL::Helper::ShippedQty;
 
  29 use SL::Helper::UserPreferences::PositionsScrollbar;
 
  30 use SL::Helper::UserPreferences::UpdatePositions;
 
  32 use SL::Controller::Helper::GetModels;
 
  34 use List::Util qw(first sum0);
 
  35 use List::UtilsBy qw(sort_by uniq_by);
 
  36 use List::MoreUtils qw(any none pairwise first_index);
 
  37 use English qw(-no_match_vars);
 
  42 use Rose::Object::MakeMethods::Generic
 
  44  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 
  45  'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors search_cvpartnumber show_update_button) ],
 
  50 __PACKAGE__->run_before('check_auth');
 
  52 __PACKAGE__->run_before('recalc',
 
  53                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
 
  56 __PACKAGE__->run_before('get_unalterable_data',
 
  57                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
 
  68   $self->order->transdate(DateTime->now_local());
 
  69   my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
 
  70                    $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
 
  71   $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
 
  77     title => $self->get_title_for('add'),
 
  78     %{$self->{template_args}}
 
  82 # edit an existing order
 
  90     # this is to edit an order from an unsaved order object
 
  92     # set item ids to new fake id, to identify them as new items
 
  93     foreach my $item (@{$self->order->items_sorted}) {
 
  94       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
  96     # trigger rendering values for second row/longdescription as hidden,
 
  97     # because they are loaded only on demand. So we need to keep the values
 
  99     $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
 100     $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
 107     title => $self->get_title_for('edit'),
 
 108     %{$self->{template_args}}
 
 112 # edit a collective order (consisting of one or more existing orders)
 
 113 sub action_edit_collective {
 
 117   my @multi_ids = map {
 
 118     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 119   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 121   # fall back to add if no ids are given
 
 122   if (scalar @multi_ids == 0) {
 
 127   # fall back to save as new if only one id is given
 
 128   if (scalar @multi_ids == 1) {
 
 129     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 130     $self->action_save_as_new();
 
 134   # make new order from given orders
 
 135   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 136   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 137   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 139   $self->action_edit();
 
 146   my $errors = $self->delete();
 
 148   if (scalar @{ $errors }) {
 
 149     $self->js->flash('error', $_) foreach @{ $errors };
 
 150     return $self->js->render();
 
 153   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 154            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 155            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 156            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 158   flash_later('info', $text);
 
 160   my @redirect_params = (
 
 165   $self->redirect_to(@redirect_params);
 
 172   my $errors = $self->save();
 
 174   if (scalar @{ $errors }) {
 
 175     $self->js->flash('error', $_) foreach @{ $errors };
 
 176     return $self->js->render();
 
 179   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 180            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 181            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 182            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 184   flash_later('info', $text);
 
 186   my @redirect_params = (
 
 189     id     => $self->order->id,
 
 192   $self->redirect_to(@redirect_params);
 
 195 # save the order as new document an open it for edit
 
 196 sub action_save_as_new {
 
 199   my $order = $self->order;
 
 202     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 203     return $self->js->render();
 
 206   # load order from db to check if values changed
 
 207   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 210   # Lets assign a new number if the user hasn't changed the previous one.
 
 211   # If it has been changed manually then use it as-is.
 
 212   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 214                         : trim($order->number);
 
 216   # Clear transdate unless changed
 
 217   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 218                         ? DateTime->today_local
 
 221   # Set new reqdate unless changed
 
 222   if ($order->reqdate == $saved_order->reqdate) {
 
 223     my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
 
 224                      $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
 
 225     $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 227     $new_attrs{reqdate} = $order->reqdate;
 
 231   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 233   # Create new record from current one
 
 234   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 236   # no linked records on save as new
 
 237   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 240   $self->action_save();
 
 245 # This is called if "print" is pressed in the print dialog.
 
 246 # If PDF creation was requested and succeeded, the pdf is offered for download
 
 247 # via send_file (which uses ajax in this case).
 
 251   my $errors = $self->save();
 
 253   if (scalar @{ $errors }) {
 
 254     $self->js->flash('error', $_) foreach @{ $errors };
 
 255     return $self->js->render();
 
 258   $self->js_reset_order_and_item_ids_after_save;
 
 260   my $format      = $::form->{print_options}->{format};
 
 261   my $media       = $::form->{print_options}->{media};
 
 262   my $formname    = $::form->{print_options}->{formname};
 
 263   my $copies      = $::form->{print_options}->{copies};
 
 264   my $groupitems  = $::form->{print_options}->{groupitems};
 
 266   # only pdf and opendocument by now
 
 267   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
 
 268     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 271   # only screen or printer by now
 
 272   if (none { $media eq $_ } qw(screen printer)) {
 
 273     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 277   $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 279   # create a form for generate_attachment_filename
 
 280   my $form   = Form->new;
 
 281   $form->{$self->nr_key()}  = $self->order->number;
 
 282   $form->{type}             = $self->type;
 
 283   $form->{format}           = $format;
 
 284   $form->{formname}         = $formname;
 
 285   $form->{language}         = '_' . $language->template_code if $language;
 
 286   my $pdf_filename          = $form->generate_attachment_filename();
 
 289   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
 
 290                                                    formname   => $formname,
 
 291                                                    language   => $language,
 
 292                                                    groupitems => $groupitems });
 
 293   if (scalar @errors) {
 
 294     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 297   if ($media eq 'screen') {
 
 299     $self->js->flash('info', t8('The PDF has been created'));
 
 302       type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 303       name         => $pdf_filename,
 
 307   } elsif ($media eq 'printer') {
 
 309     my $printer_id = $::form->{print_options}->{printer_id};
 
 310     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 315     $self->js->flash('info', t8('The PDF has been printed'));
 
 318   # copy file to webdav folder
 
 319   if ($self->order->number && $::instance_conf->get_webdav_documents) {
 
 320     my $webdav = SL::Webdav->new(
 
 322       number   => $self->order->number,
 
 324     my $webdav_file = SL::Webdav::File->new(
 
 326       filename => $pdf_filename,
 
 329       $webdav_file->store(data => \$pdf);
 
 332       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
 
 335   if ($self->order->number && $::instance_conf->get_doc_storage) {
 
 337       SL::File->save(object_id     => $self->order->id,
 
 338                      object_type   => $self->type,
 
 339                      mime_type     => 'application/pdf',
 
 341                      file_type     => 'document',
 
 342                      file_name     => $pdf_filename,
 
 343                      file_contents => $pdf);
 
 346       $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
 
 352 # open the email dialog
 
 353 sub action_save_and_show_email_dialog {
 
 356   my $errors = $self->save();
 
 358   if (scalar @{ $errors }) {
 
 359     $self->js->flash('error', $_) foreach @{ $errors };
 
 360     return $self->js->render();
 
 363   my $cv_method = $self->cv;
 
 365   if (!$self->order->$cv_method) {
 
 366     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'))
 
 371   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 372   $email_form->{to} ||= $self->order->$cv_method->email;
 
 373   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 374   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 375   # Todo: get addresses from shipto, if any
 
 377   my $form = Form->new;
 
 378   $form->{$self->nr_key()}  = $self->order->number;
 
 379   $form->{cusordnumber}     = $self->order->cusordnumber;
 
 380   $form->{formname}         = $self->type;
 
 381   $form->{type}             = $self->type;
 
 382   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 383   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
 
 384   $form->{format}           = 'pdf';
 
 386   $email_form->{subject}             = $form->generate_email_subject();
 
 387   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 388   $email_form->{message}             = $form->generate_email_body();
 
 389   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 391   my %files = $self->get_files_for_email_dialog();
 
 392   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 393                                   email_form  => $email_form,
 
 394                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
 
 396                                   is_customer => $self->cv eq 'customer',
 
 400       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 407 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 408 sub action_send_email {
 
 411   my $errors = $self->save();
 
 413   if (scalar @{ $errors }) {
 
 414     $self->js->run('kivi.Order.close_email_dialog');
 
 415     $self->js->flash('error', $_) foreach @{ $errors };
 
 416     return $self->js->render();
 
 419   $self->js_reset_order_and_item_ids_after_save;
 
 421   my $email_form  = delete $::form->{email_form};
 
 422   my %field_names = (to => 'email');
 
 424   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 426   # for Form::cleanup which may be called in Form::send_email
 
 427   $::form->{cwd}    = getcwd();
 
 428   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 430   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 431   $::form->{media}  = 'email';
 
 433   if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
 
 435     $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 438     my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
 
 439                                                     format     => $::form->{print_options}->{format},
 
 440                                                     formname   => $::form->{print_options}->{formname},
 
 441                                                     language   => $language,
 
 442                                                     groupitems => $::form->{print_options}->{groupitems}});
 
 443     if (scalar @errors) {
 
 444       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
 
 447     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 448     $sfile->fh->print($pdf);
 
 451     $::form->{tmpfile} = $sfile->file_name;
 
 452     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 455   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 456   $::form->send_email(\%::myconfig, 'pdf');
 
 459   my $intnotes = $self->order->intnotes;
 
 460   $intnotes   .= "\n\n" if $self->order->intnotes;
 
 461   $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 462   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 463   $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 464   $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 465   $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 466   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 467   $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
 469   $self->order->update_attributes(intnotes => $intnotes);
 
 471   flash_later('info', t8('The email has been sent.'));
 
 473   my @redirect_params = (
 
 476     id     => $self->order->id,
 
 479   $self->redirect_to(@redirect_params);
 
 482 # open the periodic invoices config dialog
 
 484 # If there are values in the form (i.e. dialog was opened before),
 
 485 # then use this values. Create new ones, else.
 
 486 sub action_show_periodic_invoices_config_dialog {
 
 489   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 490   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 491   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 492                                                    order_value_periodicity => 'p', # = same as periodicity
 
 493                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 494                                                    extend_automatically_by => 12,
 
 496                                                    email_subject           => GenericTranslations->get(
 
 497                                                                                 language_id      => $::form->{language_id},
 
 498                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 499                                                    email_body              => GenericTranslations->get(
 
 500                                                                                 language_id      => $::form->{language_id},
 
 501                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 503   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 504   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 506   $::form->get_lists(printers => "ALL_PRINTERS",
 
 507                      charts   => { key       => 'ALL_CHARTS',
 
 508                                    transdate => 'current_date' });
 
 510   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 512   if ($::form->{customer_id}) {
 
 513     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 514     $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail;
 
 517   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 519                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 520                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 525 # assign the values of the periodic invoices config dialog
 
 526 # as yaml in the hidden tag and set the status.
 
 527 sub action_assign_periodic_invoices_config {
 
 530   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 532   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 533                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 534                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 535                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 536                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 537                  start_date_as_date         => $::form->{start_date_as_date},
 
 538                  end_date_as_date           => $::form->{end_date_as_date},
 
 539                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 540                  print                      => $::form->{print}      ? 1                         : 0,
 
 541                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 542                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 543                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 544                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 545                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 546                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 547                  email_recipient_address    => $::form->{email_recipient_address},
 
 548                  email_sender               => $::form->{email_sender},
 
 549                  email_subject              => $::form->{email_subject},
 
 550                  email_body                 => $::form->{email_body},
 
 553   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 555   my $status = $self->get_periodic_invoices_status($config);
 
 558     ->remove('#order_periodic_invoices_config')
 
 559     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 560     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 561     ->html('#periodic_invoices_status', $status)
 
 562     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 566 sub action_get_has_active_periodic_invoices {
 
 569   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 570   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 572   my $has_active_periodic_invoices =
 
 573        $self->type eq sales_order_type()
 
 576     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 577     && $config->get_previous_billed_period_start_date;
 
 579   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 582 # save the order and redirect to the frontend subroutine for a new
 
 584 sub action_save_and_delivery_order {
 
 587   my $errors = $self->save();
 
 589   if (scalar @{ $errors }) {
 
 590     $self->js->flash('error', $_) foreach @{ $errors };
 
 591     return $self->js->render();
 
 594   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 595            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 596            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 597            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 599   flash_later('info', $text);
 
 601   my @redirect_params = (
 
 602     controller => 'oe.pl',
 
 603     action     => 'oe_delivery_order_from_order',
 
 604     id         => $self->order->id,
 
 607   $self->redirect_to(@redirect_params);
 
 610 # save the order and redirect to the frontend subroutine for a new
 
 612 sub action_save_and_invoice {
 
 615   my $errors = $self->save();
 
 617   if (scalar @{ $errors }) {
 
 618     $self->js->flash('error', $_) foreach @{ $errors };
 
 619     return $self->js->render();
 
 622   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 623            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 624            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 625            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 627   flash_later('info', $text);
 
 629   my @redirect_params = (
 
 630     controller => 'oe.pl',
 
 631     action     => 'oe_invoice_from_order',
 
 632     id         => $self->order->id,
 
 635   $self->redirect_to(@redirect_params);
 
 638 # workflow from sales quotation to sales order
 
 639 sub action_sales_order {
 
 640   $_[0]->workflow_sales_or_purchase_order();
 
 643 # workflow from rfq to purchase order
 
 644 sub action_purchase_order {
 
 645   $_[0]->workflow_sales_or_purchase_order();
 
 648 # workflow from purchase order to ap transaction
 
 649 sub action_save_and_ap_transaction {
 
 652   my $errors = $self->save();
 
 654   if (scalar @{ $errors }) {
 
 655     $self->js->flash('error', $_) foreach @{ $errors };
 
 656     return $self->js->render();
 
 659   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 660            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 661            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 662            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 664   flash_later('info', $text);
 
 666   my @redirect_params = (
 
 667     controller => 'ap.pl',
 
 668     action     => 'add_from_purchase_order',
 
 669     id         => $self->order->id,
 
 672   $self->redirect_to(@redirect_params);
 
 675 # set form elements in respect to a changed customer or vendor
 
 677 # This action is called on an change of the customer/vendor picker.
 
 678 sub action_customer_vendor_changed {
 
 681   setup_order_from_cv($self->order);
 
 684   my $cv_method = $self->cv;
 
 686   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 687     $self->js->show('#cp_row');
 
 689     $self->js->hide('#cp_row');
 
 692   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 693     $self->js->show('#shipto_selection');
 
 695     $self->js->hide('#shipto_selection');
 
 698   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 701     ->replaceWith('#order_cp_id',            $self->build_contact_select)
 
 702     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
 
 703     ->replaceWith('#shipto_inputs  ',        $self->build_shipto_inputs)
 
 704     ->replaceWith('#business_info_row',      $self->build_business_info_row)
 
 705     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
 
 706     ->val(        '#order_taxincluded',      $self->order->taxincluded)
 
 707     ->val(        '#order_currency_id',      $self->order->currency_id)
 
 708     ->val(        '#order_payment_id',       $self->order->payment_id)
 
 709     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
 
 710     ->val(        '#order_intnotes',         $self->order->intnotes)
 
 711     ->val(        '#language_id',            $self->order->$cv_method->language_id)
 
 712     ->focus(      '#order_' . $self->cv . '_id')
 
 713     ->run('kivi.Order.update_exchangerate');
 
 715   $self->js_redisplay_amounts_and_taxes;
 
 716   $self->js_redisplay_cvpartnumbers;
 
 720 # open the dialog for customer/vendor details
 
 721 sub action_show_customer_vendor_details_dialog {
 
 724   my $is_customer = 'customer' eq $::form->{vc};
 
 727     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 729     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 732   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 733   $details{discount_as_percent} = $cv->discount_as_percent;
 
 734   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 735   $details{business}            = $cv->business->description      if $cv->business;
 
 736   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 737   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 738   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 739   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 741   foreach my $entry (@{ $cv->shipto }) {
 
 742     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 744   foreach my $entry (@{ $cv->contacts }) {
 
 745     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 748   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 749                 is_customer => $is_customer,
 
 754 # called if a unit in an existing item row is changed
 
 755 sub action_unit_changed {
 
 758   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 759   my $item = $self->order->items_sorted->[$idx];
 
 761   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 762   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 767     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 768   $self->js_redisplay_line_values;
 
 769   $self->js_redisplay_amounts_and_taxes;
 
 773 # add an item row for a new item entered in the input row
 
 774 sub action_add_item {
 
 777   my $form_attr = $::form->{add_item};
 
 779   return unless $form_attr->{parts_id};
 
 781   my $item = new_item($self->order, $form_attr);
 
 783   $self->order->add_items($item);
 
 787   $self->get_item_cvpartnumber($item);
 
 789   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 790   my $row_as_html = $self->p->render('order/tabs/_row',
 
 796   if ($::form->{insert_before_item_id}) {
 
 798       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 801       ->append('#row_table_id', $row_as_html);
 
 804   if ( $item->part->is_assortment ) {
 
 805     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 806     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 807       my $attr = { parts_id => $assortment_item->parts_id,
 
 808                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 809                    unit     => $assortment_item->unit,
 
 810                    description => $assortment_item->part->description,
 
 812       my $item = new_item($self->order, $attr);
 
 814       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 815       $item->discount(1) unless $assortment_item->charge;
 
 817       $self->order->add_items( $item );
 
 819       $self->get_item_cvpartnumber($item);
 
 820       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 821       my $row_as_html = $self->p->render('order/tabs/_row',
 
 826       if ($::form->{insert_before_item_id}) {
 
 828           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 831           ->append('#row_table_id', $row_as_html);
 
 837     ->val('.add_item_input', '')
 
 838     ->run('kivi.Order.init_row_handlers')
 
 839     ->run('kivi.Order.renumber_positions')
 
 840     ->focus('#add_item_parts_id_name');
 
 842   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 844   $self->js_redisplay_amounts_and_taxes;
 
 848 # open the dialog for entering multiple items at once
 
 849 sub action_show_multi_items_dialog {
 
 850   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
 
 851                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
 
 854 # update the filter results in the multi item dialog
 
 855 sub action_multi_items_update_result {
 
 858   $::form->{multi_items}->{filter}->{obsolete} = 0;
 
 860   my $count = $_[0]->multi_items_models->count;
 
 863     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
 
 864     $_[0]->render($text, { layout => 0 });
 
 865   } elsif ($count > $max_count) {
 
 866     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
 
 867     $_[0]->render($text, { layout => 0 });
 
 869     my $multi_items = $_[0]->multi_items_models->get;
 
 870     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
 
 871                   multi_items => $multi_items);
 
 875 # add item rows for multiple items at once
 
 876 sub action_add_multi_items {
 
 879   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
 
 880   return $self->js->render() unless scalar @form_attr;
 
 883   foreach my $attr (@form_attr) {
 
 884     my $item = new_item($self->order, $attr);
 
 886     if ( $item->part->is_assortment ) {
 
 887       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 888         my $attr = { parts_id => $assortment_item->parts_id,
 
 889                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 890                      unit     => $assortment_item->unit,
 
 891                      description => $assortment_item->part->description,
 
 893         my $item = new_item($self->order, $attr);
 
 895         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 896         $item->discount(1) unless $assortment_item->charge;
 
 901   $self->order->add_items(@items);
 
 905   foreach my $item (@items) {
 
 906     $self->get_item_cvpartnumber($item);
 
 907     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 908     my $row_as_html = $self->p->render('order/tabs/_row',
 
 914     if ($::form->{insert_before_item_id}) {
 
 916         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 919         ->append('#row_table_id', $row_as_html);
 
 924     ->run('kivi.Order.close_multi_items_dialog')
 
 925     ->run('kivi.Order.init_row_handlers')
 
 926     ->run('kivi.Order.renumber_positions')
 
 927     ->focus('#add_item_parts_id_name');
 
 929   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 931   $self->js_redisplay_amounts_and_taxes;
 
 935 # recalculate all linetotals, amounts and taxes and redisplay them
 
 936 sub action_recalc_amounts_and_taxes {
 
 941   $self->js_redisplay_line_values;
 
 942   $self->js_redisplay_amounts_and_taxes;
 
 946 sub action_update_exchangerate {
 
 950     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
 951     currency_name => $self->order->currency->name,
 
 952     exchangerate  => $self->order->daily_exchangerate_as_null_number,
 
 955   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
 958 # redisplay item rows if they are sorted by an attribute
 
 959 sub action_reorder_items {
 
 963     partnumber   => sub { $_[0]->part->partnumber },
 
 964     description  => sub { $_[0]->description },
 
 965     qty          => sub { $_[0]->qty },
 
 966     sellprice    => sub { $_[0]->sellprice },
 
 967     discount     => sub { $_[0]->discount },
 
 968     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
 971   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
 973   my $method = $sort_keys{$::form->{order_by}};
 
 974   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
 975   if ($::form->{sort_dir}) {
 
 976     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 977       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
 979       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 982     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 983       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
 985       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 989     ->run('kivi.Order.redisplay_items', \@to_sort)
 
 993 # show the popup to choose a price/discount source
 
 994 sub action_price_popup {
 
 997   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 998   my $item = $self->order->items_sorted->[$idx];
 
1000   $self->render_price_dialog($item);
 
1003 # get the longdescription for an item if the dialog to enter/change the
 
1004 # longdescription was opened and the longdescription is empty
 
1006 # If this item is new, get the longdescription from Part.
 
1007 # Otherwise get it from OrderItem.
 
1008 sub action_get_item_longdescription {
 
1009   my $longdescription;
 
1011   if ($::form->{item_id}) {
 
1012     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
 
1013   } elsif ($::form->{parts_id}) {
 
1014     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
 
1016   $_[0]->render(\ $longdescription, { type => 'text' });
 
1019 # load the second row for one or more items
 
1021 # This action gets the html code for all items second rows by rendering a template for
 
1022 # the second row and sets the html code via client js.
 
1023 sub action_load_second_rows {
 
1026   $self->recalc() if $self->order->is_sales; # for margin calculation
 
1028   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1029     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1030     my $item = $self->order->items_sorted->[$idx];
 
1032     $self->js_load_second_row($item, $item_id, 0);
 
1035   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
1037   $self->js->render();
 
1040 # update description, notes and sellprice from master data
 
1041 sub action_update_row_from_master_data {
 
1044   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1045     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1046     my $item = $self->order->items_sorted->[$idx];
 
1048     $item->description($item->part->description);
 
1049     $item->longdescription($item->part->notes);
 
1051     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1054     if ($item->part->is_assortment) {
 
1055     # add assortment items with price 0, as the components carry the price
 
1056       $price_src = $price_source->price_from_source("");
 
1057       $price_src->price(0);
 
1059       $price_src = $price_source->best_price
 
1060                  ? $price_source->best_price
 
1061                  : $price_source->price_from_source("");
 
1062       $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
 
1063       $price_src->price(0) if !$price_source->best_price;
 
1067     $item->sellprice($price_src->price);
 
1068     $item->active_price_source($price_src);
 
1071       ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
 
1072       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1073       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1074       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1076     if ($self->search_cvpartnumber) {
 
1077       $self->get_item_cvpartnumber($item);
 
1078       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1083   $self->js_redisplay_line_values;
 
1084   $self->js_redisplay_amounts_and_taxes;
 
1086   $self->js->render();
 
1089 sub js_load_second_row {
 
1090   my ($self, $item, $item_id, $do_parse) = @_;
 
1093     # Parse values from form (they are formated while rendering (template)).
 
1094     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1095     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1096     foreach my $var (@{ $item->cvars_by_config }) {
 
1097       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1099     $item->parse_custom_variable_values;
 
1102   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1105     ->html('#second_row_' . $item_id, $row_as_html)
 
1106     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1109 sub js_redisplay_line_values {
 
1112   my $is_sales = $self->order->is_sales;
 
1114   # sales orders with margins
 
1119        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1120        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1121        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1122       ]} @{ $self->order->items_sorted };
 
1126        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1127       ]} @{ $self->order->items_sorted };
 
1131     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1134 sub js_redisplay_amounts_and_taxes {
 
1137   if (scalar @{ $self->{taxes} }) {
 
1138     $self->js->show('#taxincluded_row_id');
 
1140     $self->js->hide('#taxincluded_row_id');
 
1143   if ($self->order->taxincluded) {
 
1144     $self->js->hide('#subtotal_row_id');
 
1146     $self->js->show('#subtotal_row_id');
 
1149   if ($self->order->is_sales) {
 
1150     my $is_neg = $self->order->marge_total < 0;
 
1152       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1153       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1154       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1155       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1156       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1157       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1158       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1159       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1163     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1164     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1165     ->remove('.tax_row')
 
1166     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1169 sub js_redisplay_cvpartnumbers {
 
1172   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1174   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1177     ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
 
1180 sub js_reset_order_and_item_ids_after_save {
 
1184     ->val('#id', $self->order->id)
 
1185     ->val('#converted_from_oe_id', '')
 
1186     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1189   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1190     next if !$self->order->items_sorted->[$idx]->id;
 
1191     next if $form_item_id !~ m{^new};
 
1193       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1194       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1195       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1199   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1206 sub init_valid_types {
 
1207   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1213   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1214     die "Not a valid type for order";
 
1217   $self->type($::form->{type});
 
1223   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1224          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1225          : die "Not a valid type for order";
 
1230 sub init_search_cvpartnumber {
 
1233   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1234   my $search_cvpartnumber;
 
1235   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1236   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1238   return $search_cvpartnumber;
 
1241 sub init_show_update_button {
 
1244   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1255 # model used to filter/display the parts in the multi-items dialog
 
1256 sub init_multi_items_models {
 
1257   SL::Controller::Helper::GetModels->new(
 
1258     controller     => $_[0],
 
1260     with_objects   => [ qw(unit_obj) ],
 
1261     disable_plugin => 'paginated',
 
1262     source         => $::form->{multi_items},
 
1268       partnumber  => t8('Partnumber'),
 
1269       description => t8('Description')}
 
1273 sub init_all_price_factors {
 
1274   SL::DB::Manager::PriceFactor->get_all;
 
1280   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1282   my $right   = $right_for->{ $self->type };
 
1283   $right    ||= 'DOES_NOT_EXIST';
 
1285   $::auth->assert($right);
 
1288 # build the selection box for contacts
 
1290 # Needed, if customer/vendor changed.
 
1291 sub build_contact_select {
 
1294   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1295     value_key  => 'cp_id',
 
1296     title_key  => 'full_name_dep',
 
1297     default    => $self->order->cp_id,
 
1299     style      => 'width: 300px',
 
1303 # build the selection box for shiptos
 
1305 # Needed, if customer/vendor changed.
 
1306 sub build_shipto_select {
 
1309   select_tag('order.shipto_id',
 
1310              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1311              value_key  => 'shipto_id',
 
1312              title_key  => 'displayable_id',
 
1313              default    => $self->order->shipto_id,
 
1315              style      => 'width: 300px',
 
1319 # build the inputs for the cusom shipto dialog
 
1321 # Needed, if customer/vendor changed.
 
1322 sub build_shipto_inputs {
 
1325   my $content = $self->p->render('common/_ship_to_dialog',
 
1326                                  vc_obj      => $self->order->customervendor,
 
1327                                  cs_obj      => $self->order->custom_shipto,
 
1328                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1329                                  id_selector => '#order_shipto_id');
 
1331   div_tag($content, id => 'shipto_inputs');
 
1334 # render the info line for business
 
1336 # Needed, if customer/vendor changed.
 
1337 sub build_business_info_row
 
1339   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1342 # build the rows for displaying taxes
 
1344 # Called if amounts where recalculated and redisplayed.
 
1345 sub build_tax_rows {
 
1349   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1350     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1352   return $rows_as_html;
 
1356 sub render_price_dialog {
 
1357   my ($self, $record_item) = @_;
 
1359   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1363       'kivi.io.price_chooser_dialog',
 
1364       t8('Available Prices'),
 
1365       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1370 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1371 #     $self->js->show('#dialog_flash_error');
 
1380   return if !$::form->{id};
 
1382   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1384   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1385   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1386   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1388   return $self->order;
 
1391 # load or create a new order object
 
1393 # And assign changes from the form to this object.
 
1394 # If the order is loaded from db, check if items are deleted in the form,
 
1395 # remove them form the object and collect them for removing from db on saving.
 
1396 # Then create/update items from form (via make_item) and add them.
 
1400   # add_items adds items to an order with no items for saving, but they cannot
 
1401   # be retrieved via items until the order is saved. Adding empty items to new
 
1402   # order here solves this problem.
 
1404   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1405   $order ||= SL::DB::Order->new(orderitems  => [],
 
1406                                 quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
 
1407                                 currency_id => $::instance_conf->get_currency_id(),);
 
1409   my $cv_id_method = $self->cv . '_id';
 
1410   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1411     $order->$cv_id_method($::form->{$cv_id_method});
 
1412     setup_order_from_cv($order);
 
1415   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1416   my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
 
1418   $order->assign_attributes(%{$::form->{order}});
 
1420   $self->setup_custom_shipto_from_form($order, $::form);
 
1422   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1423     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1424     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1427   # remove deleted items
 
1428   $self->item_ids_to_delete([]);
 
1429   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1430     my $item = $order->orderitems->[$idx];
 
1431     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1432       splice @{$order->orderitems}, $idx, 1;
 
1433       push @{$self->item_ids_to_delete}, $item->id;
 
1439   foreach my $form_attr (@{$form_orderitems}) {
 
1440     my $item = make_item($order, $form_attr);
 
1441     $item->position($pos);
 
1445   $order->add_items(grep {!$_->id} @items);
 
1450 # create or update items from form
 
1452 # Make item objects from form values. For items already existing read from db.
 
1453 # Create a new item else. And assign attributes.
 
1455   my ($record, $attr) = @_;
 
1458   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1460   my $is_new = !$item;
 
1462   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1463   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1464   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1465   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1467   $item->assign_attributes(%$attr);
 
1468   $item->longdescription($item->part->notes)                     if $is_new && !defined $attr->{longdescription};
 
1469   $item->project_id($record->globalproject_id)                   if $is_new && !defined $attr->{project_id};
 
1470   $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
 
1477 # This is used to add one item
 
1479   my ($record, $attr) = @_;
 
1481   my $item = SL::DB::OrderItem->new;
 
1483   # Remove attributes where the user left or set the inputs empty.
 
1484   # So these attributes will be undefined and we can distinguish them
 
1485   # from zero later on.
 
1486   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1487     delete $attr->{$_} if $attr->{$_} eq '';
 
1490   $item->assign_attributes(%$attr);
 
1492   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1493   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1495   $item->unit($part->unit) if !$item->unit;
 
1498   if ( $part->is_assortment ) {
 
1499     # add assortment items with price 0, as the components carry the price
 
1500     $price_src = $price_source->price_from_source("");
 
1501     $price_src->price(0);
 
1502   } elsif (defined $item->sellprice) {
 
1503     $price_src = $price_source->price_from_source("");
 
1504     $price_src->price($item->sellprice);
 
1506     $price_src = $price_source->best_price
 
1507                ? $price_source->best_price
 
1508                : $price_source->price_from_source("");
 
1509     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
1510     $price_src->price(0) if !$price_source->best_price;
 
1514   if (defined $item->discount) {
 
1515     $discount_src = $price_source->discount_from_source("");
 
1516     $discount_src->discount($item->discount);
 
1518     $discount_src = $price_source->best_discount
 
1519                   ? $price_source->best_discount
 
1520                   : $price_source->discount_from_source("");
 
1521     $discount_src->discount(0) if !$price_source->best_discount;
 
1525   $new_attr{part}                   = $part;
 
1526   $new_attr{description}            = $part->description     if ! $item->description;
 
1527   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1528   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1529   $new_attr{sellprice}              = $price_src->price;
 
1530   $new_attr{discount}               = $discount_src->discount;
 
1531   $new_attr{active_price_source}    = $price_src;
 
1532   $new_attr{active_discount_source} = $discount_src;
 
1533   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1534   $new_attr{project_id}             = $record->globalproject_id;
 
1535   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1537   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1538   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1539   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1540   $new_attr{custom_variables} = [];
 
1542   $item->assign_attributes(%new_attr);
 
1547 sub setup_order_from_cv {
 
1550   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
 
1552   $order->intnotes($order->customervendor->notes);
 
1554   if ($order->is_sales) {
 
1555     $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1556     $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1557                         ? $order->customer->taxincluded_checked
 
1558                         : $::myconfig{taxincluded_checked});
 
1563 # setup custom shipto from form
 
1565 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1566 # with 'shiptocvar_'.
 
1567 # Mark it to be deleted if a shipto from master data is selected
 
1568 # (i.e. order has a shipto).
 
1569 # Else, update or create a new custom shipto. If the fields are empty, it
 
1570 # will not be saved on save.
 
1571 sub setup_custom_shipto_from_form {
 
1572   my ($self, $order, $form) = @_;
 
1574   if ($order->shipto) {
 
1575     $self->is_custom_shipto_to_delete(1);
 
1577     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1579     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1580     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1582     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1583     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1587 # recalculate prices and taxes
 
1589 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1593   my %pat = $self->order->calculate_prices_and_taxes();
 
1595   $self->{taxes} = [];
 
1596   foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
 
1597     my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
1599     push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
 
1600                                 netamount => $netamount,
 
1601                                 tax       => SL::DB::Tax->new(id => $tax_id)->load });
 
1603   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1606 # get data for saving, printing, ..., that is not changed in the form
 
1608 # Only cvars for now.
 
1609 sub get_unalterable_data {
 
1612   foreach my $item (@{ $self->order->items }) {
 
1613     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1614     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1615     foreach my $var (@{ $item->cvars_by_config }) {
 
1616       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1618     $item->parse_custom_variable_values;
 
1624 # And remove related files in the spool directory
 
1629   my $db     = $self->order->db;
 
1631   $db->with_transaction(
 
1633       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1634       $self->order->delete;
 
1635       my $spool = $::lx_office_conf{paths}->{spool};
 
1636       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1639   }) || push(@{$errors}, $db->error);
 
1646 # And delete items that are deleted in the form.
 
1651   my $db     = $self->order->db;
 
1653   $db->with_transaction(sub {
 
1654     # delete custom shipto if it is to be deleted or if it is empty
 
1655     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1656       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1657       $self->order->custom_shipto(undef);
 
1660     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1661     $self->order->save(cascade => 1);
 
1664     if ($::form->{converted_from_oe_id}) {
 
1665       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1666       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1667         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1668         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1669         $src->link_to_record($self->order);
 
1671       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1673         foreach (@{ $self->order->items_sorted }) {
 
1674           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1676           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1677                                   from_id    => $from_id,
 
1678                                   to_table   => 'orderitems',
 
1686   }) || push(@{$errors}, $db->error);
 
1691 sub workflow_sales_or_purchase_order {
 
1695   my $errors = $self->save();
 
1697   if (scalar @{ $errors }) {
 
1698     $self->js->flash('error', $_) foreach @{ $errors };
 
1699     return $self->js->render();
 
1702   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1703                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1704                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1705                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1708   # check for direct delivery
 
1709   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1711   if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
 
1712       && $::form->{use_shipto} && $self->order->shipto) {
 
1713     $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
 
1716   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1717   $self->{converted_from_oe_id} = delete $::form->{id};
 
1719   # set item ids to new fake id, to identify them as new items
 
1720   foreach my $item (@{$self->order->items_sorted}) {
 
1721     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1724   if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
 
1725     if ($::form->{use_shipto}) {
 
1726       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
1728       # remove any custom shipto if not wanted
 
1729       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1734   $::form->{type} = $destination_type;
 
1735   $self->type($self->init_type);
 
1736   $self->cv  ($self->init_cv);
 
1740   $self->get_unalterable_data();
 
1741   $self->pre_render();
 
1743   # trigger rendering values for second row/longdescription as hidden,
 
1744   # because they are loaded only on demand. So we need to keep the values
 
1746   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1747   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1751     title => $self->get_title_for('edit'),
 
1752     %{$self->{template_args}}
 
1760   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1761   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
1762   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1763   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1766   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1769   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1771   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1772   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1773   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1774   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1775   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1777   my $print_form = Form->new('');
 
1778   $print_form->{type}        = $self->type;
 
1779   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
1780   $print_form->{languages}   = SL::DB::Manager::Language->get_all_sorted;
 
1781   $print_form->{language_id} = $self->order->language_id;
 
1782   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
1783     form => $print_form,
 
1784     options => {dialog_name_prefix => 'print_options.',
 
1788                 no_opendocument    => 0,
 
1792   foreach my $item (@{$self->order->orderitems}) {
 
1793     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1794     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1795     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1798   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1799     # calculate shipped qtys here to prevent calling calculate for every item via the items method
 
1800     SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects;
 
1803   if ($self->order->number && $::instance_conf->get_webdav) {
 
1804     my $webdav = SL::Webdav->new(
 
1805       type     => $self->type,
 
1806       number   => $self->order->number,
 
1808     my @all_objects = $webdav->get_all_objects;
 
1809     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1811                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1815   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1817   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
1818                                                          edit_periodic_invoices_config calculate_qty kivi.Validator follow_up);
 
1819   $self->setup_edit_action_bar;
 
1822 sub setup_edit_action_bar {
 
1823   my ($self, %params) = @_;
 
1825   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
1826                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1827                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1829   for my $bar ($::request->layout->get('actionbar')) {
 
1834           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1835                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
1837           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'] ],
 
1841           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1842           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1843           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1845       ], # end of combobox "Save"
 
1852           t8('Save and Sales Order'),
 
1853           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
1854           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
1857           t8('Save and Purchase Order'),
 
1858           call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
 
1859           only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
1862           t8('Save and Delivery Order'),
 
1863           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
1864                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1866           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1867           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
 
1870           t8('Save and Invoice'),
 
1871           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1872           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1875           t8('Save and AP Transaction'),
 
1876           call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
1877           only_if   => (any { $self->type eq $_ } (purchase_order_type()))
 
1880       ], # end of combobox "Workflow"
 
1887           t8('Save and print'),
 
1888           call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ],
 
1891           t8('Save and E-mail'),
 
1892           call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts ],
 
1893           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1896           t8('Download attachments of all parts'),
 
1897           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1898           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1899           only_if  => $::instance_conf->get_doc_storage,
 
1901       ], # end of combobox "Export"
 
1905         call     => [ 'kivi.Order.delete_order' ],
 
1906         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1907         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1908         only_if  => $deletion_allowed,
 
1917           call     => [ 'kivi.Order.follow_up_window' ],
 
1918           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1919           only_if  => $::auth->assert('productivity'),
 
1921       ], # end of combobox "more"
 
1927   my ($order, $pdf_ref, $params) = @_;
 
1931   my $print_form = Form->new('');
 
1932   $print_form->{type}        = $order->type;
 
1933   $print_form->{formname}    = $params->{formname} || $order->type;
 
1934   $print_form->{format}      = $params->{format}   || 'pdf';
 
1935   $print_form->{media}       = $params->{media}    || 'file';
 
1936   $print_form->{groupitems}  = $params->{groupitems};
 
1937   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
1939   $order->language($params->{language});
 
1940   $order->flatten_to_form($print_form, format_amounts => 1);
 
1944   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
1945     $template_ext  = 'odt';
 
1946     $template_type = 'OpenDocument';
 
1949   # search for the template
 
1950   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
1951     name        => $print_form->{formname},
 
1952     extension   => $template_ext,
 
1953     email       => $print_form->{media} eq 'email',
 
1954     language    => $params->{language},
 
1955     printer_id  => $print_form->{printer_id},  # todo
 
1958   if (!defined $template_file) {
 
1959     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);
 
1962   return @errors if scalar @errors;
 
1964   $print_form->throw_on_error(sub {
 
1966       $print_form->prepare_for_printing;
 
1968       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
1969         format        => $print_form->{format},
 
1970         template_type => $template_type,
 
1971         template      => $template_file,
 
1972         variables     => $print_form,
 
1973         variable_content_types => {
 
1974           longdescription => 'html',
 
1975           partnotes       => 'html',
 
1980     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
1986 sub get_files_for_email_dialog {
 
1989   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
1991   return %files if !$::instance_conf->get_doc_storage;
 
1993   if ($self->order->id) {
 
1994     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
1995     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
1996     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2000     uniq_by { $_->{id} }
 
2002       +{ id         => $_->part->id,
 
2003          partnumber => $_->part->partnumber }
 
2004     } @{$self->order->items_sorted};
 
2006   foreach my $part (@parts) {
 
2007     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2008     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2011   foreach my $key (keys %files) {
 
2012     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2018 sub make_periodic_invoices_config_from_yaml {
 
2019   my ($yaml_config) = @_;
 
2021   return if !$yaml_config;
 
2022   my $attr = SL::YAML::Load($yaml_config);
 
2023   return if 'HASH' ne ref $attr;
 
2024   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
2028 sub get_periodic_invoices_status {
 
2029   my ($self, $config) = @_;
 
2031   return                      if $self->type ne sales_order_type();
 
2032   return t8('not configured') if !$config;
 
2034   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
2035              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
2036              :                                                     die "Cannot get status of periodic invoices config";
 
2038   return $active ? t8('active') : t8('inactive');
 
2042   my ($self, $action) = @_;
 
2044   return '' if none { lc($action)} qw(add edit);
 
2047   # $::locale->text("Add Sales Order");
 
2048   # $::locale->text("Add Purchase Order");
 
2049   # $::locale->text("Add Quotation");
 
2050   # $::locale->text("Add Request for Quotation");
 
2051   # $::locale->text("Edit Sales Order");
 
2052   # $::locale->text("Edit Purchase Order");
 
2053   # $::locale->text("Edit Quotation");
 
2054   # $::locale->text("Edit Request for Quotation");
 
2056   $action = ucfirst(lc($action));
 
2057   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
2058        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
2059        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
2060        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
2064 sub get_item_cvpartnumber {
 
2065   my ($self, $item) = @_;
 
2067   return if !$self->search_cvpartnumber;
 
2068   return if !$self->order->customervendor;
 
2070   if ($self->cv eq 'vendor') {
 
2071     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2072     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2073   } elsif ($self->cv eq 'customer') {
 
2074     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2075     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2079 sub sales_order_type {
 
2083 sub purchase_order_type {
 
2087 sub sales_quotation_type {
 
2091 sub request_quotation_type {
 
2092   'request_quotation';
 
2096   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
2097        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
2098        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
2099        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
2111 SL::Controller::Order - controller for orders
 
2115 This is a new form to enter orders, completely rewritten with the use
 
2116 of controller and java script techniques.
 
2118 The aim is to provide the user a better experience and a faster workflow. Also
 
2119 the code should be more readable, more reliable and better to maintain.
 
2127 One input row, so that input happens every time at the same place.
 
2131 Use of pickers where possible.
 
2135 Possibility to enter more than one item at once.
 
2139 Item list in a scrollable area, so that the workflow buttons stay at
 
2144 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2145 possible (by partnumber, description, qty, sellprice and discount for now).
 
2149 No C<update> is necessary. All entries and calculations are managed
 
2150 with ajax-calls and the page only reloads on C<save>.
 
2154 User can see changes immediately, because of the use of java script
 
2165 =item * C<SL/Controller/Order.pm>
 
2169 =item * C<template/webpages/order/form.html>
 
2173 =item * C<template/webpages/order/tabs/basic_data.html>
 
2175 Main tab for basic_data.
 
2177 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2178 reused from generic code.
 
2182 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
2184 For displaying information on business type
 
2186 =item * C<template/webpages/order/tabs/_item_input.html>
 
2188 The input line for items
 
2190 =item * C<template/webpages/order/tabs/_row.html>
 
2192 One row for already entered items
 
2194 =item * C<template/webpages/order/tabs/_tax_row.html>
 
2196 Displaying tax information
 
2198 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
 
2200 Dialog for entering more than one item at once
 
2202 =item * C<template/webpages/order/tabs/_multi_items_result.html>
 
2204 Results for the filter in the multi items dialog
 
2206 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
2208 Dialog for selecting price and discount sources
 
2212 =item * C<js/kivi.Order.js>
 
2214 java script functions
 
2224 =item * credit limit
 
2226 =item * more workflows (quotation, rfq)
 
2228 =item * price sources: little symbols showing better price / better discount
 
2230 =item * select units in input row?
 
2232 =item * check for direct delivery (workflow sales order -> purchase order)
 
2234 =item * language / part translations
 
2236 =item * access rights
 
2238 =item * display weights
 
2244 =item * optional client/user behaviour
 
2246 (transactions has to be set - department has to be set -
 
2247  force project if enabled in client config - transport cost reminder)
 
2251 =head1 KNOWN BUGS AND CAVEATS
 
2257 Customer discount is not displayed as a valid discount in price source popup
 
2258 (this might be a bug in price sources)
 
2260 (I cannot reproduce this (Bernd))
 
2264 No indication that <shift>-up/down expands/collapses second row.
 
2268 Inline creation of parts is not currently supported
 
2272 Table header is not sticky in the scrolling area.
 
2276 Sorting does not include C<position>, neither does reordering.
 
2278 This behavior was implemented intentionally. But we can discuss, which behavior
 
2279 should be implemented.
 
2283 C<show_multi_items_dialog> does not use the currently inserted string for
 
2288 =head1 To discuss / Nice to have
 
2294 How to expand/collapse second row. Now it can be done clicking the icon or
 
2299 Possibility to change longdescription in input row?
 
2303 Possibility to select PriceSources in input row?
 
2307 This controller uses a (changed) copy of the template for the PriceSource
 
2308 dialog. Maybe there could be used one code source.
 
2312 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2313 form. This is not only a problem here, but also in all parts using the PTC.
 
2314 There exists a ticket and a patch. This patch should be testet.
 
2318 An indicator, if the actual inputs are saved (like in an
 
2319 editor or on text processing application).
 
2323 A warning when leaving the page without saveing unchanged inputs.
 
2330 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>