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;
 
  28 use SL::Controller::Helper::GetModels;
 
  30 use List::Util qw(first);
 
  31 use List::UtilsBy qw(sort_by uniq_by);
 
  32 use List::MoreUtils qw(any none pairwise first_index);
 
  33 use English qw(-no_match_vars);
 
  38 use Rose::Object::MakeMethods::Generic
 
  40  scalar => [ qw(item_ids_to_delete) ],
 
  41  'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
 
  46 __PACKAGE__->run_before('check_auth');
 
  48 __PACKAGE__->run_before('recalc',
 
  49                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
 
  51 __PACKAGE__->run_before('get_unalterable_data',
 
  52                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
 
  62   $self->order->transdate(DateTime->now_local());
 
  63   my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
 
  64                    $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
 
  65   $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
 
  71     title => $self->get_title_for('add'),
 
  72     %{$self->{template_args}}
 
  76 # edit an existing order
 
  84     # this is to edit an order from an unsaved order object
 
  86     # set item ids to new fake id, to identify them as new items
 
  87     foreach my $item (@{$self->order->items_sorted}) {
 
  88       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
  90     # trigger rendering values for second row/longdescription as hidden,
 
  91     # because they are loaded only on demand. So we need to keep the values
 
  93     $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
  94     $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
 101     title => $self->get_title_for('edit'),
 
 102     %{$self->{template_args}}
 
 106 # edit a collective order (consisting of one or more existing orders)
 
 107 sub action_edit_collective {
 
 111   my @multi_ids = map {
 
 112     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 113   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 115   # fall back to add if no ids are given
 
 116   if (scalar @multi_ids == 0) {
 
 121   # fall back to save as new if only one id is given
 
 122   if (scalar @multi_ids == 1) {
 
 123     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 124     $self->action_save_as_new();
 
 128   # make new order from given orders
 
 129   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 130   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 131   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 133   $self->action_edit();
 
 140   my $errors = $self->delete();
 
 142   if (scalar @{ $errors }) {
 
 143     $self->js->flash('error', $_) foreach @{ $errors };
 
 144     return $self->js->render();
 
 147   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 148            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 149            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 150            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 152   flash_later('info', $text);
 
 154   my @redirect_params = (
 
 159   $self->redirect_to(@redirect_params);
 
 166   my $errors = $self->save();
 
 168   if (scalar @{ $errors }) {
 
 169     $self->js->flash('error', $_) foreach @{ $errors };
 
 170     return $self->js->render();
 
 173   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 174            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 175            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 176            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 178   flash_later('info', $text);
 
 180   my @redirect_params = (
 
 183     id     => $self->order->id,
 
 186   $self->redirect_to(@redirect_params);
 
 189 # save the order as new document an open it for edit
 
 190 sub action_save_as_new {
 
 193   my $order = $self->order;
 
 196     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 197     return $self->js->render();
 
 200   # load order from db to check if values changed
 
 201   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 204   # Lets assign a new number if the user hasn't changed the previous one.
 
 205   # If it has been changed manually then use it as-is.
 
 206   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 208                         : trim($order->number);
 
 210   # Clear transdate unless changed
 
 211   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 212                         ? DateTime->today_local
 
 215   # Set new reqdate unless changed
 
 216   if ($order->reqdate == $saved_order->reqdate) {
 
 217     my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
 
 218                      $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
 
 219     $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 221     $new_attrs{reqdate} = $order->reqdate;
 
 225   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 227   # Create new record from current one
 
 228   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 230   # no linked records on save as new
 
 231   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 234   $self->action_save();
 
 239 # This is called if "print" is pressed in the print dialog.
 
 240 # If PDF creation was requested and succeeded, the pdf is stored in a session
 
 241 # file and the filename is stored as session value with an unique key. A
 
 242 # javascript function with this key is then called. This function calls the
 
 243 # download action below (action_download_pdf), which offers the file for
 
 248   my $errors = $self->save();
 
 250   if (scalar @{ $errors }) {
 
 251     $self->js->flash('error', $_) foreach @{ $errors };
 
 252     return $self->js->render();
 
 255   $self->js_reset_order_and_item_ids_after_save;
 
 257   my $format      = $::form->{print_options}->{format};
 
 258   my $media       = $::form->{print_options}->{media};
 
 259   my $formname    = $::form->{print_options}->{formname};
 
 260   my $copies      = $::form->{print_options}->{copies};
 
 261   my $groupitems  = $::form->{print_options}->{groupitems};
 
 263   # only pdf and opendocument by now
 
 264   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
 
 265     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 268   # only screen or printer by now
 
 269   if (none { $media eq $_ } qw(screen printer)) {
 
 270     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 274   $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 276   # create a form for generate_attachment_filename
 
 277   my $form   = Form->new;
 
 278   $form->{$self->nr_key()}  = $self->order->number;
 
 279   $form->{type}             = $self->type;
 
 280   $form->{format}           = $format;
 
 281   $form->{formname}         = $formname;
 
 282   $form->{language}         = '_' . $language->template_code if $language;
 
 283   my $pdf_filename          = $form->generate_attachment_filename();
 
 286   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
 
 287                                                    formname   => $formname,
 
 288                                                    language   => $language,
 
 289                                                    groupitems => $groupitems });
 
 290   if (scalar @errors) {
 
 291     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 294   if ($media eq 'screen') {
 
 296     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 297     $sfile->fh->print($pdf);
 
 300     my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 301     $::auth->set_session_value("Order::print-${key}" => $sfile->file_name);
 
 304     ->run('kivi.Order.download_pdf', $pdf_filename, $key)
 
 305     ->flash('info', t8('The PDF has been created'));
 
 307   } elsif ($media eq 'printer') {
 
 309     my $printer_id = $::form->{print_options}->{printer_id};
 
 310     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 315     $self->js->flash('info', t8('The PDF has been printed'));
 
 318   # copy file to webdav folder
 
 319   if ($self->order->number && $::instance_conf->get_webdav_documents) {
 
 320     my $webdav = SL::Webdav->new(
 
 322       number   => $self->order->number,
 
 324     my $webdav_file = SL::Webdav::File->new(
 
 326       filename => $pdf_filename,
 
 329       $webdav_file->store(data => \$pdf);
 
 332       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
 
 335   if ($self->order->number && $::instance_conf->get_doc_storage) {
 
 337       SL::File->save(object_id     => $self->order->id,
 
 338                      object_type   => $self->type,
 
 339                      mime_type     => 'application/pdf',
 
 341                      file_type     => 'document',
 
 342                      file_name     => $pdf_filename,
 
 343                      file_contents => $pdf);
 
 346       $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
 
 352 # offer pdf for download
 
 354 # It needs to get the key for the session value to get the pdf file.
 
 355 sub action_download_pdf {
 
 358   my $key = $::form->{key};
 
 359   my $tmp_filename = $::auth->get_session_value("Order::print-${key}");
 
 360   return $self->send_file(
 
 362     type => 'application/pdf',
 
 363     name => $::form->{pdf_filename},
 
 367 # open the email dialog
 
 368 sub action_show_email_dialog {
 
 371   my $cv_method = $self->cv;
 
 373   if (!$self->order->$cv_method) {
 
 374     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'))
 
 379   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 380   $email_form->{to} ||= $self->order->$cv_method->email;
 
 381   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 382   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 383   # Todo: get addresses from shipto, if any
 
 385   my $form = Form->new;
 
 386   $form->{$self->nr_key()}  = $self->order->number;
 
 387   $form->{formname}         = $self->type;
 
 388   $form->{type}             = $self->type;
 
 389   $form->{language}         = 'de';
 
 390   $form->{format}           = 'pdf';
 
 392   $email_form->{subject}             = $form->generate_email_subject();
 
 393   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 394   $email_form->{message}             = $form->generate_email_body();
 
 395   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 397   my %files = $self->get_files_for_email_dialog();
 
 398   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 399                                   email_form  => $email_form,
 
 400                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
 
 402                                   is_customer => $self->cv eq 'customer',
 
 406       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 413 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 414 sub action_send_email {
 
 417   my $errors = $self->save();
 
 419   if (scalar @{ $errors }) {
 
 420     $self->js->run('kivi.Order.close_email_dialog');
 
 421     $self->js->flash('error', $_) foreach @{ $errors };
 
 422     return $self->js->render();
 
 425   $self->js_reset_order_and_item_ids_after_save;
 
 427   my $email_form  = delete $::form->{email_form};
 
 428   my %field_names = (to => 'email');
 
 430   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 432   # for Form::cleanup which may be called in Form::send_email
 
 433   $::form->{cwd}    = getcwd();
 
 434   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 436   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 437   $::form->{media}  = 'email';
 
 439   if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
 
 441     $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 444     my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
 
 445                                                     format     => $::form->{print_options}->{format},
 
 446                                                     formname   => $::form->{print_options}->{formname},
 
 447                                                     language   => $language,
 
 448                                                     groupitems => $::form->{print_options}->{groupitems}});
 
 449     if (scalar @errors) {
 
 450       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
 
 453     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 454     $sfile->fh->print($pdf);
 
 457     $::form->{tmpfile} = $sfile->file_name;
 
 458     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 461   $::form->send_email(\%::myconfig, 'pdf');
 
 464   my $intnotes = $self->order->intnotes;
 
 465   $intnotes   .= "\n\n" if $self->order->intnotes;
 
 466   $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 467   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 468   $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 469   $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 470   $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 471   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 472   $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
 474   $self->order->update_attributes(intnotes => $intnotes);
 
 477       ->val('#order_intnotes', $intnotes)
 
 478       ->run('kivi.Order.close_email_dialog')
 
 479       ->flash('info', t8('The email has been sent.'))
 
 483 # open the periodic invoices config dialog
 
 485 # If there are values in the form (i.e. dialog was opened before),
 
 486 # then use this values. Create new ones, else.
 
 487 sub action_show_periodic_invoices_config_dialog {
 
 490   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 491   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 492   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 493                                                    order_value_periodicity => 'p', # = same as periodicity
 
 494                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 495                                                    extend_automatically_by => 12,
 
 497                                                    email_subject           => GenericTranslations->get(
 
 498                                                                                 language_id      => $::form->{language_id},
 
 499                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 500                                                    email_body              => GenericTranslations->get(
 
 501                                                                                 language_id      => $::form->{language_id},
 
 502                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 504   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 505   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 507   $::form->get_lists(printers => "ALL_PRINTERS",
 
 508                      charts   => { key       => 'ALL_CHARTS',
 
 509                                    transdate => 'current_date' });
 
 511   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 513   if ($::form->{customer_id}) {
 
 514     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 515     $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail;
 
 518   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 520                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 521                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 526 # assign the values of the periodic invoices config dialog
 
 527 # as yaml in the hidden tag and set the status.
 
 528 sub action_assign_periodic_invoices_config {
 
 531   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 533   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 534                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 535                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 536                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 537                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 538                  start_date_as_date         => $::form->{start_date_as_date},
 
 539                  end_date_as_date           => $::form->{end_date_as_date},
 
 540                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 541                  print                      => $::form->{print}      ? 1                         : 0,
 
 542                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 543                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 544                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 545                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 546                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 547                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 548                  email_recipient_address    => $::form->{email_recipient_address},
 
 549                  email_sender               => $::form->{email_sender},
 
 550                  email_subject              => $::form->{email_subject},
 
 551                  email_body                 => $::form->{email_body},
 
 554   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 556   my $status = $self->get_periodic_invoices_status($config);
 
 559     ->remove('#order_periodic_invoices_config')
 
 560     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 561     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 562     ->html('#periodic_invoices_status', $status)
 
 563     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 567 sub action_get_has_active_periodic_invoices {
 
 570   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 571   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 573   my $has_active_periodic_invoices =
 
 574        $self->type eq sales_order_type()
 
 577     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 578     && $config->get_previous_billed_period_start_date;
 
 580   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 583 # save the order and redirect to the frontend subroutine for a new
 
 585 sub action_save_and_delivery_order {
 
 588   my $errors = $self->save();
 
 590   if (scalar @{ $errors }) {
 
 591     $self->js->flash('error', $_) foreach @{ $errors };
 
 592     return $self->js->render();
 
 595   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 596            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 597            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 598            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 600   flash_later('info', $text);
 
 602   my @redirect_params = (
 
 603     controller => 'oe.pl',
 
 604     action     => 'oe_delivery_order_from_order',
 
 605     id         => $self->order->id,
 
 608   $self->redirect_to(@redirect_params);
 
 611 # save the order and redirect to the frontend subroutine for a new
 
 613 sub action_save_and_invoice {
 
 616   my $errors = $self->save();
 
 618   if (scalar @{ $errors }) {
 
 619     $self->js->flash('error', $_) foreach @{ $errors };
 
 620     return $self->js->render();
 
 623   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 624            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 625            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 626            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 628   flash_later('info', $text);
 
 630   my @redirect_params = (
 
 631     controller => 'oe.pl',
 
 632     action     => 'oe_invoice_from_order',
 
 633     id         => $self->order->id,
 
 636   $self->redirect_to(@redirect_params);
 
 639 # workflow from sales quotation to sales order
 
 640 sub action_sales_order {
 
 641   $_[0]->workflow_sales_or_purchase_order();
 
 644 # workflow from rfq to purchase order
 
 645 sub action_purchase_order {
 
 646   $_[0]->workflow_sales_or_purchase_order();
 
 649 # set form elements in respect to a changed customer or vendor
 
 651 # This action is called on an change of the customer/vendor picker.
 
 652 sub action_customer_vendor_changed {
 
 655   setup_order_from_cv($self->order);
 
 658   my $cv_method = $self->cv;
 
 660   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 661     $self->js->show('#cp_row');
 
 663     $self->js->hide('#cp_row');
 
 666   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 667     $self->js->show('#shipto_row');
 
 669     $self->js->hide('#shipto_row');
 
 672   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 675     ->replaceWith('#order_cp_id',            $self->build_contact_select)
 
 676     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
 
 677     ->replaceWith('#business_info_row',      $self->build_business_info_row)
 
 678     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
 
 679     ->val(        '#order_taxincluded',      $self->order->taxincluded)
 
 680     ->val(        '#order_payment_id',       $self->order->payment_id)
 
 681     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
 
 682     ->val(        '#order_intnotes',         $self->order->intnotes)
 
 683     ->val(        '#language_id',            $self->order->$cv_method->language_id)
 
 684     ->focus(      '#order_' . $self->cv . '_id');
 
 686   $self->js_redisplay_amounts_and_taxes;
 
 690 # open the dialog for customer/vendor details
 
 691 sub action_show_customer_vendor_details_dialog {
 
 694   my $is_customer = 'customer' eq $::form->{vc};
 
 697     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 699     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 702   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 703   $details{discount_as_percent} = $cv->discount_as_percent;
 
 704   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 705   $details{business}            = $cv->business->description      if $cv->business;
 
 706   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 707   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 708   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 709   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 711   foreach my $entry (@{ $cv->shipto }) {
 
 712     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 714   foreach my $entry (@{ $cv->contacts }) {
 
 715     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 718   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 719                 is_customer => $is_customer,
 
 724 # called if a unit in an existing item row is changed
 
 725 sub action_unit_changed {
 
 728   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 729   my $item = $self->order->items_sorted->[$idx];
 
 731   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 732   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 737     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 738   $self->js_redisplay_line_values;
 
 739   $self->js_redisplay_amounts_and_taxes;
 
 743 # add an item row for a new item entered in the input row
 
 744 sub action_add_item {
 
 747   my $form_attr = $::form->{add_item};
 
 749   return unless $form_attr->{parts_id};
 
 751   my $item = new_item($self->order, $form_attr);
 
 753   $self->order->add_items($item);
 
 757   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 758   my $row_as_html = $self->p->render('order/tabs/_row',
 
 762                                      ALL_PRICE_FACTORS => $self->all_price_factors
 
 766     ->append('#row_table_id', $row_as_html);
 
 768   if ( $item->part->is_assortment ) {
 
 769     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 770     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 771       my $attr = { parts_id => $assortment_item->parts_id,
 
 772                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 773                    unit     => $assortment_item->unit,
 
 774                    description => $assortment_item->part->description,
 
 776       my $item = new_item($self->order, $attr);
 
 778       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 779       $item->discount(1) unless $assortment_item->charge;
 
 781       $self->order->add_items( $item );
 
 783       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 784       my $row_as_html = $self->p->render('order/tabs/_row',
 
 788                                          ALL_PRICE_FACTORS => $self->all_price_factors
 
 791         ->append('#row_table_id', $row_as_html);
 
 796     ->val('.add_item_input', '')
 
 797     ->run('kivi.Order.init_row_handlers')
 
 798     ->run('kivi.Order.row_table_scroll_down')
 
 799     ->run('kivi.Order.renumber_positions')
 
 800     ->focus('#add_item_parts_id_name');
 
 802   $self->js_redisplay_amounts_and_taxes;
 
 806 # open the dialog for entering multiple items at once
 
 807 sub action_show_multi_items_dialog {
 
 808   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
 
 809                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
 
 812 # update the filter results in the multi item dialog
 
 813 sub action_multi_items_update_result {
 
 816   $::form->{multi_items}->{filter}->{obsolete} = 0;
 
 818   my $count = $_[0]->multi_items_models->count;
 
 821     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
 
 822     $_[0]->render($text, { layout => 0 });
 
 823   } elsif ($count > $max_count) {
 
 824     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
 
 825     $_[0]->render($text, { layout => 0 });
 
 827     my $multi_items = $_[0]->multi_items_models->get;
 
 828     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
 
 829                   multi_items => $multi_items);
 
 833 # add item rows for multiple items at once
 
 834 sub action_add_multi_items {
 
 837   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
 
 838   return $self->js->render() unless scalar @form_attr;
 
 841   foreach my $attr (@form_attr) {
 
 842     my $item = new_item($self->order, $attr);
 
 844     if ( $item->part->is_assortment ) {
 
 845       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 846         my $attr = { parts_id => $assortment_item->parts_id,
 
 847                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 848                      unit     => $assortment_item->unit,
 
 849                      description => $assortment_item->part->description,
 
 851         my $item = new_item($self->order, $attr);
 
 853         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 854         $item->discount(1) unless $assortment_item->charge;
 
 859   $self->order->add_items(@items);
 
 863   foreach my $item (@items) {
 
 864     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 865     my $row_as_html = $self->p->render('order/tabs/_row',
 
 869                                        ALL_PRICE_FACTORS => $self->all_price_factors
 
 872     $self->js->append('#row_table_id', $row_as_html);
 
 876     ->run('kivi.Order.close_multi_items_dialog')
 
 877     ->run('kivi.Order.init_row_handlers')
 
 878     ->run('kivi.Order.row_table_scroll_down')
 
 879     ->run('kivi.Order.renumber_positions')
 
 880     ->focus('#add_item_parts_id_name');
 
 882   $self->js_redisplay_amounts_and_taxes;
 
 886 # recalculate all linetotals, amounts and taxes and redisplay them
 
 887 sub action_recalc_amounts_and_taxes {
 
 892   $self->js_redisplay_line_values;
 
 893   $self->js_redisplay_amounts_and_taxes;
 
 897 # redisplay item rows if they are sorted by an attribute
 
 898 sub action_reorder_items {
 
 902     partnumber  => sub { $_[0]->part->partnumber },
 
 903     description => sub { $_[0]->description },
 
 904     qty         => sub { $_[0]->qty },
 
 905     sellprice   => sub { $_[0]->sellprice },
 
 906     discount    => sub { $_[0]->discount },
 
 909   my $method = $sort_keys{$::form->{order_by}};
 
 910   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
 911   if ($::form->{sort_dir}) {
 
 912     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 913       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
 915       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 918     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 919       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
 921       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 925     ->run('kivi.Order.redisplay_items', \@to_sort)
 
 929 # show the popup to choose a price/discount source
 
 930 sub action_price_popup {
 
 933   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 934   my $item = $self->order->items_sorted->[$idx];
 
 936   $self->render_price_dialog($item);
 
 939 # get the longdescription for an item if the dialog to enter/change the
 
 940 # longdescription was opened and the longdescription is empty
 
 942 # If this item is new, get the longdescription from Part.
 
 943 # Otherwise get it from OrderItem.
 
 944 sub action_get_item_longdescription {
 
 947   if ($::form->{item_id}) {
 
 948     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
 
 949   } elsif ($::form->{parts_id}) {
 
 950     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
 
 952   $_[0]->render(\ $longdescription, { type => 'text' });
 
 955 # load the second row for one or more items
 
 957 # This action gets the html code for all items second rows by rendering a template for
 
 958 # the second row and sets the html code via client js.
 
 959 sub action_load_second_rows {
 
 962   $self->recalc() if $self->order->is_sales; # for margin calculation
 
 964   foreach my $item_id (@{ $::form->{item_ids} }) {
 
 965     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
 966     my $item = $self->order->items_sorted->[$idx];
 
 968     $self->js_load_second_row($item, $item_id, 0);
 
 971   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
 976 sub js_load_second_row {
 
 977   my ($self, $item, $item_id, $do_parse) = @_;
 
 980     # Parse values from form (they are formated while rendering (template)).
 
 981     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
 982     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
 983     foreach my $var (@{ $item->cvars_by_config }) {
 
 984       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
 986     $item->parse_custom_variable_values;
 
 989   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
 992     ->html('#second_row_' . $item_id, $row_as_html)
 
 993     ->data('#second_row_' . $item_id, 'loaded', 1);
 
 996 sub js_redisplay_line_values {
 
 999   my $is_sales = $self->order->is_sales;
 
1001   # sales orders with margins
 
1006        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1007        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1008        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1009       ]} @{ $self->order->items_sorted };
 
1013        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1014       ]} @{ $self->order->items_sorted };
 
1018     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1021 sub js_redisplay_amounts_and_taxes {
 
1024   if (scalar @{ $self->{taxes} }) {
 
1025     $self->js->show('#taxincluded_row_id');
 
1027     $self->js->hide('#taxincluded_row_id');
 
1030   if ($self->order->taxincluded) {
 
1031     $self->js->hide('#subtotal_row_id');
 
1033     $self->js->show('#subtotal_row_id');
 
1036   if ($self->order->is_sales) {
 
1037     my $is_neg = $self->order->marge_total < 0;
 
1039       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1040       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1041       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1042       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1043       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1044       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1045       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1046       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1050     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1051     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1052     ->remove('.tax_row')
 
1053     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1056 sub js_reset_order_and_item_ids_after_save {
 
1060     ->val('#id', $self->order->id)
 
1061     ->val('#converted_from_oe_id', '')
 
1062     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1065   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1066     next if !$self->order->items_sorted->[$idx]->id;
 
1067     next if $form_item_id !~ m{^new};
 
1069       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1070       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1071       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1074   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1081 sub init_valid_types {
 
1082   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1088   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1089     die "Not a valid type for order";
 
1092   $self->type($::form->{type});
 
1098   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1099          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1100          : die "Not a valid type for order";
 
1113 # model used to filter/display the parts in the multi-items dialog
 
1114 sub init_multi_items_models {
 
1115   SL::Controller::Helper::GetModels->new(
 
1116     controller     => $_[0],
 
1118     with_objects   => [ qw(unit_obj) ],
 
1119     disable_plugin => 'paginated',
 
1120     source         => $::form->{multi_items},
 
1126       partnumber  => t8('Partnumber'),
 
1127       description => t8('Description')}
 
1131 sub init_all_price_factors {
 
1132   SL::DB::Manager::PriceFactor->get_all;
 
1138   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1140   my $right   = $right_for->{ $self->type };
 
1141   $right    ||= 'DOES_NOT_EXIST';
 
1143   $::auth->assert($right);
 
1146 # build the selection box for contacts
 
1148 # Needed, if customer/vendor changed.
 
1149 sub build_contact_select {
 
1152   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1153     value_key  => 'cp_id',
 
1154     title_key  => 'full_name_dep',
 
1155     default    => $self->order->cp_id,
 
1157     style      => 'width: 300px',
 
1161 # build the selection box for shiptos
 
1163 # Needed, if customer/vendor changed.
 
1164 sub build_shipto_select {
 
1167   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
 
1168     value_key  => 'shipto_id',
 
1169     title_key  => 'displayable_id',
 
1170     default    => $self->order->shipto_id,
 
1172     style      => 'width: 300px',
 
1176 # render the info line for business
 
1178 # Needed, if customer/vendor changed.
 
1179 sub build_business_info_row
 
1181   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1184 # build the rows for displaying taxes
 
1186 # Called if amounts where recalculated and redisplayed.
 
1187 sub build_tax_rows {
 
1191   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1192     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1194   return $rows_as_html;
 
1198 sub render_price_dialog {
 
1199   my ($self, $record_item) = @_;
 
1201   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1205       'kivi.io.price_chooser_dialog',
 
1206       t8('Available Prices'),
 
1207       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1212 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1213 #     $self->js->show('#dialog_flash_error');
 
1222   return if !$::form->{id};
 
1224   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1227 # load or create a new order object
 
1229 # And assign changes from the form to this object.
 
1230 # If the order is loaded from db, check if items are deleted in the form,
 
1231 # remove them form the object and collect them for removing from db on saving.
 
1232 # Then create/update items from form (via make_item) and add them.
 
1236   # add_items adds items to an order with no items for saving, but they cannot
 
1237   # be retrieved via items until the order is saved. Adding empty items to new
 
1238   # order here solves this problem.
 
1240   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1241   $order ||= SL::DB::Order->new(orderitems => [],
 
1242                                 quotation  => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())));
 
1244   my $cv_id_method = $self->cv . '_id';
 
1245   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1246     $order->$cv_id_method($::form->{$cv_id_method});
 
1247     setup_order_from_cv($order);
 
1250   my $form_orderitems               = delete $::form->{order}->{orderitems};
 
1251   my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
 
1253   $order->assign_attributes(%{$::form->{order}});
 
1255   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1256     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1257     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1260   # remove deleted items
 
1261   $self->item_ids_to_delete([]);
 
1262   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1263     my $item = $order->orderitems->[$idx];
 
1264     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1265       splice @{$order->orderitems}, $idx, 1;
 
1266       push @{$self->item_ids_to_delete}, $item->id;
 
1272   foreach my $form_attr (@{$form_orderitems}) {
 
1273     my $item = make_item($order, $form_attr);
 
1274     $item->position($pos);
 
1278   $order->add_items(grep {!$_->id} @items);
 
1283 # create or update items from form
 
1285 # Make item objects from form values. For items already existing read from db.
 
1286 # Create a new item else. And assign attributes.
 
1288   my ($record, $attr) = @_;
 
1291   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1293   my $is_new = !$item;
 
1295   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1296   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1297   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1298   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1300   $item->assign_attributes(%$attr);
 
1301   $item->longdescription($item->part->notes)                     if $is_new && !defined $attr->{longdescription};
 
1302   $item->project_id($record->globalproject_id)                   if $is_new && !defined $attr->{project_id};
 
1303   $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
 
1310 # This is used to add one item
 
1312   my ($record, $attr) = @_;
 
1314   my $item = SL::DB::OrderItem->new;
 
1316   # Remove attributes where the user left or set the inputs empty.
 
1317   # So these attributes will be undefined and we can distinguish them
 
1318   # from zero later on.
 
1319   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1320     delete $attr->{$_} if $attr->{$_} eq '';
 
1323   $item->assign_attributes(%$attr);
 
1325   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1326   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1328   $item->unit($part->unit) if !$item->unit;
 
1331   if ( $part->is_assortment ) {
 
1332     # add assortment items with price 0, as the components carry the price
 
1333     $price_src = $price_source->price_from_source("");
 
1334     $price_src->price(0);
 
1335   } elsif (defined $item->sellprice) {
 
1336     $price_src = $price_source->price_from_source("");
 
1337     $price_src->price($item->sellprice);
 
1339     $price_src = $price_source->best_price
 
1340            ? $price_source->best_price
 
1341            : $price_source->price_from_source("");
 
1342     $price_src->price(0) if !$price_source->best_price;
 
1346   if (defined $item->discount) {
 
1347     $discount_src = $price_source->discount_from_source("");
 
1348     $discount_src->discount($item->discount);
 
1350     $discount_src = $price_source->best_discount
 
1351                   ? $price_source->best_discount
 
1352                   : $price_source->discount_from_source("");
 
1353     $discount_src->discount(0) if !$price_source->best_discount;
 
1357   $new_attr{part}                   = $part;
 
1358   $new_attr{description}            = $part->description     if ! $item->description;
 
1359   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1360   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1361   $new_attr{sellprice}              = $price_src->price;
 
1362   $new_attr{discount}               = $discount_src->discount;
 
1363   $new_attr{active_price_source}    = $price_src;
 
1364   $new_attr{active_discount_source} = $discount_src;
 
1365   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1366   $new_attr{project_id}             = $record->globalproject_id;
 
1367   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1369   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1370   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1371   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1372   $new_attr{custom_variables} = [];
 
1374   $item->assign_attributes(%new_attr);
 
1379 sub setup_order_from_cv {
 
1382   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id));
 
1384   $order->intnotes($order->customervendor->notes);
 
1386   if ($order->is_sales) {
 
1387     $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1388     $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1389                         ? $order->customer->taxincluded_checked
 
1390                         : $::myconfig{taxincluded_checked});
 
1395 # recalculate prices and taxes
 
1397 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1401   # bb: todo: currency later
 
1402   $self->order->currency_id($::instance_conf->get_currency_id());
 
1404   my %pat = $self->order->calculate_prices_and_taxes();
 
1405   $self->{taxes} = [];
 
1406   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
 
1407     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
 
1409     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
 
1410     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
 
1411                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
 
1415   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1418 # get data for saving, printing, ..., that is not changed in the form
 
1420 # Only cvars for now.
 
1421 sub get_unalterable_data {
 
1424   foreach my $item (@{ $self->order->items }) {
 
1425     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1426     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1427     foreach my $var (@{ $item->cvars_by_config }) {
 
1428       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1430     $item->parse_custom_variable_values;
 
1436 # And remove related files in the spool directory
 
1441   my $db     = $self->order->db;
 
1443   $db->with_transaction(
 
1445       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1446       $self->order->delete;
 
1447       my $spool = $::lx_office_conf{paths}->{spool};
 
1448       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1451   }) || push(@{$errors}, $db->error);
 
1458 # And delete items that are deleted in the form.
 
1463   my $db     = $self->order->db;
 
1465   $db->with_transaction(sub {
 
1466     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1467     $self->order->save(cascade => 1);
 
1470     if ($::form->{converted_from_oe_id}) {
 
1471       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1472       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1473         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1474         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1475         $src->link_to_record($self->order);
 
1477       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1479         foreach (@{ $self->order->items_sorted }) {
 
1480           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1482           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1483                                   from_id    => $from_id,
 
1484                                   to_table   => 'orderitems',
 
1492   }) || push(@{$errors}, $db->error);
 
1497 sub workflow_sales_or_purchase_order {
 
1501   my $errors = $self->save();
 
1503   if (scalar @{ $errors }) {
 
1504     $self->js->flash('error', $_) foreach @{ $errors };
 
1505     return $self->js->render();
 
1508   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1509                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1510                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1511                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1514   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1515   $self->{converted_from_oe_id} = delete $::form->{id};
 
1517   # set item ids to new fake id, to identify them as new items
 
1518   foreach my $item (@{$self->order->items_sorted}) {
 
1519     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1523   $::form->{type} = $destination_type;
 
1524   $self->type($self->init_type);
 
1525   $self->cv  ($self->init_cv);
 
1529   $self->get_unalterable_data();
 
1530   $self->pre_render();
 
1532   # trigger rendering values for second row/longdescription as hidden,
 
1533   # because they are loaded only on demand. So we need to keep the values
 
1535   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1536   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1540     title => $self->get_title_for('edit'),
 
1541     %{$self->{template_args}}
 
1549   $self->{all_taxzones}             = SL::DB::Manager::TaxZone->get_all_sorted();
 
1550   $self->{all_departments}          = SL::DB::Manager::Department->get_all_sorted();
 
1551   $self->{all_employees}            = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1554   $self->{all_salesmen}             = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1557   $self->{all_payment_terms}        = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1559   $self->{all_delivery_terms}       = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1560   $self->{current_employee_id}      = SL::DB::Manager::Employee->current->id;
 
1561   $self->{periodic_invoices_status} = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1562   $self->{order_probabilities}      = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1564   my $print_form = Form->new('');
 
1565   $print_form->{type}      = $self->type;
 
1566   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
 
1567   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
 
1568   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
 
1569     form => $print_form,
 
1570     options => {dialog_name_prefix => 'print_options.',
 
1574                 no_opendocument    => 0,
 
1578   foreach my $item (@{$self->order->orderitems}) {
 
1579     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1580     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1581     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1584   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1585     # calculate shipped qtys here to prevent calling calculate for every item via the items method
 
1586     SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects;
 
1589   if ($self->order->number && $::instance_conf->get_webdav) {
 
1590     my $webdav = SL::Webdav->new(
 
1591       type     => $self->type,
 
1592       number   => $self->order->number,
 
1594     my @all_objects = $webdav->get_all_objects;
 
1595     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1597                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1601   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config calculate_qty);
 
1602   $self->setup_edit_action_bar;
 
1605 sub setup_edit_action_bar {
 
1606   my ($self, %params) = @_;
 
1608   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
1609                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1610                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1612   for my $bar ($::request->layout->get('actionbar')) {
 
1617           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1618                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
1620           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1624           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1625           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1626           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1628       ], # end of combobox "Save"
 
1635           t8('Save and Sales Order'),
 
1636           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
1637           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
1638           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1641           t8('Save and Purchase Order'),
 
1642           submit   => [ '#order_form', { action => "Order/purchase_order" } ],
 
1643           only_if  => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
1644           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1647           t8('Save and Delivery Order'),
 
1648           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
1649                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1651           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1652           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
 
1655           t8('Save and Invoice'),
 
1656           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1657           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1659       ], # end of combobox "Workflow"
 
1666           t8('Save and print'),
 
1667           call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ],
 
1670           t8('Save and E-mail'),
 
1671           call => [ 'kivi.Order.email', $::instance_conf->get_order_warn_duplicate_parts ],
 
1674           t8('Download attachments of all parts'),
 
1675           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1676           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1677           only_if  => $::instance_conf->get_doc_storage,
 
1679       ], # end of combobox "Export"
 
1683         call     => [ 'kivi.Order.delete_order' ],
 
1684         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1685         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1686         only_if  => $deletion_allowed,
 
1693   my ($order, $pdf_ref, $params) = @_;
 
1697   my $print_form = Form->new('');
 
1698   $print_form->{type}        = $order->type;
 
1699   $print_form->{formname}    = $params->{formname} || $order->type;
 
1700   $print_form->{format}      = $params->{format}   || 'pdf';
 
1701   $print_form->{media}       = $params->{media}    || 'file';
 
1702   $print_form->{groupitems}  = $params->{groupitems};
 
1703   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
1705   $order->language($params->{language});
 
1706   $order->flatten_to_form($print_form, format_amounts => 1);
 
1710   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
1711     $template_ext  = 'odt';
 
1712     $template_type = 'OpenDocument';
 
1715   # search for the template
 
1716   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
1717     name        => $print_form->{formname},
 
1718     extension   => $template_ext,
 
1719     email       => $print_form->{media} eq 'email',
 
1720     language    => $params->{language},
 
1721     printer_id  => $print_form->{printer_id},  # todo
 
1724   if (!defined $template_file) {
 
1725     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);
 
1728   return @errors if scalar @errors;
 
1730   $print_form->throw_on_error(sub {
 
1732       $print_form->prepare_for_printing;
 
1734       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
1735         format        => $print_form->{format},
 
1736         template_type => $template_type,
 
1737         template      => $template_file,
 
1738         variables     => $print_form,
 
1739         variable_content_types => {
 
1740           longdescription => 'html',
 
1741           partnotes       => 'html',
 
1746     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
1752 sub get_files_for_email_dialog {
 
1755   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
1757   return %files if !$::instance_conf->get_doc_storage;
 
1759   if ($self->order->id) {
 
1760     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
1761     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
1762     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
1766     uniq_by { $_->{id} }
 
1768       +{ id         => $_->part->id,
 
1769          partnumber => $_->part->partnumber }
 
1770     } @{$self->order->items_sorted};
 
1772   foreach my $part (@parts) {
 
1773     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
1774     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
1777   foreach my $key (keys %files) {
 
1778     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
1784 sub make_periodic_invoices_config_from_yaml {
 
1785   my ($yaml_config) = @_;
 
1787   return if !$yaml_config;
 
1788   my $attr = SL::YAML::Load($yaml_config);
 
1789   return if 'HASH' ne ref $attr;
 
1790   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
1794 sub get_periodic_invoices_status {
 
1795   my ($self, $config) = @_;
 
1797   return                      if $self->type ne sales_order_type();
 
1798   return t8('not configured') if !$config;
 
1800   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
1801              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
1802              :                                                     die "Cannot get status of periodic invoices config";
 
1804   return $active ? t8('active') : t8('inactive');
 
1808   my ($self, $action) = @_;
 
1810   return '' if none { lc($action)} qw(add edit);
 
1813   # $::locale->text("Add Sales Order");
 
1814   # $::locale->text("Add Purchase Order");
 
1815   # $::locale->text("Add Quotation");
 
1816   # $::locale->text("Add Request for Quotation");
 
1817   # $::locale->text("Edit Sales Order");
 
1818   # $::locale->text("Edit Purchase Order");
 
1819   # $::locale->text("Edit Quotation");
 
1820   # $::locale->text("Edit Request for Quotation");
 
1822   $action = ucfirst(lc($action));
 
1823   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
1824        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
1825        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
1826        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
1830 sub sales_order_type {
 
1834 sub purchase_order_type {
 
1838 sub sales_quotation_type {
 
1842 sub request_quotation_type {
 
1843   'request_quotation';
 
1847   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
1848        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
1849        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
1850        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
1862 SL::Controller::Order - controller for orders
 
1866 This is a new form to enter orders, completely rewritten with the use
 
1867 of controller and java script techniques.
 
1869 The aim is to provide the user a better expirience and a faster flow
 
1870 of work. Also the code should be more readable, more reliable and
 
1879 One input row, so that input happens every time at the same place.
 
1883 Use of pickers where possible.
 
1887 Possibility to enter more than one item at once.
 
1891 Item list in a scrollable area, so that the workflow buttons stay at
 
1896 Reordering item rows with drag and drop is possible. Sorting item rows is
 
1897 possible (by partnumber, description, qty, sellprice and discount for now).
 
1901 No C<update> is necessary. All entries and calculations are managed
 
1902 with ajax-calls and the page does only reload on C<save>.
 
1906 User can see changes immediately, because of the use of java script
 
1917 =item * C<SL/Controller/Order.pm>
 
1921 =item * C<template/webpages/order/form.html>
 
1925 =item * C<template/webpages/order/tabs/basic_data.html>
 
1927 Main tab for basic_data.
 
1929 This is the only tab here for now. "linked records" and "webdav" tabs are
 
1930 reused from generic code.
 
1934 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
1936 For displaying information on business type
 
1938 =item * C<template/webpages/order/tabs/_item_input.html>
 
1940 The input line for items
 
1942 =item * C<template/webpages/order/tabs/_row.html>
 
1944 One row for already entered items
 
1946 =item * C<template/webpages/order/tabs/_tax_row.html>
 
1948 Displaying tax information
 
1950 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
 
1952 Dialog for entering more than one item at once
 
1954 =item * C<template/webpages/order/tabs/_multi_items_result.html>
 
1956 Results for the filter in the multi items dialog
 
1958 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
1960 Dialog for selecting price and discount sources
 
1964 =item * C<js/kivi.Order.js>
 
1966 java script functions
 
1978 =item * credit limit
 
1980 =item * more workflows (quotation, rfq)
 
1982 =item * price sources: little symbols showing better price / better discount
 
1984 =item * select units in input row?
 
1986 =item * custom shipto address
 
1988 =item * check for direct delivery (workflow sales order -> purchase order)
 
1990 =item * language / part translations
 
1992 =item * access rights
 
1994 =item * display weights
 
2000 =item * optional client/user behaviour
 
2002 (transactions has to be set - department has to be set -
 
2003  force project if enabled in client config - transport cost reminder)
 
2007 =head1 KNOWN BUGS AND CAVEATS
 
2013 Customer discount is not displayed as a valid discount in price source popup
 
2014 (this might be a bug in price sources)
 
2016 (I cannot reproduce this (Bernd))
 
2020 No indication that <shift>-up/down expands/collapses second row.
 
2024 Inline creation of parts is not currently supported
 
2028 Table header is not sticky in the scrolling area.
 
2032 Sorting does not include C<position>, neither does reordering.
 
2034 This behavior was implemented intentionally. But we can discuss, which behavior
 
2035 should be implemented.
 
2039 C<show_multi_items_dialog> does not use the currently inserted string for
 
2044 The language selected in print or email dialog is not saved when the order is saved.
 
2048 =head1 To discuss / Nice to have
 
2054 How to expand/collapse second row. Now it can be done clicking the icon or
 
2059 Possibility to change longdescription in input row?
 
2063 Possibility to select PriceSources in input row?
 
2067 This controller uses a (changed) copy of the template for the PriceSource
 
2068 dialog. Maybe there could be used one code source.
 
2072 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2073 form. This is not only a problem here, but also in all parts using the PTC.
 
2074 There exists a ticket and a patch. This patch should be testet.
 
2078 An indicator, if the actual inputs are saved (like in an
 
2079 editor or on text processing application).
 
2083 A warning when leaving the page without saveing unchanged inputs.
 
2090 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>