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);
 
1076   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1083 sub init_valid_types {
 
1084   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1090   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1091     die "Not a valid type for order";
 
1094   $self->type($::form->{type});
 
1100   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1101          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1102          : die "Not a valid type for order";
 
1115 # model used to filter/display the parts in the multi-items dialog
 
1116 sub init_multi_items_models {
 
1117   SL::Controller::Helper::GetModels->new(
 
1118     controller     => $_[0],
 
1120     with_objects   => [ qw(unit_obj) ],
 
1121     disable_plugin => 'paginated',
 
1122     source         => $::form->{multi_items},
 
1128       partnumber  => t8('Partnumber'),
 
1129       description => t8('Description')}
 
1133 sub init_all_price_factors {
 
1134   SL::DB::Manager::PriceFactor->get_all;
 
1140   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1142   my $right   = $right_for->{ $self->type };
 
1143   $right    ||= 'DOES_NOT_EXIST';
 
1145   $::auth->assert($right);
 
1148 # build the selection box for contacts
 
1150 # Needed, if customer/vendor changed.
 
1151 sub build_contact_select {
 
1154   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1155     value_key  => 'cp_id',
 
1156     title_key  => 'full_name_dep',
 
1157     default    => $self->order->cp_id,
 
1159     style      => 'width: 300px',
 
1163 # build the selection box for shiptos
 
1165 # Needed, if customer/vendor changed.
 
1166 sub build_shipto_select {
 
1169   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
 
1170     value_key  => 'shipto_id',
 
1171     title_key  => 'displayable_id',
 
1172     default    => $self->order->shipto_id,
 
1174     style      => 'width: 300px',
 
1178 # render the info line for business
 
1180 # Needed, if customer/vendor changed.
 
1181 sub build_business_info_row
 
1183   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1186 # build the rows for displaying taxes
 
1188 # Called if amounts where recalculated and redisplayed.
 
1189 sub build_tax_rows {
 
1193   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1194     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1196   return $rows_as_html;
 
1200 sub render_price_dialog {
 
1201   my ($self, $record_item) = @_;
 
1203   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1207       'kivi.io.price_chooser_dialog',
 
1208       t8('Available Prices'),
 
1209       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1214 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1215 #     $self->js->show('#dialog_flash_error');
 
1224   return if !$::form->{id};
 
1226   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1229 # load or create a new order object
 
1231 # And assign changes from the form to this object.
 
1232 # If the order is loaded from db, check if items are deleted in the form,
 
1233 # remove them form the object and collect them for removing from db on saving.
 
1234 # Then create/update items from form (via make_item) and add them.
 
1238   # add_items adds items to an order with no items for saving, but they cannot
 
1239   # be retrieved via items until the order is saved. Adding empty items to new
 
1240   # order here solves this problem.
 
1242   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1243   $order ||= SL::DB::Order->new(orderitems => [],
 
1244                                 quotation  => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())));
 
1246   my $cv_id_method = $self->cv . '_id';
 
1247   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1248     $order->$cv_id_method($::form->{$cv_id_method});
 
1249     setup_order_from_cv($order);
 
1252   my $form_orderitems               = delete $::form->{order}->{orderitems};
 
1253   my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
 
1255   $order->assign_attributes(%{$::form->{order}});
 
1257   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1258     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1259     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1262   # remove deleted items
 
1263   $self->item_ids_to_delete([]);
 
1264   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1265     my $item = $order->orderitems->[$idx];
 
1266     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1267       splice @{$order->orderitems}, $idx, 1;
 
1268       push @{$self->item_ids_to_delete}, $item->id;
 
1274   foreach my $form_attr (@{$form_orderitems}) {
 
1275     my $item = make_item($order, $form_attr);
 
1276     $item->position($pos);
 
1280   $order->add_items(grep {!$_->id} @items);
 
1285 # create or update items from form
 
1287 # Make item objects from form values. For items already existing read from db.
 
1288 # Create a new item else. And assign attributes.
 
1290   my ($record, $attr) = @_;
 
1293   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1295   my $is_new = !$item;
 
1297   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1298   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1299   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1300   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1302   $item->assign_attributes(%$attr);
 
1303   $item->longdescription($item->part->notes)                     if $is_new && !defined $attr->{longdescription};
 
1304   $item->project_id($record->globalproject_id)                   if $is_new && !defined $attr->{project_id};
 
1305   $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
 
1312 # This is used to add one item
 
1314   my ($record, $attr) = @_;
 
1316   my $item = SL::DB::OrderItem->new;
 
1318   # Remove attributes where the user left or set the inputs empty.
 
1319   # So these attributes will be undefined and we can distinguish them
 
1320   # from zero later on.
 
1321   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1322     delete $attr->{$_} if $attr->{$_} eq '';
 
1325   $item->assign_attributes(%$attr);
 
1327   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1328   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1330   $item->unit($part->unit) if !$item->unit;
 
1333   if ( $part->is_assortment ) {
 
1334     # add assortment items with price 0, as the components carry the price
 
1335     $price_src = $price_source->price_from_source("");
 
1336     $price_src->price(0);
 
1337   } elsif (defined $item->sellprice) {
 
1338     $price_src = $price_source->price_from_source("");
 
1339     $price_src->price($item->sellprice);
 
1341     $price_src = $price_source->best_price
 
1342            ? $price_source->best_price
 
1343            : $price_source->price_from_source("");
 
1344     $price_src->price(0) if !$price_source->best_price;
 
1348   if (defined $item->discount) {
 
1349     $discount_src = $price_source->discount_from_source("");
 
1350     $discount_src->discount($item->discount);
 
1352     $discount_src = $price_source->best_discount
 
1353                   ? $price_source->best_discount
 
1354                   : $price_source->discount_from_source("");
 
1355     $discount_src->discount(0) if !$price_source->best_discount;
 
1359   $new_attr{part}                   = $part;
 
1360   $new_attr{description}            = $part->description     if ! $item->description;
 
1361   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1362   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1363   $new_attr{sellprice}              = $price_src->price;
 
1364   $new_attr{discount}               = $discount_src->discount;
 
1365   $new_attr{active_price_source}    = $price_src;
 
1366   $new_attr{active_discount_source} = $discount_src;
 
1367   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1368   $new_attr{project_id}             = $record->globalproject_id;
 
1369   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1371   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1372   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1373   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1374   $new_attr{custom_variables} = [];
 
1376   $item->assign_attributes(%new_attr);
 
1381 sub setup_order_from_cv {
 
1384   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id));
 
1386   $order->intnotes($order->customervendor->notes);
 
1388   if ($order->is_sales) {
 
1389     $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1390     $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1391                         ? $order->customer->taxincluded_checked
 
1392                         : $::myconfig{taxincluded_checked});
 
1397 # recalculate prices and taxes
 
1399 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1403   # bb: todo: currency later
 
1404   $self->order->currency_id($::instance_conf->get_currency_id());
 
1406   my %pat = $self->order->calculate_prices_and_taxes();
 
1407   $self->{taxes} = [];
 
1408   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
 
1409     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
 
1411     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
 
1412     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
 
1413                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
 
1417   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1420 # get data for saving, printing, ..., that is not changed in the form
 
1422 # Only cvars for now.
 
1423 sub get_unalterable_data {
 
1426   foreach my $item (@{ $self->order->items }) {
 
1427     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1428     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1429     foreach my $var (@{ $item->cvars_by_config }) {
 
1430       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1432     $item->parse_custom_variable_values;
 
1438 # And remove related files in the spool directory
 
1443   my $db     = $self->order->db;
 
1445   $db->with_transaction(
 
1447       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1448       $self->order->delete;
 
1449       my $spool = $::lx_office_conf{paths}->{spool};
 
1450       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1453   }) || push(@{$errors}, $db->error);
 
1460 # And delete items that are deleted in the form.
 
1465   my $db     = $self->order->db;
 
1467   $db->with_transaction(sub {
 
1468     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1469     $self->order->save(cascade => 1);
 
1472     if ($::form->{converted_from_oe_id}) {
 
1473       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1474       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1475         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1476         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1477         $src->link_to_record($self->order);
 
1479       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1481         foreach (@{ $self->order->items_sorted }) {
 
1482           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1484           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1485                                   from_id    => $from_id,
 
1486                                   to_table   => 'orderitems',
 
1494   }) || push(@{$errors}, $db->error);
 
1499 sub workflow_sales_or_purchase_order {
 
1503   my $errors = $self->save();
 
1505   if (scalar @{ $errors }) {
 
1506     $self->js->flash('error', $_) foreach @{ $errors };
 
1507     return $self->js->render();
 
1510   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1511                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1512                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1513                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1516   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1517   $self->{converted_from_oe_id} = delete $::form->{id};
 
1519   # set item ids to new fake id, to identify them as new items
 
1520   foreach my $item (@{$self->order->items_sorted}) {
 
1521     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1525   $::form->{type} = $destination_type;
 
1526   $self->type($self->init_type);
 
1527   $self->cv  ($self->init_cv);
 
1531   $self->get_unalterable_data();
 
1532   $self->pre_render();
 
1534   # trigger rendering values for second row/longdescription as hidden,
 
1535   # because they are loaded only on demand. So we need to keep the values
 
1537   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1538   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1542     title => $self->get_title_for('edit'),
 
1543     %{$self->{template_args}}
 
1551   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1552   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1553   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1556   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1559   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1561   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1562   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1563   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1564   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1565   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1567   my $print_form = Form->new('');
 
1568   $print_form->{type}      = $self->type;
 
1569   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
 
1570   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
 
1571   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
 
1572     form => $print_form,
 
1573     options => {dialog_name_prefix => 'print_options.',
 
1577                 no_opendocument    => 0,
 
1581   foreach my $item (@{$self->order->orderitems}) {
 
1582     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1583     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1584     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1587   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1588     # calculate shipped qtys here to prevent calling calculate for every item via the items method
 
1589     SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects;
 
1592   if ($self->order->number && $::instance_conf->get_webdav) {
 
1593     my $webdav = SL::Webdav->new(
 
1594       type     => $self->type,
 
1595       number   => $self->order->number,
 
1597     my @all_objects = $webdav->get_all_objects;
 
1598     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1600                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1604   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config calculate_qty);
 
1605   $self->setup_edit_action_bar;
 
1608 sub setup_edit_action_bar {
 
1609   my ($self, %params) = @_;
 
1611   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
1612                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1613                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1615   for my $bar ($::request->layout->get('actionbar')) {
 
1620           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1621                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
1623           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1627           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1628           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1629           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1631       ], # end of combobox "Save"
 
1638           t8('Save and Sales Order'),
 
1639           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
1640           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
1641           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1644           t8('Save and Purchase Order'),
 
1645           submit   => [ '#order_form', { action => "Order/purchase_order" } ],
 
1646           only_if  => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
1647           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1650           t8('Save and Delivery Order'),
 
1651           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
1652                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1654           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1655           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
 
1658           t8('Save and Invoice'),
 
1659           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1660           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1662       ], # end of combobox "Workflow"
 
1669           t8('Save and print'),
 
1670           call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ],
 
1673           t8('Save and E-mail'),
 
1674           call => [ 'kivi.Order.email', $::instance_conf->get_order_warn_duplicate_parts ],
 
1677           t8('Download attachments of all parts'),
 
1678           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1679           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1680           only_if  => $::instance_conf->get_doc_storage,
 
1682       ], # end of combobox "Export"
 
1686         call     => [ 'kivi.Order.delete_order' ],
 
1687         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1688         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1689         only_if  => $deletion_allowed,
 
1696   my ($order, $pdf_ref, $params) = @_;
 
1700   my $print_form = Form->new('');
 
1701   $print_form->{type}        = $order->type;
 
1702   $print_form->{formname}    = $params->{formname} || $order->type;
 
1703   $print_form->{format}      = $params->{format}   || 'pdf';
 
1704   $print_form->{media}       = $params->{media}    || 'file';
 
1705   $print_form->{groupitems}  = $params->{groupitems};
 
1706   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
1708   $order->language($params->{language});
 
1709   $order->flatten_to_form($print_form, format_amounts => 1);
 
1713   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
1714     $template_ext  = 'odt';
 
1715     $template_type = 'OpenDocument';
 
1718   # search for the template
 
1719   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
1720     name        => $print_form->{formname},
 
1721     extension   => $template_ext,
 
1722     email       => $print_form->{media} eq 'email',
 
1723     language    => $params->{language},
 
1724     printer_id  => $print_form->{printer_id},  # todo
 
1727   if (!defined $template_file) {
 
1728     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);
 
1731   return @errors if scalar @errors;
 
1733   $print_form->throw_on_error(sub {
 
1735       $print_form->prepare_for_printing;
 
1737       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
1738         format        => $print_form->{format},
 
1739         template_type => $template_type,
 
1740         template      => $template_file,
 
1741         variables     => $print_form,
 
1742         variable_content_types => {
 
1743           longdescription => 'html',
 
1744           partnotes       => 'html',
 
1749     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
1755 sub get_files_for_email_dialog {
 
1758   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
1760   return %files if !$::instance_conf->get_doc_storage;
 
1762   if ($self->order->id) {
 
1763     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
1764     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
1765     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
1769     uniq_by { $_->{id} }
 
1771       +{ id         => $_->part->id,
 
1772          partnumber => $_->part->partnumber }
 
1773     } @{$self->order->items_sorted};
 
1775   foreach my $part (@parts) {
 
1776     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
1777     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
1780   foreach my $key (keys %files) {
 
1781     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
1787 sub make_periodic_invoices_config_from_yaml {
 
1788   my ($yaml_config) = @_;
 
1790   return if !$yaml_config;
 
1791   my $attr = SL::YAML::Load($yaml_config);
 
1792   return if 'HASH' ne ref $attr;
 
1793   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
1797 sub get_periodic_invoices_status {
 
1798   my ($self, $config) = @_;
 
1800   return                      if $self->type ne sales_order_type();
 
1801   return t8('not configured') if !$config;
 
1803   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
1804              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
1805              :                                                     die "Cannot get status of periodic invoices config";
 
1807   return $active ? t8('active') : t8('inactive');
 
1811   my ($self, $action) = @_;
 
1813   return '' if none { lc($action)} qw(add edit);
 
1816   # $::locale->text("Add Sales Order");
 
1817   # $::locale->text("Add Purchase Order");
 
1818   # $::locale->text("Add Quotation");
 
1819   # $::locale->text("Add Request for Quotation");
 
1820   # $::locale->text("Edit Sales Order");
 
1821   # $::locale->text("Edit Purchase Order");
 
1822   # $::locale->text("Edit Quotation");
 
1823   # $::locale->text("Edit Request for Quotation");
 
1825   $action = ucfirst(lc($action));
 
1826   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
1827        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
1828        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
1829        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
1833 sub sales_order_type {
 
1837 sub purchase_order_type {
 
1841 sub sales_quotation_type {
 
1845 sub request_quotation_type {
 
1846   'request_quotation';
 
1850   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
1851        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
1852        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
1853        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
1865 SL::Controller::Order - controller for orders
 
1869 This is a new form to enter orders, completely rewritten with the use
 
1870 of controller and java script techniques.
 
1872 The aim is to provide the user a better expirience and a faster flow
 
1873 of work. Also the code should be more readable, more reliable and
 
1882 One input row, so that input happens every time at the same place.
 
1886 Use of pickers where possible.
 
1890 Possibility to enter more than one item at once.
 
1894 Item list in a scrollable area, so that the workflow buttons stay at
 
1899 Reordering item rows with drag and drop is possible. Sorting item rows is
 
1900 possible (by partnumber, description, qty, sellprice and discount for now).
 
1904 No C<update> is necessary. All entries and calculations are managed
 
1905 with ajax-calls and the page does only reload on C<save>.
 
1909 User can see changes immediately, because of the use of java script
 
1920 =item * C<SL/Controller/Order.pm>
 
1924 =item * C<template/webpages/order/form.html>
 
1928 =item * C<template/webpages/order/tabs/basic_data.html>
 
1930 Main tab for basic_data.
 
1932 This is the only tab here for now. "linked records" and "webdav" tabs are
 
1933 reused from generic code.
 
1937 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
1939 For displaying information on business type
 
1941 =item * C<template/webpages/order/tabs/_item_input.html>
 
1943 The input line for items
 
1945 =item * C<template/webpages/order/tabs/_row.html>
 
1947 One row for already entered items
 
1949 =item * C<template/webpages/order/tabs/_tax_row.html>
 
1951 Displaying tax information
 
1953 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
 
1955 Dialog for entering more than one item at once
 
1957 =item * C<template/webpages/order/tabs/_multi_items_result.html>
 
1959 Results for the filter in the multi items dialog
 
1961 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
1963 Dialog for selecting price and discount sources
 
1967 =item * C<js/kivi.Order.js>
 
1969 java script functions
 
1981 =item * credit limit
 
1983 =item * more workflows (quotation, rfq)
 
1985 =item * price sources: little symbols showing better price / better discount
 
1987 =item * select units in input row?
 
1989 =item * custom shipto address
 
1991 =item * check for direct delivery (workflow sales order -> purchase order)
 
1993 =item * language / part translations
 
1995 =item * access rights
 
1997 =item * display weights
 
2003 =item * optional client/user behaviour
 
2005 (transactions has to be set - department has to be set -
 
2006  force project if enabled in client config - transport cost reminder)
 
2010 =head1 KNOWN BUGS AND CAVEATS
 
2016 Customer discount is not displayed as a valid discount in price source popup
 
2017 (this might be a bug in price sources)
 
2019 (I cannot reproduce this (Bernd))
 
2023 No indication that <shift>-up/down expands/collapses second row.
 
2027 Inline creation of parts is not currently supported
 
2031 Table header is not sticky in the scrolling area.
 
2035 Sorting does not include C<position>, neither does reordering.
 
2037 This behavior was implemented intentionally. But we can discuss, which behavior
 
2038 should be implemented.
 
2042 C<show_multi_items_dialog> does not use the currently inserted string for
 
2047 The language selected in print or email dialog is not saved when the order is saved.
 
2051 =head1 To discuss / Nice to have
 
2057 How to expand/collapse second row. Now it can be done clicking the icon or
 
2062 Possibility to change longdescription in input row?
 
2066 Possibility to select PriceSources in input row?
 
2070 This controller uses a (changed) copy of the template for the PriceSource
 
2071 dialog. Maybe there could be used one code source.
 
2075 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2076 form. This is not only a problem here, but also in all parts using the PTC.
 
2077 There exists a ticket and a patch. This patch should be testet.
 
2081 An indicator, if the actual inputs are saved (like in an
 
2082 editor or on text processing application).
 
2086 A warning when leaving the page without saveing unchanged inputs.
 
2093 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>