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);
 
  18 use SL::DB::PartsGroup;
 
  21 use SL::DB::RecordLink;
 
  23 use SL::Helper::CreatePDF qw(:all);
 
  24 use SL::Helper::PrintOptions;
 
  25 use SL::Helper::ShippedQty;
 
  27 use SL::Controller::Helper::GetModels;
 
  29 use List::Util qw(first);
 
  30 use List::UtilsBy qw(sort_by uniq_by);
 
  31 use List::MoreUtils qw(any none pairwise first_index);
 
  32 use English qw(-no_match_vars);
 
  36 use Rose::Object::MakeMethods::Generic
 
  38  scalar => [ qw(item_ids_to_delete) ],
 
  39  'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
 
  44 __PACKAGE__->run_before('check_auth');
 
  46 __PACKAGE__->run_before('recalc',
 
  47                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
 
  49 __PACKAGE__->run_before('get_unalterable_data',
 
  50                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
 
  60   $self->order->transdate(DateTime->now_local());
 
  61   my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
 
  62                    $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
 
  63   $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
 
  69     title => $self->get_title_for('add'),
 
  70     %{$self->{template_args}}
 
  74 # edit an existing order
 
  82     # this is to edit an order from an unsaved order object
 
  84     # set item ids to new fake id, to identify them as new items
 
  85     foreach my $item (@{$self->order->items_sorted}) {
 
  86       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
  88     # trigger rendering values for second row/longdescription as hidden,
 
  89     # because they are loaded only on demand. So we need to keep the values
 
  91     $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
  92     $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
  99     title => $self->get_title_for('edit'),
 
 100     %{$self->{template_args}}
 
 104 # edit a collective order (consisting of one or more existing orders)
 
 105 sub action_edit_collective {
 
 109   my @multi_ids = map {
 
 110     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 111   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 113   # fall back to add if no ids are given
 
 114   if (scalar @multi_ids == 0) {
 
 119   # fall back to save as new if only one id is given
 
 120   if (scalar @multi_ids == 1) {
 
 121     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 122     $self->action_save_as_new();
 
 126   # make new order from given orders
 
 127   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 128   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 129   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 131   $self->action_edit();
 
 138   my $errors = $self->delete();
 
 140   if (scalar @{ $errors }) {
 
 141     $self->js->flash('error', $_) foreach @{ $errors };
 
 142     return $self->js->render();
 
 145   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 146            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 147            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 148            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 150   flash_later('info', $text);
 
 152   my @redirect_params = (
 
 157   $self->redirect_to(@redirect_params);
 
 164   my $errors = $self->save();
 
 166   if (scalar @{ $errors }) {
 
 167     $self->js->flash('error', $_) foreach @{ $errors };
 
 168     return $self->js->render();
 
 171   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 172            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 173            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 174            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 176   flash_later('info', $text);
 
 178   my @redirect_params = (
 
 181     id     => $self->order->id,
 
 184   $self->redirect_to(@redirect_params);
 
 187 # save the order as new document an open it for edit
 
 188 sub action_save_as_new {
 
 191   my $order = $self->order;
 
 194     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 195     return $self->js->render();
 
 198   # load order from db to check if values changed
 
 199   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 202   # Lets assign a new number if the user hasn't changed the previous one.
 
 203   # If it has been changed manually then use it as-is.
 
 204   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 206                         : trim($order->number);
 
 208   # Clear transdate unless changed
 
 209   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 210                         ? DateTime->today_local
 
 213   # Set new reqdate unless changed
 
 214   if ($order->reqdate == $saved_order->reqdate) {
 
 215     my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
 
 216                      $self->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
 
 217     $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 219     $new_attrs{reqdate} = $order->reqdate;
 
 223   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 225   # Create new record from current one
 
 226   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 228   # no linked records on save as new
 
 229   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 232   $self->action_save();
 
 237 # This is called if "print" is pressed in the print dialog.
 
 238 # If PDF creation was requested and succeeded, the pdf is stored in a session
 
 239 # file and the filename is stored as session value with an unique key. A
 
 240 # javascript function with this key is then called. This function calls the
 
 241 # download action below (action_download_pdf), which offers the file for
 
 246   my $errors = $self->save();
 
 248   if (scalar @{ $errors }) {
 
 249     $self->js->flash('error', $_) foreach @{ $errors };
 
 250     return $self->js->render();
 
 253   $self->js->val('#id', $self->order->id)
 
 254            ->val('#order_' . $self->nr_key(), $self->order->number);
 
 256   my $format      = $::form->{print_options}->{format};
 
 257   my $media       = $::form->{print_options}->{media};
 
 258   my $formname    = $::form->{print_options}->{formname};
 
 259   my $copies      = $::form->{print_options}->{copies};
 
 260   my $groupitems  = $::form->{print_options}->{groupitems};
 
 262   # only pdf and opendocument by now
 
 263   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
 
 264     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 267   # only screen or printer by now
 
 268   if (none { $media eq $_ } qw(screen printer)) {
 
 269     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 273   $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 275   # create a form for generate_attachment_filename
 
 276   my $form   = Form->new;
 
 277   $form->{$self->nr_key()}  = $self->order->number;
 
 278   $form->{type}             = $self->type;
 
 279   $form->{format}           = $format;
 
 280   $form->{formname}         = $formname;
 
 281   $form->{language}         = '_' . $language->template_code if $language;
 
 282   my $pdf_filename          = $form->generate_attachment_filename();
 
 285   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
 
 286                                                    formname   => $formname,
 
 287                                                    language   => $language,
 
 288                                                    groupitems => $groupitems });
 
 289   if (scalar @errors) {
 
 290     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 293   if ($media eq 'screen') {
 
 295     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 296     $sfile->fh->print($pdf);
 
 299     my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 300     $::auth->set_session_value("Order::print-${key}" => $sfile->file_name);
 
 303     ->run('kivi.Order.download_pdf', $pdf_filename, $key)
 
 304     ->flash('info', t8('The PDF has been created'));
 
 306   } elsif ($media eq 'printer') {
 
 308     my $printer_id = $::form->{print_options}->{printer_id};
 
 309     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 314     $self->js->flash('info', t8('The PDF has been printed'));
 
 317   # copy file to webdav folder
 
 318   if ($self->order->number && $::instance_conf->get_webdav_documents) {
 
 319     my $webdav = SL::Webdav->new(
 
 321       number   => $self->order->number,
 
 323     my $webdav_file = SL::Webdav::File->new(
 
 325       filename => $pdf_filename,
 
 328       $webdav_file->store(data => \$pdf);
 
 331       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
 
 334   if ($self->order->number && $::instance_conf->get_doc_storage) {
 
 336       SL::File->save(object_id     => $self->order->id,
 
 337                      object_type   => $self->type,
 
 338                      mime_type     => 'application/pdf',
 
 340                      file_type     => 'document',
 
 341                      file_name     => $pdf_filename,
 
 342                      file_contents => $pdf);
 
 345       $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
 
 351 # offer pdf for download
 
 353 # It needs to get the key for the session value to get the pdf file.
 
 354 sub action_download_pdf {
 
 357   my $key = $::form->{key};
 
 358   my $tmp_filename = $::auth->get_session_value("Order::print-${key}");
 
 359   return $self->send_file(
 
 361     type => 'application/pdf',
 
 362     name => $::form->{pdf_filename},
 
 366 # open the email dialog
 
 367 sub action_show_email_dialog {
 
 370   my $cv_method = $self->cv;
 
 372   if (!$self->order->$cv_method) {
 
 373     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'))
 
 378   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 379   $email_form->{to} ||= $self->order->$cv_method->email;
 
 380   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 381   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 382   # Todo: get addresses from shipto, if any
 
 384   my $form = Form->new;
 
 385   $form->{$self->nr_key()}  = $self->order->number;
 
 386   $form->{formname}         = $self->type;
 
 387   $form->{type}             = $self->type;
 
 388   $form->{language}         = 'de';
 
 389   $form->{format}           = 'pdf';
 
 391   $email_form->{subject}             = $form->generate_email_subject();
 
 392   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 393   $email_form->{message}             = $form->generate_email_body();
 
 394   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 396   my %files = $self->get_files_for_email_dialog();
 
 397   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 398                                   email_form  => $email_form,
 
 399                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
 
 401                                   is_customer => $self->cv eq 'customer',
 
 405       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 412 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 413 sub action_send_email {
 
 416   my $errors = $self->save();
 
 418   if (scalar @{ $errors }) {
 
 419     $self->js->run('kivi.Order.close_email_dialog');
 
 420     $self->js->flash('error', $_) foreach @{ $errors };
 
 421     return $self->js->render();
 
 424   $self->js->val('#id', $self->order->id)
 
 425            ->val('#order_' . $self->nr_key(), $self->order->number);
 
 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 = 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     @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 914     @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 917     ->run('kivi.Order.redisplay_items', \@to_sort)
 
 921 # show the popup to choose a price/discount source
 
 922 sub action_price_popup {
 
 925   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 926   my $item = $self->order->items_sorted->[$idx];
 
 928   $self->render_price_dialog($item);
 
 931 # get the longdescription for an item if the dialog to enter/change the
 
 932 # longdescription was opened and the longdescription is empty
 
 934 # If this item is new, get the longdescription from Part.
 
 935 # Otherwise get it from OrderItem.
 
 936 sub action_get_item_longdescription {
 
 939   if ($::form->{item_id}) {
 
 940     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
 
 941   } elsif ($::form->{parts_id}) {
 
 942     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
 
 944   $_[0]->render(\ $longdescription, { type => 'text' });
 
 947 # load the second row for one or more items
 
 949 # This action gets the html code for all items second rows by rendering a template for
 
 950 # the second row and sets the html code via client js.
 
 951 sub action_load_second_rows {
 
 954   $self->recalc() if $self->order->is_sales; # for margin calculation
 
 956   foreach my $item_id (@{ $::form->{item_ids} }) {
 
 957     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
 958     my $item = $self->order->items_sorted->[$idx];
 
 960     $self->js_load_second_row($item, $item_id, 0);
 
 963   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
 968 sub js_load_second_row {
 
 969   my ($self, $item, $item_id, $do_parse) = @_;
 
 972     # Parse values from form (they are formated while rendering (template)).
 
 973     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
 974     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
 975     foreach my $var (@{ $item->cvars_by_config }) {
 
 976       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
 978     $item->parse_custom_variable_values;
 
 981   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
 984     ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
 
 985     ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
 
 988 sub js_redisplay_line_values {
 
 991   my $is_sales = $self->order->is_sales;
 
 993   # sales orders with margins
 
 998        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
 999        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1000        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1001       ]} @{ $self->order->items_sorted };
 
1005        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1006       ]} @{ $self->order->items_sorted };
 
1010     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1013 sub js_redisplay_amounts_and_taxes {
 
1016   if (scalar @{ $self->{taxes} }) {
 
1017     $self->js->show('#taxincluded_row_id');
 
1019     $self->js->hide('#taxincluded_row_id');
 
1022   if ($self->order->taxincluded) {
 
1023     $self->js->hide('#subtotal_row_id');
 
1025     $self->js->show('#subtotal_row_id');
 
1028   if ($self->order->is_sales) {
 
1029     my $is_neg = $self->order->marge_total < 0;
 
1031       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1032       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1033       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1034       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1035       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1036       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1037       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1038       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1042     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1043     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1044     ->remove('.tax_row')
 
1045     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1052 sub init_valid_types {
 
1053   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1059   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1060     die "Not a valid type for order";
 
1063   $self->type($::form->{type});
 
1069   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1070          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1071          : die "Not a valid type for order";
 
1084 # model used to filter/display the parts in the multi-items dialog
 
1085 sub init_multi_items_models {
 
1086   SL::Controller::Helper::GetModels->new(
 
1087     controller     => $_[0],
 
1089     with_objects   => [ qw(unit_obj) ],
 
1090     disable_plugin => 'paginated',
 
1091     source         => $::form->{multi_items},
 
1097       partnumber  => t8('Partnumber'),
 
1098       description => t8('Description')}
 
1102 sub init_all_price_factors {
 
1103   SL::DB::Manager::PriceFactor->get_all;
 
1109   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1111   my $right   = $right_for->{ $self->type };
 
1112   $right    ||= 'DOES_NOT_EXIST';
 
1114   $::auth->assert($right);
 
1117 # build the selection box for contacts
 
1119 # Needed, if customer/vendor changed.
 
1120 sub build_contact_select {
 
1123   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1124     value_key  => 'cp_id',
 
1125     title_key  => 'full_name_dep',
 
1126     default    => $self->order->cp_id,
 
1128     style      => 'width: 300px',
 
1132 # build the selection box for shiptos
 
1134 # Needed, if customer/vendor changed.
 
1135 sub build_shipto_select {
 
1138   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
 
1139     value_key  => 'shipto_id',
 
1140     title_key  => 'displayable_id',
 
1141     default    => $self->order->shipto_id,
 
1143     style      => 'width: 300px',
 
1147 # render the info line for business
 
1149 # Needed, if customer/vendor changed.
 
1150 sub build_business_info_row
 
1152   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1155 # build the rows for displaying taxes
 
1157 # Called if amounts where recalculated and redisplayed.
 
1158 sub build_tax_rows {
 
1162   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1163     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1165   return $rows_as_html;
 
1169 sub render_price_dialog {
 
1170   my ($self, $record_item) = @_;
 
1172   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1176       'kivi.io.price_chooser_dialog',
 
1177       t8('Available Prices'),
 
1178       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1183 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1184 #     $self->js->show('#dialog_flash_error');
 
1193   return if !$::form->{id};
 
1195   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1198 # load or create a new order object
 
1200 # And assign changes from the form to this object.
 
1201 # If the order is loaded from db, check if items are deleted in the form,
 
1202 # remove them form the object and collect them for removing from db on saving.
 
1203 # Then create/update items from form (via make_item) and add them.
 
1207   # add_items adds items to an order with no items for saving, but they cannot
 
1208   # be retrieved via items until the order is saved. Adding empty items to new
 
1209   # order here solves this problem.
 
1211   $order   = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
 
1212   $order ||= SL::DB::Order->new(orderitems => [],
 
1213                                 quotation  => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())));
 
1215   my $cv_id_method = $self->cv . '_id';
 
1216   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1217     $order->$cv_id_method($::form->{$cv_id_method});
 
1218     setup_order_from_cv($order);
 
1221   my $form_orderitems               = delete $::form->{order}->{orderitems};
 
1222   my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
 
1224   $order->assign_attributes(%{$::form->{order}});
 
1226   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? YAML::Load($form_periodic_invoices_config) : undef) {
 
1227     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1228     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1231   # remove deleted items
 
1232   $self->item_ids_to_delete([]);
 
1233   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1234     my $item = $order->orderitems->[$idx];
 
1235     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1236       splice @{$order->orderitems}, $idx, 1;
 
1237       push @{$self->item_ids_to_delete}, $item->id;
 
1243   foreach my $form_attr (@{$form_orderitems}) {
 
1244     my $item = make_item($order, $form_attr);
 
1245     $item->position($pos);
 
1249   $order->add_items(grep {!$_->id} @items);
 
1254 # create or update items from form
 
1256 # Make item objects from form values. For items already existing read from db.
 
1257 # Create a new item else. And assign attributes.
 
1259   my ($record, $attr) = @_;
 
1262   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1264   my $is_new = !$item;
 
1266   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1267   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1268   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1269   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1271   $item->assign_attributes(%$attr);
 
1272   $item->longdescription($item->part->notes)                     if $is_new && !defined $attr->{longdescription};
 
1273   $item->project_id($record->globalproject_id)                   if $is_new && !defined $attr->{project_id};
 
1274   $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
 
1281 # This is used to add one item
 
1283   my ($record, $attr) = @_;
 
1285   my $item = SL::DB::OrderItem->new;
 
1287   # Remove attributes where the user left or set the inputs empty.
 
1288   # So these attributes will be undefined and we can distinguish them
 
1289   # from zero later on.
 
1290   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1291     delete $attr->{$_} if $attr->{$_} eq '';
 
1294   $item->assign_attributes(%$attr);
 
1296   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1297   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1299   $item->unit($part->unit) if !$item->unit;
 
1302   if ( $part->is_assortment ) {
 
1303     # add assortment items with price 0, as the components carry the price
 
1304     $price_src = $price_source->price_from_source("");
 
1305     $price_src->price(0);
 
1306   } elsif (defined $item->sellprice) {
 
1307     $price_src = $price_source->price_from_source("");
 
1308     $price_src->price($item->sellprice);
 
1310     $price_src = $price_source->best_price
 
1311            ? $price_source->best_price
 
1312            : $price_source->price_from_source("");
 
1313     $price_src->price(0) if !$price_source->best_price;
 
1317   if (defined $item->discount) {
 
1318     $discount_src = $price_source->discount_from_source("");
 
1319     $discount_src->discount($item->discount);
 
1321     $discount_src = $price_source->best_discount
 
1322                   ? $price_source->best_discount
 
1323                   : $price_source->discount_from_source("");
 
1324     $discount_src->discount(0) if !$price_source->best_discount;
 
1328   $new_attr{part}                   = $part;
 
1329   $new_attr{description}            = $part->description     if ! $item->description;
 
1330   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1331   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1332   $new_attr{sellprice}              = $price_src->price;
 
1333   $new_attr{discount}               = $discount_src->discount;
 
1334   $new_attr{active_price_source}    = $price_src;
 
1335   $new_attr{active_discount_source} = $discount_src;
 
1336   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1337   $new_attr{project_id}             = $record->globalproject_id;
 
1338   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1340   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1341   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1342   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1343   $new_attr{custom_variables} = [];
 
1345   $item->assign_attributes(%new_attr);
 
1350 sub setup_order_from_cv {
 
1353   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id));
 
1355   $order->intnotes($order->customervendor->notes);
 
1357   if ($order->is_sales) {
 
1358     $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1359     $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1360                         ? $order->customer->taxincluded_checked
 
1361                         : $::myconfig{taxincluded_checked});
 
1366 # recalculate prices and taxes
 
1368 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1372   # bb: todo: currency later
 
1373   $self->order->currency_id($::instance_conf->get_currency_id());
 
1375   my %pat = $self->order->calculate_prices_and_taxes();
 
1376   $self->{taxes} = [];
 
1377   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
 
1378     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
 
1380     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
 
1381     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
 
1382                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
 
1386   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
 
1389 # get data for saving, printing, ..., that is not changed in the form
 
1391 # Only cvars for now.
 
1392 sub get_unalterable_data {
 
1395   foreach my $item (@{ $self->order->items }) {
 
1396     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1397     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1398     foreach my $var (@{ $item->cvars_by_config }) {
 
1399       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1401     $item->parse_custom_variable_values;
 
1407 # And remove related files in the spool directory
 
1412   my $db     = $self->order->db;
 
1414   $db->with_transaction(
 
1416       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1417       $self->order->delete;
 
1418       my $spool = $::lx_office_conf{paths}->{spool};
 
1419       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1422   }) || push(@{$errors}, $db->error);
 
1429 # And delete items that are deleted in the form.
 
1434   my $db     = $self->order->db;
 
1436   $db->with_transaction(sub {
 
1437     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1438     $self->order->save(cascade => 1);
 
1441     if ($::form->{converted_from_oe_id}) {
 
1442       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1443       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1444         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1445         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1446         $src->link_to_record($self->order);
 
1448       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1450         foreach (@{ $self->order->items_sorted }) {
 
1451           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1453           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1454                                   from_id    => $from_id,
 
1455                                   to_table   => 'orderitems',
 
1463   }) || push(@{$errors}, $db->error);
 
1468 sub workflow_sales_or_purchase_order {
 
1471   my $errors = $self->save();
 
1473   if (scalar @{ $errors }) {
 
1474     $self->js->flash('error', $_) foreach @{ $errors };
 
1475     return $self->js->render();
 
1479   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1480                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1481                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1482                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1485   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1486   $self->{converted_from_oe_id} = delete $::form->{id};
 
1488   # set item ids to new fake id, to identify them as new items
 
1489   foreach my $item (@{$self->order->items_sorted}) {
 
1490     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1494   $::form->{type} = $destination_type;
 
1495   $self->type($self->init_type);
 
1496   $self->cv  ($self->init_cv);
 
1500   $self->get_unalterable_data();
 
1501   $self->pre_render();
 
1503   # trigger rendering values for second row/longdescription as hidden,
 
1504   # because they are loaded only on demand. So we need to keep the values
 
1506   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1507   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1511     title => $self->get_title_for('edit'),
 
1512     %{$self->{template_args}}
 
1520   $self->{all_taxzones}             = SL::DB::Manager::TaxZone->get_all_sorted();
 
1521   $self->{all_departments}          = SL::DB::Manager::Department->get_all_sorted();
 
1522   $self->{all_employees}            = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1525   $self->{all_salesmen}             = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1528   $self->{all_payment_terms}        = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1530   $self->{all_delivery_terms}       = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1531   $self->{current_employee_id}      = SL::DB::Manager::Employee->current->id;
 
1532   $self->{periodic_invoices_status} = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1533   $self->{order_probabilities}      = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1535   my $print_form = Form->new('');
 
1536   $print_form->{type}      = $self->type;
 
1537   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
 
1538   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
 
1539   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
 
1540     form => $print_form,
 
1541     options => {dialog_name_prefix => 'print_options.',
 
1545                 no_opendocument    => 0,
 
1549   foreach my $item (@{$self->order->orderitems}) {
 
1550     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1551     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1552     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1555   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1556     # calculate shipped qtys here to prevent calling calculate for every item via the items method
 
1557     SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects;
 
1560   if ($self->order->number && $::instance_conf->get_webdav) {
 
1561     my $webdav = SL::Webdav->new(
 
1562       type     => $self->type,
 
1563       number   => $self->order->number,
 
1565     my @all_objects = $webdav->get_all_objects;
 
1566     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1568                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1572   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config calculate_qty);
 
1573   $self->setup_edit_action_bar;
 
1576 sub setup_edit_action_bar {
 
1577   my ($self, %params) = @_;
 
1579   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
1580                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1581                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1583   for my $bar ($::request->layout->get('actionbar')) {
 
1588           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1589                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
1591           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1595           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1596           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1597           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1599       ], # end of combobox "Save"
 
1606           t8('Save and Sales Order'),
 
1607           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
1608           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
1609           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1612           t8('Save and Purchase Order'),
 
1613           submit   => [ '#order_form', { action => "Order/purchase_order" } ],
 
1614           only_if  => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
1615           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1618           t8('Save and Delivery Order'),
 
1619           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
1620                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1622           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1623           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
 
1626           t8('Save and Invoice'),
 
1627           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1628           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1630       ], # end of combobox "Workflow"
 
1637           t8('Save and print'),
 
1638           call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ],
 
1641           t8('Save and E-mail'),
 
1642           call => [ 'kivi.Order.email', $::instance_conf->get_order_warn_duplicate_parts ],
 
1645           t8('Download attachments of all parts'),
 
1646           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1647           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1648           only_if  => $::instance_conf->get_doc_storage,
 
1650       ], # end of combobox "Export"
 
1654         call     => [ 'kivi.Order.delete_order' ],
 
1655         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1656         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1657         only_if  => $deletion_allowed,
 
1664   my ($order, $pdf_ref, $params) = @_;
 
1668   my $print_form = Form->new('');
 
1669   $print_form->{type}        = $order->type;
 
1670   $print_form->{formname}    = $params->{formname} || $order->type;
 
1671   $print_form->{format}      = $params->{format}   || 'pdf';
 
1672   $print_form->{media}       = $params->{media}    || 'file';
 
1673   $print_form->{groupitems}  = $params->{groupitems};
 
1674   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
1676   $order->language($params->{language});
 
1677   $order->flatten_to_form($print_form, format_amounts => 1);
 
1681   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
1682     $template_ext  = 'odt';
 
1683     $template_type = 'OpenDocument';
 
1686   # search for the template
 
1687   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
1688     name        => $print_form->{formname},
 
1689     extension   => $template_ext,
 
1690     email       => $print_form->{media} eq 'email',
 
1691     language    => $params->{language},
 
1692     printer_id  => $print_form->{printer_id},  # todo
 
1695   if (!defined $template_file) {
 
1696     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);
 
1699   return @errors if scalar @errors;
 
1701   $print_form->throw_on_error(sub {
 
1703       $print_form->prepare_for_printing;
 
1705       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
1706         format        => $print_form->{format},
 
1707         template_type => $template_type,
 
1708         template      => $template_file,
 
1709         variables     => $print_form,
 
1710         variable_content_types => {
 
1711           longdescription => 'html',
 
1712           partnotes       => 'html',
 
1717     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
 
1723 sub get_files_for_email_dialog {
 
1726   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
1728   return %files if !$::instance_conf->get_doc_storage;
 
1730   if ($self->order->id) {
 
1731     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
1732     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
1733     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
1737     uniq_by { $_->{id} }
 
1739       +{ id         => $_->part->id,
 
1740          partnumber => $_->part->partnumber }
 
1741     } @{$self->order->items_sorted};
 
1743   foreach my $part (@parts) {
 
1744     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
1745     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
1748   foreach my $key (keys %files) {
 
1749     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
1755 sub make_periodic_invoices_config_from_yaml {
 
1756   my ($yaml_config) = @_;
 
1758   return if !$yaml_config;
 
1759   my $attr = YAML::Load($yaml_config);
 
1760   return if 'HASH' ne ref $attr;
 
1761   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
1765 sub get_periodic_invoices_status {
 
1766   my ($self, $config) = @_;
 
1768   return                      if $self->type ne sales_order_type();
 
1769   return t8('not configured') if !$config;
 
1771   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
1772              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
1773              :                                                     die "Cannot get status of periodic invoices config";
 
1775   return $active ? t8('active') : t8('inactive');
 
1779   my ($self, $action) = @_;
 
1781   return '' if none { lc($action)} qw(add edit);
 
1784   # $::locale->text("Add Sales Order");
 
1785   # $::locale->text("Add Purchase Order");
 
1786   # $::locale->text("Add Quotation");
 
1787   # $::locale->text("Add Request for Quotation");
 
1788   # $::locale->text("Edit Sales Order");
 
1789   # $::locale->text("Edit Purchase Order");
 
1790   # $::locale->text("Edit Quotation");
 
1791   # $::locale->text("Edit Request for Quotation");
 
1793   $action = ucfirst(lc($action));
 
1794   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
1795        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
1796        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
1797        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
1801 sub sales_order_type {
 
1805 sub purchase_order_type {
 
1809 sub sales_quotation_type {
 
1813 sub request_quotation_type {
 
1814   'request_quotation';
 
1818   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
1819        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
1820        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
1821        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
1833 SL::Controller::Order - controller for orders
 
1837 This is a new form to enter orders, completely rewritten with the use
 
1838 of controller and java script techniques.
 
1840 The aim is to provide the user a better expirience and a faster flow
 
1841 of work. Also the code should be more readable, more reliable and
 
1850 One input row, so that input happens every time at the same place.
 
1854 Use of pickers where possible.
 
1858 Possibility to enter more than one item at once.
 
1862 Item list in a scrollable area, so that the workflow buttons stay at
 
1867 Reordering item rows with drag and drop is possible. Sorting item rows is
 
1868 possible (by partnumber, description, qty, sellprice and discount for now).
 
1872 No C<update> is necessary. All entries and calculations are managed
 
1873 with ajax-calls and the page does only reload on C<save>.
 
1877 User can see changes immediately, because of the use of java script
 
1888 =item * C<SL/Controller/Order.pm>
 
1892 =item * C<template/webpages/order/form.html>
 
1896 =item * C<template/webpages/order/tabs/basic_data.html>
 
1898 Main tab for basic_data.
 
1900 This is the only tab here for now. "linked records" and "webdav" tabs are
 
1901 reused from generic code.
 
1905 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
1907 For displaying information on business type
 
1909 =item * C<template/webpages/order/tabs/_item_input.html>
 
1911 The input line for items
 
1913 =item * C<template/webpages/order/tabs/_row.html>
 
1915 One row for already entered items
 
1917 =item * C<template/webpages/order/tabs/_tax_row.html>
 
1919 Displaying tax information
 
1921 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
 
1923 Dialog for entering more than one item at once
 
1925 =item * C<template/webpages/order/tabs/_multi_items_result.html>
 
1927 Results for the filter in the multi items dialog
 
1929 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
1931 Dialog for selecting price and discount sources
 
1935 =item * C<js/kivi.Order.js>
 
1937 java script functions
 
1949 =item * credit limit
 
1951 =item * more workflows (quotation, rfq)
 
1953 =item * price sources: little symbols showing better price / better discount
 
1955 =item * select units in input row?
 
1957 =item * custom shipto address
 
1959 =item * check for direct delivery (workflow sales order -> purchase order)
 
1961 =item * language / part translations
 
1963 =item * access rights
 
1965 =item * display weights
 
1971 =item * optional client/user behaviour
 
1973 (transactions has to be set - department has to be set -
 
1974  force project if enabled in client config - transport cost reminder)
 
1978 =head1 KNOWN BUGS AND CAVEATS
 
1984 Customer discount is not displayed as a valid discount in price source popup
 
1985 (this might be a bug in price sources)
 
1987 (I cannot reproduce this (Bernd))
 
1991 No indication that <shift>-up/down expands/collapses second row.
 
1995 Inline creation of parts is not currently supported
 
1999 Table header is not sticky in the scrolling area.
 
2003 Sorting does not include C<position>, neither does reordering.
 
2005 This behavior was implemented intentionally. But we can discuss, which behavior
 
2006 should be implemented.
 
2010 C<show_multi_items_dialog> does not use the currently inserted string for
 
2015 The language selected in print or email dialog is not saved when the order is saved.
 
2019 =head1 To discuss / Nice to have
 
2025 How to expand/collapse second row. Now it can be done clicking the icon or
 
2030 Possibility to change longdescription in input row?
 
2034 Possibility to select PriceSources in input row?
 
2038 This controller uses a (changed) copy of the template for the PriceSource
 
2039 dialog. Maybe there could be used one code source.
 
2043 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2044 form. This is not only a problem here, but also in all parts using the PTC.
 
2045 There exists a ticket and a patch. This patch should be testet.
 
2049 An indicator, if the actual inputs are saved (like in an
 
2050 editor or on text processing application).
 
2054 A warning when leaving the page without saveing unchanged inputs.
 
2061 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>