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);
 
   8 use SL::Locale::String qw(t8);
 
   9 use SL::SessionFile::Random;
 
  13 use SL::Util qw(trim);
 
  19 use SL::DB::PartsGroup;
 
  22 use SL::DB::RecordLink;
 
  24 use SL::Helper::CreatePDF qw(:all);
 
  25 use SL::Helper::PrintOptions;
 
  26 use SL::Helper::ShippedQty;
 
  27 use SL::Helper::UserPreferences::PositionsScrollbar;
 
  29 use SL::Controller::Helper::GetModels;
 
  31 use List::Util qw(first);
 
  32 use List::UtilsBy qw(sort_by uniq_by);
 
  33 use List::MoreUtils qw(any none pairwise first_index);
 
  34 use English qw(-no_match_vars);
 
  39 use Rose::Object::MakeMethods::Generic
 
  41  scalar => [ qw(item_ids_to_delete) ],
 
  42  'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
 
  47 __PACKAGE__->run_before('check_auth');
 
  49 __PACKAGE__->run_before('recalc',
 
  50                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
 
  52 __PACKAGE__->run_before('get_unalterable_data',
 
  53                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
 
  63   $self->order->transdate(DateTime->now_local());
 
  64   my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
 
  65                    $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
 
  66   $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
 
  72     title => $self->get_title_for('add'),
 
  73     %{$self->{template_args}}
 
  77 # edit an existing order
 
  85     # this is to edit an order from an unsaved order object
 
  87     # set item ids to new fake id, to identify them as new items
 
  88     foreach my $item (@{$self->order->items_sorted}) {
 
  89       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
  91     # trigger rendering values for second row/longdescription as hidden,
 
  92     # because they are loaded only on demand. So we need to keep the values
 
  94     $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
  95     $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
 102     title => $self->get_title_for('edit'),
 
 103     %{$self->{template_args}}
 
 107 # edit a collective order (consisting of one or more existing orders)
 
 108 sub action_edit_collective {
 
 112   my @multi_ids = map {
 
 113     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 114   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 116   # fall back to add if no ids are given
 
 117   if (scalar @multi_ids == 0) {
 
 122   # fall back to save as new if only one id is given
 
 123   if (scalar @multi_ids == 1) {
 
 124     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 125     $self->action_save_as_new();
 
 129   # make new order from given orders
 
 130   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 131   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 132   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 134   $self->action_edit();
 
 141   my $errors = $self->delete();
 
 143   if (scalar @{ $errors }) {
 
 144     $self->js->flash('error', $_) foreach @{ $errors };
 
 145     return $self->js->render();
 
 148   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 149            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 150            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 151            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 153   flash_later('info', $text);
 
 155   my @redirect_params = (
 
 160   $self->redirect_to(@redirect_params);
 
 167   my $errors = $self->save();
 
 169   if (scalar @{ $errors }) {
 
 170     $self->js->flash('error', $_) foreach @{ $errors };
 
 171     return $self->js->render();
 
 174   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 175            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 176            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 177            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 179   flash_later('info', $text);
 
 181   my @redirect_params = (
 
 184     id     => $self->order->id,
 
 187   $self->redirect_to(@redirect_params);
 
 190 # save the order as new document an open it for edit
 
 191 sub action_save_as_new {
 
 194   my $order = $self->order;
 
 197     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 198     return $self->js->render();
 
 201   # load order from db to check if values changed
 
 202   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 205   # Lets assign a new number if the user hasn't changed the previous one.
 
 206   # If it has been changed manually then use it as-is.
 
 207   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 209                         : trim($order->number);
 
 211   # Clear transdate unless changed
 
 212   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 213                         ? DateTime->today_local
 
 216   # Set new reqdate unless changed
 
 217   if ($order->reqdate == $saved_order->reqdate) {
 
 218     my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
 
 219                      $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
 
 220     $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 222     $new_attrs{reqdate} = $order->reqdate;
 
 226   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 228   # Create new record from current one
 
 229   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 231   # no linked records on save as new
 
 232   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 235   $self->action_save();
 
 240 # This is called if "print" is pressed in the print dialog.
 
 241 # If PDF creation was requested and succeeded, the pdf is stored in a session
 
 242 # file and the filename is stored as session value with an unique key. A
 
 243 # javascript function with this key is then called. This function calls the
 
 244 # download action below (action_download_pdf), which offers the file for
 
 249   my $errors = $self->save();
 
 251   if (scalar @{ $errors }) {
 
 252     $self->js->flash('error', $_) foreach @{ $errors };
 
 253     return $self->js->render();
 
 256   $self->js_reset_order_and_item_ids_after_save;
 
 258   my $format      = $::form->{print_options}->{format};
 
 259   my $media       = $::form->{print_options}->{media};
 
 260   my $formname    = $::form->{print_options}->{formname};
 
 261   my $copies      = $::form->{print_options}->{copies};
 
 262   my $groupitems  = $::form->{print_options}->{groupitems};
 
 264   # only pdf and opendocument by now
 
 265   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
 
 266     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 269   # only screen or printer by now
 
 270   if (none { $media eq $_ } qw(screen printer)) {
 
 271     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 275   $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 277   # create a form for generate_attachment_filename
 
 278   my $form   = Form->new;
 
 279   $form->{$self->nr_key()}  = $self->order->number;
 
 280   $form->{type}             = $self->type;
 
 281   $form->{format}           = $format;
 
 282   $form->{formname}         = $formname;
 
 283   $form->{language}         = '_' . $language->template_code if $language;
 
 284   my $pdf_filename          = $form->generate_attachment_filename();
 
 287   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
 
 288                                                    formname   => $formname,
 
 289                                                    language   => $language,
 
 290                                                    groupitems => $groupitems });
 
 291   if (scalar @errors) {
 
 292     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 295   if ($media eq 'screen') {
 
 297     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 298     $sfile->fh->print($pdf);
 
 301     my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 302     $::auth->set_session_value("Order::print-${key}" => $sfile->file_name);
 
 305     ->run('kivi.Order.download_pdf', $pdf_filename, $key)
 
 306     ->flash('info', t8('The PDF has been created'));
 
 308   } elsif ($media eq 'printer') {
 
 310     my $printer_id = $::form->{print_options}->{printer_id};
 
 311     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 316     $self->js->flash('info', t8('The PDF has been printed'));
 
 319   # copy file to webdav folder
 
 320   if ($self->order->number && $::instance_conf->get_webdav_documents) {
 
 321     my $webdav = SL::Webdav->new(
 
 323       number   => $self->order->number,
 
 325     my $webdav_file = SL::Webdav::File->new(
 
 327       filename => $pdf_filename,
 
 330       $webdav_file->store(data => \$pdf);
 
 333       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
 
 336   if ($self->order->number && $::instance_conf->get_doc_storage) {
 
 338       SL::File->save(object_id     => $self->order->id,
 
 339                      object_type   => $self->type,
 
 340                      mime_type     => 'application/pdf',
 
 342                      file_type     => 'document',
 
 343                      file_name     => $pdf_filename,
 
 344                      file_contents => $pdf);
 
 347       $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
 
 353 # offer pdf for download
 
 355 # It needs to get the key for the session value to get the pdf file.
 
 356 sub action_download_pdf {
 
 359   my $key = $::form->{key};
 
 360   my $tmp_filename = $::auth->get_session_value("Order::print-${key}");
 
 361   return $self->send_file(
 
 363     type => 'application/pdf',
 
 364     name => $::form->{pdf_filename},
 
 368 # open the email dialog
 
 369 sub action_show_email_dialog {
 
 372   my $cv_method = $self->cv;
 
 374   if (!$self->order->$cv_method) {
 
 375     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'))
 
 380   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 381   $email_form->{to} ||= $self->order->$cv_method->email;
 
 382   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 383   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 384   # Todo: get addresses from shipto, if any
 
 386   my $form = Form->new;
 
 387   $form->{$self->nr_key()}  = $self->order->number;
 
 388   $form->{formname}         = $self->type;
 
 389   $form->{type}             = $self->type;
 
 390   $form->{language}         = 'de';
 
 391   $form->{format}           = 'pdf';
 
 393   $email_form->{subject}             = $form->generate_email_subject();
 
 394   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 395   $email_form->{message}             = $form->generate_email_body();
 
 396   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 398   my %files = $self->get_files_for_email_dialog();
 
 399   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 400                                   email_form  => $email_form,
 
 401                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
 
 403                                   is_customer => $self->cv eq 'customer',
 
 407       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 414 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 415 sub action_send_email {
 
 418   my $errors = $self->save();
 
 420   if (scalar @{ $errors }) {
 
 421     $self->js->run('kivi.Order.close_email_dialog');
 
 422     $self->js->flash('error', $_) foreach @{ $errors };
 
 423     return $self->js->render();
 
 426   $self->js_reset_order_and_item_ids_after_save;
 
 428   my $email_form  = delete $::form->{email_form};
 
 429   my %field_names = (to => 'email');
 
 431   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 433   # for Form::cleanup which may be called in Form::send_email
 
 434   $::form->{cwd}    = getcwd();
 
 435   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 437   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 438   $::form->{media}  = 'email';
 
 440   if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
 
 442     $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 445     my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
 
 446                                                     format     => $::form->{print_options}->{format},
 
 447                                                     formname   => $::form->{print_options}->{formname},
 
 448                                                     language   => $language,
 
 449                                                     groupitems => $::form->{print_options}->{groupitems}});
 
 450     if (scalar @errors) {
 
 451       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
 
 454     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 455     $sfile->fh->print($pdf);
 
 458     $::form->{tmpfile} = $sfile->file_name;
 
 459     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 462   $::form->send_email(\%::myconfig, 'pdf');
 
 465   my $intnotes = $self->order->intnotes;
 
 466   $intnotes   .= "\n\n" if $self->order->intnotes;
 
 467   $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 468   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 469   $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 470   $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 471   $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 472   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 473   $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
 475   $self->order->update_attributes(intnotes => $intnotes);
 
 478       ->val('#order_intnotes', $intnotes)
 
 479       ->run('kivi.Order.close_email_dialog')
 
 480       ->flash('info', t8('The email has been sent.'))
 
 484 # open the periodic invoices config dialog
 
 486 # If there are values in the form (i.e. dialog was opened before),
 
 487 # then use this values. Create new ones, else.
 
 488 sub action_show_periodic_invoices_config_dialog {
 
 491   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 492   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 493   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 494                                                    order_value_periodicity => 'p', # = same as periodicity
 
 495                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 496                                                    extend_automatically_by => 12,
 
 498                                                    email_subject           => GenericTranslations->get(
 
 499                                                                                 language_id      => $::form->{language_id},
 
 500                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 501                                                    email_body              => GenericTranslations->get(
 
 502                                                                                 language_id      => $::form->{language_id},
 
 503                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 505   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 506   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 508   $::form->get_lists(printers => "ALL_PRINTERS",
 
 509                      charts   => { key       => 'ALL_CHARTS',
 
 510                                    transdate => 'current_date' });
 
 512   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 514   if ($::form->{customer_id}) {
 
 515     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 516     $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail;
 
 519   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 521                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 522                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 527 # assign the values of the periodic invoices config dialog
 
 528 # as yaml in the hidden tag and set the status.
 
 529 sub action_assign_periodic_invoices_config {
 
 532   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 534   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 535                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 536                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 537                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 538                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 539                  start_date_as_date         => $::form->{start_date_as_date},
 
 540                  end_date_as_date           => $::form->{end_date_as_date},
 
 541                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 542                  print                      => $::form->{print}      ? 1                         : 0,
 
 543                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 544                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 545                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 546                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 547                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 548                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 549                  email_recipient_address    => $::form->{email_recipient_address},
 
 550                  email_sender               => $::form->{email_sender},
 
 551                  email_subject              => $::form->{email_subject},
 
 552                  email_body                 => $::form->{email_body},
 
 555   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 557   my $status = $self->get_periodic_invoices_status($config);
 
 560     ->remove('#order_periodic_invoices_config')
 
 561     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 562     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 563     ->html('#periodic_invoices_status', $status)
 
 564     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 568 sub action_get_has_active_periodic_invoices {
 
 571   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 572   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 574   my $has_active_periodic_invoices =
 
 575        $self->type eq sales_order_type()
 
 578     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 579     && $config->get_previous_billed_period_start_date;
 
 581   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 584 # save the order and redirect to the frontend subroutine for a new
 
 586 sub action_save_and_delivery_order {
 
 589   my $errors = $self->save();
 
 591   if (scalar @{ $errors }) {
 
 592     $self->js->flash('error', $_) foreach @{ $errors };
 
 593     return $self->js->render();
 
 596   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 597            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 598            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 599            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 601   flash_later('info', $text);
 
 603   my @redirect_params = (
 
 604     controller => 'oe.pl',
 
 605     action     => 'oe_delivery_order_from_order',
 
 606     id         => $self->order->id,
 
 609   $self->redirect_to(@redirect_params);
 
 612 # save the order and redirect to the frontend subroutine for a new
 
 614 sub action_save_and_invoice {
 
 617   my $errors = $self->save();
 
 619   if (scalar @{ $errors }) {
 
 620     $self->js->flash('error', $_) foreach @{ $errors };
 
 621     return $self->js->render();
 
 624   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 625            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 626            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 627            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 629   flash_later('info', $text);
 
 631   my @redirect_params = (
 
 632     controller => 'oe.pl',
 
 633     action     => 'oe_invoice_from_order',
 
 634     id         => $self->order->id,
 
 637   $self->redirect_to(@redirect_params);
 
 640 # workflow from sales quotation to sales order
 
 641 sub action_sales_order {
 
 642   $_[0]->workflow_sales_or_purchase_order();
 
 645 # workflow from rfq to purchase order
 
 646 sub action_purchase_order {
 
 647   $_[0]->workflow_sales_or_purchase_order();
 
 650 # set form elements in respect to a changed customer or vendor
 
 652 # This action is called on an change of the customer/vendor picker.
 
 653 sub action_customer_vendor_changed {
 
 656   setup_order_from_cv($self->order);
 
 659   my $cv_method = $self->cv;
 
 661   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 662     $self->js->show('#cp_row');
 
 664     $self->js->hide('#cp_row');
 
 667   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 668     $self->js->show('#shipto_row');
 
 670     $self->js->hide('#shipto_row');
 
 673   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 676     ->replaceWith('#order_cp_id',            $self->build_contact_select)
 
 677     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
 
 678     ->replaceWith('#business_info_row',      $self->build_business_info_row)
 
 679     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
 
 680     ->val(        '#order_taxincluded',      $self->order->taxincluded)
 
 681     ->val(        '#order_payment_id',       $self->order->payment_id)
 
 682     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
 
 683     ->val(        '#order_intnotes',         $self->order->intnotes)
 
 684     ->val(        '#language_id',            $self->order->$cv_method->language_id)
 
 685     ->focus(      '#order_' . $self->cv . '_id');
 
 687   $self->js_redisplay_amounts_and_taxes;
 
 691 # open the dialog for customer/vendor details
 
 692 sub action_show_customer_vendor_details_dialog {
 
 695   my $is_customer = 'customer' eq $::form->{vc};
 
 698     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 700     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 703   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 704   $details{discount_as_percent} = $cv->discount_as_percent;
 
 705   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 706   $details{business}            = $cv->business->description      if $cv->business;
 
 707   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 708   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 709   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 710   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 712   foreach my $entry (@{ $cv->shipto }) {
 
 713     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 715   foreach my $entry (@{ $cv->contacts }) {
 
 716     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 719   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 720                 is_customer => $is_customer,
 
 725 # called if a unit in an existing item row is changed
 
 726 sub action_unit_changed {
 
 729   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 730   my $item = $self->order->items_sorted->[$idx];
 
 732   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 733   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 738     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 739   $self->js_redisplay_line_values;
 
 740   $self->js_redisplay_amounts_and_taxes;
 
 744 # add an item row for a new item entered in the input row
 
 745 sub action_add_item {
 
 748   my $form_attr = $::form->{add_item};
 
 750   return unless $form_attr->{parts_id};
 
 752   my $item = new_item($self->order, $form_attr);
 
 754   $self->order->add_items($item);
 
 758   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 759   my $row_as_html = $self->p->render('order/tabs/_row',
 
 763                                      ALL_PRICE_FACTORS => $self->all_price_factors
 
 767     ->append('#row_table_id', $row_as_html);
 
 769   if ( $item->part->is_assortment ) {
 
 770     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 771     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 772       my $attr = { parts_id => $assortment_item->parts_id,
 
 773                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 774                    unit     => $assortment_item->unit,
 
 775                    description => $assortment_item->part->description,
 
 777       my $item = new_item($self->order, $attr);
 
 779       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 780       $item->discount(1) unless $assortment_item->charge;
 
 782       $self->order->add_items( $item );
 
 784       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 785       my $row_as_html = $self->p->render('order/tabs/_row',
 
 789                                          ALL_PRICE_FACTORS => $self->all_price_factors
 
 792         ->append('#row_table_id', $row_as_html);
 
 797     ->val('.add_item_input', '')
 
 798     ->run('kivi.Order.init_row_handlers')
 
 799     ->run('kivi.Order.row_table_scroll_down')
 
 800     ->run('kivi.Order.renumber_positions')
 
 801     ->focus('#add_item_parts_id_name');
 
 803   $self->js_redisplay_amounts_and_taxes;
 
 807 # open the dialog for entering multiple items at once
 
 808 sub action_show_multi_items_dialog {
 
 809   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
 
 810                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
 
 813 # update the filter results in the multi item dialog
 
 814 sub action_multi_items_update_result {
 
 817   $::form->{multi_items}->{filter}->{obsolete} = 0;
 
 819   my $count = $_[0]->multi_items_models->count;
 
 822     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
 
 823     $_[0]->render($text, { layout => 0 });
 
 824   } elsif ($count > $max_count) {
 
 825     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
 
 826     $_[0]->render($text, { layout => 0 });
 
 828     my $multi_items = $_[0]->multi_items_models->get;
 
 829     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
 
 830                   multi_items => $multi_items);
 
 834 # add item rows for multiple items at once
 
 835 sub action_add_multi_items {
 
 838   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
 
 839   return $self->js->render() unless scalar @form_attr;
 
 842   foreach my $attr (@form_attr) {
 
 843     my $item = new_item($self->order, $attr);
 
 845     if ( $item->part->is_assortment ) {
 
 846       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 847         my $attr = { parts_id => $assortment_item->parts_id,
 
 848                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 849                      unit     => $assortment_item->unit,
 
 850                      description => $assortment_item->part->description,
 
 852         my $item = new_item($self->order, $attr);
 
 854         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 855         $item->discount(1) unless $assortment_item->charge;
 
 860   $self->order->add_items(@items);
 
 864   foreach my $item (@items) {
 
 865     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 866     my $row_as_html = $self->p->render('order/tabs/_row',
 
 870                                        ALL_PRICE_FACTORS => $self->all_price_factors
 
 873     $self->js->append('#row_table_id', $row_as_html);
 
 877     ->run('kivi.Order.close_multi_items_dialog')
 
 878     ->run('kivi.Order.init_row_handlers')
 
 879     ->run('kivi.Order.row_table_scroll_down')
 
 880     ->run('kivi.Order.renumber_positions')
 
 881     ->focus('#add_item_parts_id_name');
 
 883   $self->js_redisplay_amounts_and_taxes;
 
 887 # recalculate all linetotals, amounts and taxes and redisplay them
 
 888 sub action_recalc_amounts_and_taxes {
 
 893   $self->js_redisplay_line_values;
 
 894   $self->js_redisplay_amounts_and_taxes;
 
 898 # redisplay item rows if they are sorted by an attribute
 
 899 sub action_reorder_items {
 
 903     partnumber  => sub { $_[0]->part->partnumber },
 
 904     description => sub { $_[0]->description },
 
 905     qty         => sub { $_[0]->qty },
 
 906     sellprice   => sub { $_[0]->sellprice },
 
 907     discount    => sub { $_[0]->discount },
 
 910   my $method = $sort_keys{$::form->{order_by}};
 
 911   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
 912   if ($::form->{sort_dir}) {
 
 913     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 914       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
 916       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 919     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 920       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
 922       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 926     ->run('kivi.Order.redisplay_items', \@to_sort)
 
 930 # show the popup to choose a price/discount source
 
 931 sub action_price_popup {
 
 934   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 935   my $item = $self->order->items_sorted->[$idx];
 
 937   $self->render_price_dialog($item);
 
 940 # get the longdescription for an item if the dialog to enter/change the
 
 941 # longdescription was opened and the longdescription is empty
 
 943 # If this item is new, get the longdescription from Part.
 
 944 # Otherwise get it from OrderItem.
 
 945 sub action_get_item_longdescription {
 
 948   if ($::form->{item_id}) {
 
 949     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
 
 950   } elsif ($::form->{parts_id}) {
 
 951     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
 
 953   $_[0]->render(\ $longdescription, { type => 'text' });
 
 956 # load the second row for one or more items
 
 958 # This action gets the html code for all items second rows by rendering a template for
 
 959 # the second row and sets the html code via client js.
 
 960 sub action_load_second_rows {
 
 963   $self->recalc() if $self->order->is_sales; # for margin calculation
 
 965   foreach my $item_id (@{ $::form->{item_ids} }) {
 
 966     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
 967     my $item = $self->order->items_sorted->[$idx];
 
 969     $self->js_load_second_row($item, $item_id, 0);
 
 972   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
 977 sub js_load_second_row {
 
 978   my ($self, $item, $item_id, $do_parse) = @_;
 
 981     # Parse values from form (they are formated while rendering (template)).
 
 982     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
 983     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
 984     foreach my $var (@{ $item->cvars_by_config }) {
 
 985       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
 987     $item->parse_custom_variable_values;
 
 990   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
 993     ->html('#second_row_' . $item_id, $row_as_html)
 
 994     ->data('#second_row_' . $item_id, 'loaded', 1);
 
 997 sub js_redisplay_line_values {
 
1000   my $is_sales = $self->order->is_sales;
 
1002   # sales orders with margins
 
1007        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1008        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1009        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1010       ]} @{ $self->order->items_sorted };
 
1014        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1015       ]} @{ $self->order->items_sorted };
 
1019     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1022 sub js_redisplay_amounts_and_taxes {
 
1025   if (scalar @{ $self->{taxes} }) {
 
1026     $self->js->show('#taxincluded_row_id');
 
1028     $self->js->hide('#taxincluded_row_id');
 
1031   if ($self->order->taxincluded) {
 
1032     $self->js->hide('#subtotal_row_id');
 
1034     $self->js->show('#subtotal_row_id');
 
1037   if ($self->order->is_sales) {
 
1038     my $is_neg = $self->order->marge_total < 0;
 
1040       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1041       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1042       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1043       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1044       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1045       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1046       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1047       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1051     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1052     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1053     ->remove('.tax_row')
 
1054     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1057 sub js_reset_order_and_item_ids_after_save {
 
1061     ->val('#id', $self->order->id)
 
1062     ->val('#converted_from_oe_id', '')
 
1063     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1066   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1067     next if !$self->order->items_sorted->[$idx]->id;
 
1068     next if $form_item_id !~ m{^new};
 
1070       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1071       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1072       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1075   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1082 sub init_valid_types {
 
1083   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1089   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1090     die "Not a valid type for order";
 
1093   $self->type($::form->{type});
 
1099   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1100          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1101          : die "Not a valid type for order";
 
1114 # model used to filter/display the parts in the multi-items dialog
 
1115 sub init_multi_items_models {
 
1116   SL::Controller::Helper::GetModels->new(
 
1117     controller     => $_[0],
 
1119     with_objects   => [ qw(unit_obj) ],
 
1120     disable_plugin => 'paginated',
 
1121     source         => $::form->{multi_items},
 
1127       partnumber  => t8('Partnumber'),
 
1128       description => t8('Description')}
 
1132 sub init_all_price_factors {
 
1133   SL::DB::Manager::PriceFactor->get_all;
 
1139   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1141   my $right   = $right_for->{ $self->type };
 
1142   $right    ||= 'DOES_NOT_EXIST';
 
1144   $::auth->assert($right);
 
1147 # build the selection box for contacts
 
1149 # Needed, if customer/vendor changed.
 
1150 sub build_contact_select {
 
1153   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1154     value_key  => 'cp_id',
 
1155     title_key  => 'full_name_dep',
 
1156     default    => $self->order->cp_id,
 
1158     style      => 'width: 300px',
 
1162 # build the selection box for shiptos
 
1164 # Needed, if customer/vendor changed.
 
1165 sub build_shipto_select {
 
1168   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
 
1169     value_key  => 'shipto_id',
 
1170     title_key  => 'displayable_id',
 
1171     default    => $self->order->shipto_id,
 
1173     style      => 'width: 300px',
 
1177 # render the info line for business
 
1179 # Needed, if customer/vendor changed.
 
1180 sub build_business_info_row
 
1182   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1185 # build the rows for displaying taxes
 
1187 # Called if amounts where recalculated and redisplayed.
 
1188 sub build_tax_rows {
 
1192   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1193     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1195   return $rows_as_html;
 
1199 sub render_price_dialog {
 
1200   my ($self, $record_item) = @_;
 
1202   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1206       'kivi.io.price_chooser_dialog',
 
1207       t8('Available Prices'),
 
1208       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1213 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1214 #     $self->js->show('#dialog_flash_error');
 
1223   return if !$::form->{id};
 
1225   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1228 # load or create a new order object
 
1230 # And assign changes from the form to this object.
 
1231 # If the order is loaded from db, check if items are deleted in the form,
 
1232 # remove them form the object and collect them for removing from db on saving.
 
1233 # Then create/update items from form (via make_item) and add them.
 
1237   # add_items adds items to an order with no items for saving, but they cannot
 
1238   # be retrieved via items until the order is saved. Adding empty items to new
 
1239   # order here solves this problem.
 
1241   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1242   $order ||= SL::DB::Order->new(orderitems => [],
 
1243                                 quotation  => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())));
 
1245   my $cv_id_method = $self->cv . '_id';
 
1246   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1247     $order->$cv_id_method($::form->{$cv_id_method});
 
1248     setup_order_from_cv($order);
 
1251   my $form_orderitems               = delete $::form->{order}->{orderitems};
 
1252   my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
 
1254   $order->assign_attributes(%{$::form->{order}});
 
1256   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1257     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1258     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1261   # remove deleted items
 
1262   $self->item_ids_to_delete([]);
 
1263   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1264     my $item = $order->orderitems->[$idx];
 
1265     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1266       splice @{$order->orderitems}, $idx, 1;
 
1267       push @{$self->item_ids_to_delete}, $item->id;
 
1273   foreach my $form_attr (@{$form_orderitems}) {
 
1274     my $item = make_item($order, $form_attr);
 
1275     $item->position($pos);
 
1279   $order->add_items(grep {!$_->id} @items);
 
1284 # create or update items from form
 
1286 # Make item objects from form values. For items already existing read from db.
 
1287 # Create a new item else. And assign attributes.
 
1289   my ($record, $attr) = @_;
 
1292   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1294   my $is_new = !$item;
 
1296   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1297   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1298   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1299   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1301   $item->assign_attributes(%$attr);
 
1302   $item->longdescription($item->part->notes)                     if $is_new && !defined $attr->{longdescription};
 
1303   $item->project_id($record->globalproject_id)                   if $is_new && !defined $attr->{project_id};
 
1304   $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
 
1311 # This is used to add one item
 
1313   my ($record, $attr) = @_;
 
1315   my $item = SL::DB::OrderItem->new;
 
1317   # Remove attributes where the user left or set the inputs empty.
 
1318   # So these attributes will be undefined and we can distinguish them
 
1319   # from zero later on.
 
1320   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1321     delete $attr->{$_} if $attr->{$_} eq '';
 
1324   $item->assign_attributes(%$attr);
 
1326   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1327   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1329   $item->unit($part->unit) if !$item->unit;
 
1332   if ( $part->is_assortment ) {
 
1333     # add assortment items with price 0, as the components carry the price
 
1334     $price_src = $price_source->price_from_source("");
 
1335     $price_src->price(0);
 
1336   } elsif (defined $item->sellprice) {
 
1337     $price_src = $price_source->price_from_source("");
 
1338     $price_src->price($item->sellprice);
 
1340     $price_src = $price_source->best_price
 
1341            ? $price_source->best_price
 
1342            : $price_source->price_from_source("");
 
1343     $price_src->price(0) if !$price_source->best_price;
 
1347   if (defined $item->discount) {
 
1348     $discount_src = $price_source->discount_from_source("");
 
1349     $discount_src->discount($item->discount);
 
1351     $discount_src = $price_source->best_discount
 
1352                   ? $price_source->best_discount
 
1353                   : $price_source->discount_from_source("");
 
1354     $discount_src->discount(0) if !$price_source->best_discount;
 
1358   $new_attr{part}                   = $part;
 
1359   $new_attr{description}            = $part->description     if ! $item->description;
 
1360   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1361   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1362   $new_attr{sellprice}              = $price_src->price;
 
1363   $new_attr{discount}               = $discount_src->discount;
 
1364   $new_attr{active_price_source}    = $price_src;
 
1365   $new_attr{active_discount_source} = $discount_src;
 
1366   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1367   $new_attr{project_id}             = $record->globalproject_id;
 
1368   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1370   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1371   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1372   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1373   $new_attr{custom_variables} = [];
 
1375   $item->assign_attributes(%new_attr);
 
1380 sub setup_order_from_cv {
 
1383   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id));
 
1385   $order->intnotes($order->customervendor->notes);
 
1387   if ($order->is_sales) {
 
1388     $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1389     $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1390                         ? $order->customer->taxincluded_checked
 
1391                         : $::myconfig{taxincluded_checked});
 
1396 # recalculate prices and taxes
 
1398 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1402   # bb: todo: currency later
 
1403   $self->order->currency_id($::instance_conf->get_currency_id());
 
1405   my %pat = $self->order->calculate_prices_and_taxes();
 
1406   $self->{taxes} = [];
 
1407   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
 
1408     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
 
1410     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
 
1411     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
 
1412                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
 
1416   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1419 # get data for saving, printing, ..., that is not changed in the form
 
1421 # Only cvars for now.
 
1422 sub get_unalterable_data {
 
1425   foreach my $item (@{ $self->order->items }) {
 
1426     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1427     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1428     foreach my $var (@{ $item->cvars_by_config }) {
 
1429       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1431     $item->parse_custom_variable_values;
 
1437 # And remove related files in the spool directory
 
1442   my $db     = $self->order->db;
 
1444   $db->with_transaction(
 
1446       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1447       $self->order->delete;
 
1448       my $spool = $::lx_office_conf{paths}->{spool};
 
1449       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1452   }) || push(@{$errors}, $db->error);
 
1459 # And delete items that are deleted in the form.
 
1464   my $db     = $self->order->db;
 
1466   $db->with_transaction(sub {
 
1467     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1468     $self->order->save(cascade => 1);
 
1471     if ($::form->{converted_from_oe_id}) {
 
1472       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1473       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1474         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1475         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1476         $src->link_to_record($self->order);
 
1478       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1480         foreach (@{ $self->order->items_sorted }) {
 
1481           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1483           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1484                                   from_id    => $from_id,
 
1485                                   to_table   => 'orderitems',
 
1493   }) || push(@{$errors}, $db->error);
 
1498 sub workflow_sales_or_purchase_order {
 
1502   my $errors = $self->save();
 
1504   if (scalar @{ $errors }) {
 
1505     $self->js->flash('error', $_) foreach @{ $errors };
 
1506     return $self->js->render();
 
1509   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1510                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1511                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1512                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1515   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1516   $self->{converted_from_oe_id} = delete $::form->{id};
 
1518   # set item ids to new fake id, to identify them as new items
 
1519   foreach my $item (@{$self->order->items_sorted}) {
 
1520     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1524   $::form->{type} = $destination_type;
 
1525   $self->type($self->init_type);
 
1526   $self->cv  ($self->init_cv);
 
1530   $self->get_unalterable_data();
 
1531   $self->pre_render();
 
1533   # trigger rendering values for second row/longdescription as hidden,
 
1534   # because they are loaded only on demand. So we need to keep the values
 
1536   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1537   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1541     title => $self->get_title_for('edit'),
 
1542     %{$self->{template_args}}
 
1550   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1551   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1552   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1555   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1558   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1560   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1561   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1562   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1563   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1564   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1566   my $print_form = Form->new('');
 
1567   $print_form->{type}      = $self->type;
 
1568   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
 
1569   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
 
1570   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
 
1571     form => $print_form,
 
1572     options => {dialog_name_prefix => 'print_options.',
 
1576                 no_opendocument    => 0,
 
1580   foreach my $item (@{$self->order->orderitems}) {
 
1581     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1582     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1583     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1586   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1587     # calculate shipped qtys here to prevent calling calculate for every item via the items method
 
1588     SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects;
 
1591   if ($self->order->number && $::instance_conf->get_webdav) {
 
1592     my $webdav = SL::Webdav->new(
 
1593       type     => $self->type,
 
1594       number   => $self->order->number,
 
1596     my @all_objects = $webdav->get_all_objects;
 
1597     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1599                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1603   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config calculate_qty);
 
1604   $self->setup_edit_action_bar;
 
1607 sub setup_edit_action_bar {
 
1608   my ($self, %params) = @_;
 
1610   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
1611                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1612                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1614   for my $bar ($::request->layout->get('actionbar')) {
 
1619           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1620                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
1622           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1626           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1627           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1628           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1630       ], # end of combobox "Save"
 
1637           t8('Save and Sales Order'),
 
1638           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
1639           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
1640           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1643           t8('Save and Purchase Order'),
 
1644           submit   => [ '#order_form', { action => "Order/purchase_order" } ],
 
1645           only_if  => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
1646           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1649           t8('Save and Delivery Order'),
 
1650           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
1651                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1653           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1654           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
 
1657           t8('Save and Invoice'),
 
1658           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1659           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1661       ], # end of combobox "Workflow"
 
1668           t8('Save and print'),
 
1669           call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ],
 
1672           t8('Save and E-mail'),
 
1673           call => [ 'kivi.Order.email', $::instance_conf->get_order_warn_duplicate_parts ],
 
1676           t8('Download attachments of all parts'),
 
1677           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1678           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1679           only_if  => $::instance_conf->get_doc_storage,
 
1681       ], # end of combobox "Export"
 
1685         call     => [ 'kivi.Order.delete_order' ],
 
1686         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1687         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1688         only_if  => $deletion_allowed,
 
1695   my ($order, $pdf_ref, $params) = @_;
 
1699   my $print_form = Form->new('');
 
1700   $print_form->{type}        = $order->type;
 
1701   $print_form->{formname}    = $params->{formname} || $order->type;
 
1702   $print_form->{format}      = $params->{format}   || 'pdf';
 
1703   $print_form->{media}       = $params->{media}    || 'file';
 
1704   $print_form->{groupitems}  = $params->{groupitems};
 
1705   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
1707   $order->language($params->{language});
 
1708   $order->flatten_to_form($print_form, format_amounts => 1);
 
1712   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
1713     $template_ext  = 'odt';
 
1714     $template_type = 'OpenDocument';
 
1717   # search for the template
 
1718   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
1719     name        => $print_form->{formname},
 
1720     extension   => $template_ext,
 
1721     email       => $print_form->{media} eq 'email',
 
1722     language    => $params->{language},
 
1723     printer_id  => $print_form->{printer_id},  # todo
 
1726   if (!defined $template_file) {
 
1727     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);
 
1730   return @errors if scalar @errors;
 
1732   $print_form->throw_on_error(sub {
 
1734       $print_form->prepare_for_printing;
 
1736       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
1737         format        => $print_form->{format},
 
1738         template_type => $template_type,
 
1739         template      => $template_file,
 
1740         variables     => $print_form,
 
1741         variable_content_types => {
 
1742           longdescription => 'html',
 
1743           partnotes       => 'html',
 
1748     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
1754 sub get_files_for_email_dialog {
 
1757   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
1759   return %files if !$::instance_conf->get_doc_storage;
 
1761   if ($self->order->id) {
 
1762     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
1763     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
1764     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
1768     uniq_by { $_->{id} }
 
1770       +{ id         => $_->part->id,
 
1771          partnumber => $_->part->partnumber }
 
1772     } @{$self->order->items_sorted};
 
1774   foreach my $part (@parts) {
 
1775     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
1776     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
1779   foreach my $key (keys %files) {
 
1780     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
1786 sub make_periodic_invoices_config_from_yaml {
 
1787   my ($yaml_config) = @_;
 
1789   return if !$yaml_config;
 
1790   my $attr = SL::YAML::Load($yaml_config);
 
1791   return if 'HASH' ne ref $attr;
 
1792   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
1796 sub get_periodic_invoices_status {
 
1797   my ($self, $config) = @_;
 
1799   return                      if $self->type ne sales_order_type();
 
1800   return t8('not configured') if !$config;
 
1802   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
1803              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
1804              :                                                     die "Cannot get status of periodic invoices config";
 
1806   return $active ? t8('active') : t8('inactive');
 
1810   my ($self, $action) = @_;
 
1812   return '' if none { lc($action)} qw(add edit);
 
1815   # $::locale->text("Add Sales Order");
 
1816   # $::locale->text("Add Purchase Order");
 
1817   # $::locale->text("Add Quotation");
 
1818   # $::locale->text("Add Request for Quotation");
 
1819   # $::locale->text("Edit Sales Order");
 
1820   # $::locale->text("Edit Purchase Order");
 
1821   # $::locale->text("Edit Quotation");
 
1822   # $::locale->text("Edit Request for Quotation");
 
1824   $action = ucfirst(lc($action));
 
1825   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
1826        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
1827        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
1828        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
1832 sub sales_order_type {
 
1836 sub purchase_order_type {
 
1840 sub sales_quotation_type {
 
1844 sub request_quotation_type {
 
1845   'request_quotation';
 
1849   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
1850        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
1851        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
1852        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
1864 SL::Controller::Order - controller for orders
 
1868 This is a new form to enter orders, completely rewritten with the use
 
1869 of controller and java script techniques.
 
1871 The aim is to provide the user a better expirience and a faster flow
 
1872 of work. Also the code should be more readable, more reliable and
 
1881 One input row, so that input happens every time at the same place.
 
1885 Use of pickers where possible.
 
1889 Possibility to enter more than one item at once.
 
1893 Item list in a scrollable area, so that the workflow buttons stay at
 
1898 Reordering item rows with drag and drop is possible. Sorting item rows is
 
1899 possible (by partnumber, description, qty, sellprice and discount for now).
 
1903 No C<update> is necessary. All entries and calculations are managed
 
1904 with ajax-calls and the page does only reload on C<save>.
 
1908 User can see changes immediately, because of the use of java script
 
1919 =item * C<SL/Controller/Order.pm>
 
1923 =item * C<template/webpages/order/form.html>
 
1927 =item * C<template/webpages/order/tabs/basic_data.html>
 
1929 Main tab for basic_data.
 
1931 This is the only tab here for now. "linked records" and "webdav" tabs are
 
1932 reused from generic code.
 
1936 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
1938 For displaying information on business type
 
1940 =item * C<template/webpages/order/tabs/_item_input.html>
 
1942 The input line for items
 
1944 =item * C<template/webpages/order/tabs/_row.html>
 
1946 One row for already entered items
 
1948 =item * C<template/webpages/order/tabs/_tax_row.html>
 
1950 Displaying tax information
 
1952 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
 
1954 Dialog for entering more than one item at once
 
1956 =item * C<template/webpages/order/tabs/_multi_items_result.html>
 
1958 Results for the filter in the multi items dialog
 
1960 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
1962 Dialog for selecting price and discount sources
 
1966 =item * C<js/kivi.Order.js>
 
1968 java script functions
 
1980 =item * credit limit
 
1982 =item * more workflows (quotation, rfq)
 
1984 =item * price sources: little symbols showing better price / better discount
 
1986 =item * select units in input row?
 
1988 =item * custom shipto address
 
1990 =item * check for direct delivery (workflow sales order -> purchase order)
 
1992 =item * language / part translations
 
1994 =item * access rights
 
1996 =item * display weights
 
2002 =item * optional client/user behaviour
 
2004 (transactions has to be set - department has to be set -
 
2005  force project if enabled in client config - transport cost reminder)
 
2009 =head1 KNOWN BUGS AND CAVEATS
 
2015 Customer discount is not displayed as a valid discount in price source popup
 
2016 (this might be a bug in price sources)
 
2018 (I cannot reproduce this (Bernd))
 
2022 No indication that <shift>-up/down expands/collapses second row.
 
2026 Inline creation of parts is not currently supported
 
2030 Table header is not sticky in the scrolling area.
 
2034 Sorting does not include C<position>, neither does reordering.
 
2036 This behavior was implemented intentionally. But we can discuss, which behavior
 
2037 should be implemented.
 
2041 C<show_multi_items_dialog> does not use the currently inserted string for
 
2046 The language selected in print or email dialog is not saved when the order is saved.
 
2050 =head1 To discuss / Nice to have
 
2056 How to expand/collapse second row. Now it can be done clicking the icon or
 
2061 Possibility to change longdescription in input row?
 
2065 Possibility to select PriceSources in input row?
 
2069 This controller uses a (changed) copy of the template for the PriceSource
 
2070 dialog. Maybe there could be used one code source.
 
2074 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2075 form. This is not only a problem here, but also in all parts using the PTC.
 
2076 There exists a ticket and a patch. This patch should be testet.
 
2080 An indicator, if the actual inputs are saved (like in an
 
2081 editor or on text processing application).
 
2085 A warning when leaving the page without saveing unchanged inputs.
 
2092 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>