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->val('#id', $self->order->id)
 
 256            ->val('#order_' . $self->nr_key(), $self->order->number);
 
 258   my $format      = $::form->{print_options}->{format};
 
 259   my $media       = $::form->{print_options}->{media};
 
 260   my $formname    = $::form->{print_options}->{formname};
 
 261   my $copies      = $::form->{print_options}->{copies};
 
 262   my $groupitems  = $::form->{print_options}->{groupitems};
 
 264   # only pdf and opendocument by now
 
 265   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
 
 266     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 269   # only screen or printer by now
 
 270   if (none { $media eq $_ } qw(screen printer)) {
 
 271     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 275   $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 277   # create a form for generate_attachment_filename
 
 278   my $form   = Form->new;
 
 279   $form->{$self->nr_key()}  = $self->order->number;
 
 280   $form->{type}             = $self->type;
 
 281   $form->{format}           = $format;
 
 282   $form->{formname}         = $formname;
 
 283   $form->{language}         = '_' . $language->template_code if $language;
 
 284   my $pdf_filename          = $form->generate_attachment_filename();
 
 287   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
 
 288                                                    formname   => $formname,
 
 289                                                    language   => $language,
 
 290                                                    groupitems => $groupitems });
 
 291   if (scalar @errors) {
 
 292     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 295   if ($media eq 'screen') {
 
 297     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 298     $sfile->fh->print($pdf);
 
 301     my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 302     $::auth->set_session_value("Order::print-${key}" => $sfile->file_name);
 
 305     ->run('kivi.Order.download_pdf', $pdf_filename, $key)
 
 306     ->flash('info', t8('The PDF has been created'));
 
 308   } elsif ($media eq 'printer') {
 
 310     my $printer_id = $::form->{print_options}->{printer_id};
 
 311     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 316     $self->js->flash('info', t8('The PDF has been printed'));
 
 319   # copy file to webdav folder
 
 320   if ($self->order->number && $::instance_conf->get_webdav_documents) {
 
 321     my $webdav = SL::Webdav->new(
 
 323       number   => $self->order->number,
 
 325     my $webdav_file = SL::Webdav::File->new(
 
 327       filename => $pdf_filename,
 
 330       $webdav_file->store(data => \$pdf);
 
 333       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
 
 336   if ($self->order->number && $::instance_conf->get_doc_storage) {
 
 338       SL::File->save(object_id     => $self->order->id,
 
 339                      object_type   => $self->type,
 
 340                      mime_type     => 'application/pdf',
 
 342                      file_type     => 'document',
 
 343                      file_name     => $pdf_filename,
 
 344                      file_contents => $pdf);
 
 347       $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
 
 353 # offer pdf for download
 
 355 # It needs to get the key for the session value to get the pdf file.
 
 356 sub action_download_pdf {
 
 359   my $key = $::form->{key};
 
 360   my $tmp_filename = $::auth->get_session_value("Order::print-${key}");
 
 361   return $self->send_file(
 
 363     type => 'application/pdf',
 
 364     name => $::form->{pdf_filename},
 
 368 # open the email dialog
 
 369 sub action_show_email_dialog {
 
 372   my $cv_method = $self->cv;
 
 374   if (!$self->order->$cv_method) {
 
 375     return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
 
 380   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 381   $email_form->{to} ||= $self->order->$cv_method->email;
 
 382   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 383   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 384   # Todo: get addresses from shipto, if any
 
 386   my $form = Form->new;
 
 387   $form->{$self->nr_key()}  = $self->order->number;
 
 388   $form->{formname}         = $self->type;
 
 389   $form->{type}             = $self->type;
 
 390   $form->{language}         = 'de';
 
 391   $form->{format}           = 'pdf';
 
 393   $email_form->{subject}             = $form->generate_email_subject();
 
 394   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 395   $email_form->{message}             = $form->generate_email_body();
 
 396   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 398   my %files = $self->get_files_for_email_dialog();
 
 399   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 400                                   email_form  => $email_form,
 
 401                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
 
 403                                   is_customer => $self->cv eq 'customer',
 
 407       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 414 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 415 sub action_send_email {
 
 418   my $errors = $self->save();
 
 420   if (scalar @{ $errors }) {
 
 421     $self->js->run('kivi.Order.close_email_dialog');
 
 422     $self->js->flash('error', $_) foreach @{ $errors };
 
 423     return $self->js->render();
 
 426   $self->js->val('#id', $self->order->id)
 
 427            ->val('#order_' . $self->nr_key(), $self->order->number);
 
 429   my $email_form  = delete $::form->{email_form};
 
 430   my %field_names = (to => 'email');
 
 432   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 434   # for Form::cleanup which may be called in Form::send_email
 
 435   $::form->{cwd}    = getcwd();
 
 436   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 438   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 439   $::form->{media}  = 'email';
 
 441   if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
 
 443     $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 446     my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
 
 447                                                     format     => $::form->{print_options}->{format},
 
 448                                                     formname   => $::form->{print_options}->{formname},
 
 449                                                     language   => $language,
 
 450                                                     groupitems => $::form->{print_options}->{groupitems}});
 
 451     if (scalar @errors) {
 
 452       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
 
 455     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 456     $sfile->fh->print($pdf);
 
 459     $::form->{tmpfile} = $sfile->file_name;
 
 460     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 463   $::form->send_email(\%::myconfig, 'pdf');
 
 466   my $intnotes = $self->order->intnotes;
 
 467   $intnotes   .= "\n\n" if $self->order->intnotes;
 
 468   $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 469   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 470   $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 471   $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 472   $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 473   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 474   $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
 476   $self->order->update_attributes(intnotes => $intnotes);
 
 479       ->val('#order_intnotes', $intnotes)
 
 480       ->run('kivi.Order.close_email_dialog')
 
 481       ->flash('info', t8('The email has been sent.'))
 
 485 # open the periodic invoices config dialog
 
 487 # If there are values in the form (i.e. dialog was opened before),
 
 488 # then use this values. Create new ones, else.
 
 489 sub action_show_periodic_invoices_config_dialog {
 
 492   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 493   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 494   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 495                                                    order_value_periodicity => 'p', # = same as periodicity
 
 496                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 497                                                    extend_automatically_by => 12,
 
 499                                                    email_subject           => GenericTranslations->get(
 
 500                                                                                 language_id      => $::form->{language_id},
 
 501                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 502                                                    email_body              => GenericTranslations->get(
 
 503                                                                                 language_id      => $::form->{language_id},
 
 504                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 506   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 507   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 509   $::form->get_lists(printers => "ALL_PRINTERS",
 
 510                      charts   => { key       => 'ALL_CHARTS',
 
 511                                    transdate => 'current_date' });
 
 513   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 515   if ($::form->{customer_id}) {
 
 516     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 517     $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail;
 
 520   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 522                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 523                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 528 # assign the values of the periodic invoices config dialog
 
 529 # as yaml in the hidden tag and set the status.
 
 530 sub action_assign_periodic_invoices_config {
 
 533   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 535   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 536                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 537                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 538                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 539                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 540                  start_date_as_date         => $::form->{start_date_as_date},
 
 541                  end_date_as_date           => $::form->{end_date_as_date},
 
 542                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 543                  print                      => $::form->{print}      ? 1                         : 0,
 
 544                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 545                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 546                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 547                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 548                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 549                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 550                  email_recipient_address    => $::form->{email_recipient_address},
 
 551                  email_sender               => $::form->{email_sender},
 
 552                  email_subject              => $::form->{email_subject},
 
 553                  email_body                 => $::form->{email_body},
 
 556   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 558   my $status = $self->get_periodic_invoices_status($config);
 
 561     ->remove('#order_periodic_invoices_config')
 
 562     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 563     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 564     ->html('#periodic_invoices_status', $status)
 
 565     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 569 sub action_get_has_active_periodic_invoices {
 
 572   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 573   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 575   my $has_active_periodic_invoices =
 
 576        $self->type eq sales_order_type()
 
 579     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 580     && $config->get_previous_billed_period_start_date;
 
 582   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 585 # save the order and redirect to the frontend subroutine for a new
 
 587 sub action_save_and_delivery_order {
 
 590   my $errors = $self->save();
 
 592   if (scalar @{ $errors }) {
 
 593     $self->js->flash('error', $_) foreach @{ $errors };
 
 594     return $self->js->render();
 
 597   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 598            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 599            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 600            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 602   flash_later('info', $text);
 
 604   my @redirect_params = (
 
 605     controller => 'oe.pl',
 
 606     action     => 'oe_delivery_order_from_order',
 
 607     id         => $self->order->id,
 
 610   $self->redirect_to(@redirect_params);
 
 613 # save the order and redirect to the frontend subroutine for a new
 
 615 sub action_save_and_invoice {
 
 618   my $errors = $self->save();
 
 620   if (scalar @{ $errors }) {
 
 621     $self->js->flash('error', $_) foreach @{ $errors };
 
 622     return $self->js->render();
 
 625   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 626            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 627            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 628            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 630   flash_later('info', $text);
 
 632   my @redirect_params = (
 
 633     controller => 'oe.pl',
 
 634     action     => 'oe_invoice_from_order',
 
 635     id         => $self->order->id,
 
 638   $self->redirect_to(@redirect_params);
 
 641 # workflow from sales quotation to sales order
 
 642 sub action_sales_order {
 
 643   $_[0]->workflow_sales_or_purchase_order();
 
 646 # workflow from rfq to purchase order
 
 647 sub action_purchase_order {
 
 648   $_[0]->workflow_sales_or_purchase_order();
 
 651 # set form elements in respect to a changed customer or vendor
 
 653 # This action is called on an change of the customer/vendor picker.
 
 654 sub action_customer_vendor_changed {
 
 657   setup_order_from_cv($self->order);
 
 660   my $cv_method = $self->cv;
 
 662   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 663     $self->js->show('#cp_row');
 
 665     $self->js->hide('#cp_row');
 
 668   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 669     $self->js->show('#shipto_row');
 
 671     $self->js->hide('#shipto_row');
 
 674   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 677     ->replaceWith('#order_cp_id',            $self->build_contact_select)
 
 678     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
 
 679     ->replaceWith('#business_info_row',      $self->build_business_info_row)
 
 680     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
 
 681     ->val(        '#order_taxincluded',      $self->order->taxincluded)
 
 682     ->val(        '#order_payment_id',       $self->order->payment_id)
 
 683     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
 
 684     ->val(        '#order_intnotes',         $self->order->intnotes)
 
 685     ->val(        '#language_id',            $self->order->$cv_method->language_id)
 
 686     ->focus(      '#order_' . $self->cv . '_id');
 
 688   $self->js_redisplay_amounts_and_taxes;
 
 692 # open the dialog for customer/vendor details
 
 693 sub action_show_customer_vendor_details_dialog {
 
 696   my $is_customer = 'customer' eq $::form->{vc};
 
 699     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 701     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 704   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 705   $details{discount_as_percent} = $cv->discount_as_percent;
 
 706   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 707   $details{business}            = $cv->business->description      if $cv->business;
 
 708   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 709   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 710   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 711   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 713   foreach my $entry (@{ $cv->shipto }) {
 
 714     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 716   foreach my $entry (@{ $cv->contacts }) {
 
 717     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 720   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 721                 is_customer => $is_customer,
 
 726 # called if a unit in an existing item row is changed
 
 727 sub action_unit_changed {
 
 730   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 731   my $item = $self->order->items_sorted->[$idx];
 
 733   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 734   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 739     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 740   $self->js_redisplay_line_values;
 
 741   $self->js_redisplay_amounts_and_taxes;
 
 745 # add an item row for a new item entered in the input row
 
 746 sub action_add_item {
 
 749   my $form_attr = $::form->{add_item};
 
 751   return unless $form_attr->{parts_id};
 
 753   my $item = new_item($self->order, $form_attr);
 
 755   $self->order->add_items($item);
 
 759   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 760   my $row_as_html = $self->p->render('order/tabs/_row',
 
 764                                      ALL_PRICE_FACTORS => $self->all_price_factors
 
 768     ->append('#row_table_id', $row_as_html);
 
 770   if ( $item->part->is_assortment ) {
 
 771     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 772     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 773       my $attr = { parts_id => $assortment_item->parts_id,
 
 774                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 775                    unit     => $assortment_item->unit,
 
 776                    description => $assortment_item->part->description,
 
 778       my $item = new_item($self->order, $attr);
 
 780       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 781       $item->discount(1) unless $assortment_item->charge;
 
 783       $self->order->add_items( $item );
 
 785       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 786       my $row_as_html = $self->p->render('order/tabs/_row',
 
 790                                          ALL_PRICE_FACTORS => $self->all_price_factors
 
 793         ->append('#row_table_id', $row_as_html);
 
 798     ->val('.add_item_input', '')
 
 799     ->run('kivi.Order.init_row_handlers')
 
 800     ->run('kivi.Order.row_table_scroll_down')
 
 801     ->run('kivi.Order.renumber_positions')
 
 802     ->focus('#add_item_parts_id_name');
 
 804   $self->js_redisplay_amounts_and_taxes;
 
 808 # open the dialog for entering multiple items at once
 
 809 sub action_show_multi_items_dialog {
 
 810   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
 
 811                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
 
 814 # update the filter results in the multi item dialog
 
 815 sub action_multi_items_update_result {
 
 818   $::form->{multi_items}->{filter}->{obsolete} = 0;
 
 820   my $count = $_[0]->multi_items_models->count;
 
 823     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
 
 824     $_[0]->render($text, { layout => 0 });
 
 825   } elsif ($count > $max_count) {
 
 826     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
 
 827     $_[0]->render($text, { layout => 0 });
 
 829     my $multi_items = $_[0]->multi_items_models->get;
 
 830     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
 
 831                   multi_items => $multi_items);
 
 835 # add item rows for multiple items at once
 
 836 sub action_add_multi_items {
 
 839   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
 
 840   return $self->js->render() unless scalar @form_attr;
 
 843   foreach my $attr (@form_attr) {
 
 844     my $item = new_item($self->order, $attr);
 
 846     if ( $item->part->is_assortment ) {
 
 847       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 848         my $attr = { parts_id => $assortment_item->parts_id,
 
 849                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 850                      unit     => $assortment_item->unit,
 
 851                      description => $assortment_item->part->description,
 
 853         my $item = new_item($self->order, $attr);
 
 855         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 856         $item->discount(1) unless $assortment_item->charge;
 
 861   $self->order->add_items(@items);
 
 865   foreach my $item (@items) {
 
 866     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 867     my $row_as_html = $self->p->render('order/tabs/_row',
 
 871                                        ALL_PRICE_FACTORS => $self->all_price_factors
 
 874     $self->js->append('#row_table_id', $row_as_html);
 
 878     ->run('kivi.Order.close_multi_items_dialog')
 
 879     ->run('kivi.Order.init_row_handlers')
 
 880     ->run('kivi.Order.row_table_scroll_down')
 
 881     ->run('kivi.Order.renumber_positions')
 
 882     ->focus('#add_item_parts_id_name');
 
 884   $self->js_redisplay_amounts_and_taxes;
 
 888 # recalculate all linetotals, amounts and taxes and redisplay them
 
 889 sub action_recalc_amounts_and_taxes {
 
 894   $self->js_redisplay_line_values;
 
 895   $self->js_redisplay_amounts_and_taxes;
 
 899 # redisplay item rows if they are sorted by an attribute
 
 900 sub action_reorder_items {
 
 904     partnumber  => sub { $_[0]->part->partnumber },
 
 905     description => sub { $_[0]->description },
 
 906     qty         => sub { $_[0]->qty },
 
 907     sellprice   => sub { $_[0]->sellprice },
 
 908     discount    => sub { $_[0]->discount },
 
 911   my $method = $sort_keys{$::form->{order_by}};
 
 912   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
 913   if ($::form->{sort_dir}) {
 
 914     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 915       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
 917       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 920     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 921       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
 923       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 927     ->run('kivi.Order.redisplay_items', \@to_sort)
 
 931 # show the popup to choose a price/discount source
 
 932 sub action_price_popup {
 
 935   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 936   my $item = $self->order->items_sorted->[$idx];
 
 938   $self->render_price_dialog($item);
 
 941 # get the longdescription for an item if the dialog to enter/change the
 
 942 # longdescription was opened and the longdescription is empty
 
 944 # If this item is new, get the longdescription from Part.
 
 945 # Otherwise get it from OrderItem.
 
 946 sub action_get_item_longdescription {
 
 949   if ($::form->{item_id}) {
 
 950     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
 
 951   } elsif ($::form->{parts_id}) {
 
 952     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
 
 954   $_[0]->render(\ $longdescription, { type => 'text' });
 
 957 # load the second row for one or more items
 
 959 # This action gets the html code for all items second rows by rendering a template for
 
 960 # the second row and sets the html code via client js.
 
 961 sub action_load_second_rows {
 
 964   $self->recalc() if $self->order->is_sales; # for margin calculation
 
 966   foreach my $item_id (@{ $::form->{item_ids} }) {
 
 967     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
 968     my $item = $self->order->items_sorted->[$idx];
 
 970     $self->js_load_second_row($item, $item_id, 0);
 
 973   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
 978 sub js_load_second_row {
 
 979   my ($self, $item, $item_id, $do_parse) = @_;
 
 982     # Parse values from form (they are formated while rendering (template)).
 
 983     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
 984     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
 985     foreach my $var (@{ $item->cvars_by_config }) {
 
 986       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
 988     $item->parse_custom_variable_values;
 
 991   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
 994     ->html('#second_row_' . $item_id, $row_as_html)
 
 995     ->data('#second_row_' . $item_id, 'loaded', 1);
 
 998 sub js_redisplay_line_values {
 
1001   my $is_sales = $self->order->is_sales;
 
1003   # sales orders with margins
 
1008        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1009        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1010        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1011       ]} @{ $self->order->items_sorted };
 
1015        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1016       ]} @{ $self->order->items_sorted };
 
1020     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1023 sub js_redisplay_amounts_and_taxes {
 
1026   if (scalar @{ $self->{taxes} }) {
 
1027     $self->js->show('#taxincluded_row_id');
 
1029     $self->js->hide('#taxincluded_row_id');
 
1032   if ($self->order->taxincluded) {
 
1033     $self->js->hide('#subtotal_row_id');
 
1035     $self->js->show('#subtotal_row_id');
 
1038   if ($self->order->is_sales) {
 
1039     my $is_neg = $self->order->marge_total < 0;
 
1041       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1042       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1043       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1044       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1045       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1046       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1047       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1048       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1052     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1053     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1054     ->remove('.tax_row')
 
1055     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1062 sub init_valid_types {
 
1063   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1069   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1070     die "Not a valid type for order";
 
1073   $self->type($::form->{type});
 
1079   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1080          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1081          : die "Not a valid type for order";
 
1094 # model used to filter/display the parts in the multi-items dialog
 
1095 sub init_multi_items_models {
 
1096   SL::Controller::Helper::GetModels->new(
 
1097     controller     => $_[0],
 
1099     with_objects   => [ qw(unit_obj) ],
 
1100     disable_plugin => 'paginated',
 
1101     source         => $::form->{multi_items},
 
1107       partnumber  => t8('Partnumber'),
 
1108       description => t8('Description')}
 
1112 sub init_all_price_factors {
 
1113   SL::DB::Manager::PriceFactor->get_all;
 
1119   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1121   my $right   = $right_for->{ $self->type };
 
1122   $right    ||= 'DOES_NOT_EXIST';
 
1124   $::auth->assert($right);
 
1127 # build the selection box for contacts
 
1129 # Needed, if customer/vendor changed.
 
1130 sub build_contact_select {
 
1133   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1134     value_key  => 'cp_id',
 
1135     title_key  => 'full_name_dep',
 
1136     default    => $self->order->cp_id,
 
1138     style      => 'width: 300px',
 
1142 # build the selection box for shiptos
 
1144 # Needed, if customer/vendor changed.
 
1145 sub build_shipto_select {
 
1148   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
 
1149     value_key  => 'shipto_id',
 
1150     title_key  => 'displayable_id',
 
1151     default    => $self->order->shipto_id,
 
1153     style      => 'width: 300px',
 
1157 # render the info line for business
 
1159 # Needed, if customer/vendor changed.
 
1160 sub build_business_info_row
 
1162   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1165 # build the rows for displaying taxes
 
1167 # Called if amounts where recalculated and redisplayed.
 
1168 sub build_tax_rows {
 
1172   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1173     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1175   return $rows_as_html;
 
1179 sub render_price_dialog {
 
1180   my ($self, $record_item) = @_;
 
1182   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1186       'kivi.io.price_chooser_dialog',
 
1187       t8('Available Prices'),
 
1188       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1193 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1194 #     $self->js->show('#dialog_flash_error');
 
1203   return if !$::form->{id};
 
1205   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1208 # load or create a new order object
 
1210 # And assign changes from the form to this object.
 
1211 # If the order is loaded from db, check if items are deleted in the form,
 
1212 # remove them form the object and collect them for removing from db on saving.
 
1213 # Then create/update items from form (via make_item) and add them.
 
1217   # add_items adds items to an order with no items for saving, but they cannot
 
1218   # be retrieved via items until the order is saved. Adding empty items to new
 
1219   # order here solves this problem.
 
1221   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1222   $order ||= SL::DB::Order->new(orderitems => [],
 
1223                                 quotation  => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())));
 
1225   my $cv_id_method = $self->cv . '_id';
 
1226   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1227     $order->$cv_id_method($::form->{$cv_id_method});
 
1228     setup_order_from_cv($order);
 
1231   my $form_orderitems               = delete $::form->{order}->{orderitems};
 
1232   my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
 
1234   $order->assign_attributes(%{$::form->{order}});
 
1236   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1237     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1238     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1241   # remove deleted items
 
1242   $self->item_ids_to_delete([]);
 
1243   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1244     my $item = $order->orderitems->[$idx];
 
1245     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1246       splice @{$order->orderitems}, $idx, 1;
 
1247       push @{$self->item_ids_to_delete}, $item->id;
 
1253   foreach my $form_attr (@{$form_orderitems}) {
 
1254     my $item = make_item($order, $form_attr);
 
1255     $item->position($pos);
 
1259   $order->add_items(grep {!$_->id} @items);
 
1264 # create or update items from form
 
1266 # Make item objects from form values. For items already existing read from db.
 
1267 # Create a new item else. And assign attributes.
 
1269   my ($record, $attr) = @_;
 
1272   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1274   my $is_new = !$item;
 
1276   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1277   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1278   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1279   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1281   $item->assign_attributes(%$attr);
 
1282   $item->longdescription($item->part->notes)                     if $is_new && !defined $attr->{longdescription};
 
1283   $item->project_id($record->globalproject_id)                   if $is_new && !defined $attr->{project_id};
 
1284   $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
 
1291 # This is used to add one item
 
1293   my ($record, $attr) = @_;
 
1295   my $item = SL::DB::OrderItem->new;
 
1297   # Remove attributes where the user left or set the inputs empty.
 
1298   # So these attributes will be undefined and we can distinguish them
 
1299   # from zero later on.
 
1300   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1301     delete $attr->{$_} if $attr->{$_} eq '';
 
1304   $item->assign_attributes(%$attr);
 
1306   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1307   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1309   $item->unit($part->unit) if !$item->unit;
 
1312   if ( $part->is_assortment ) {
 
1313     # add assortment items with price 0, as the components carry the price
 
1314     $price_src = $price_source->price_from_source("");
 
1315     $price_src->price(0);
 
1316   } elsif (defined $item->sellprice) {
 
1317     $price_src = $price_source->price_from_source("");
 
1318     $price_src->price($item->sellprice);
 
1320     $price_src = $price_source->best_price
 
1321            ? $price_source->best_price
 
1322            : $price_source->price_from_source("");
 
1323     $price_src->price(0) if !$price_source->best_price;
 
1327   if (defined $item->discount) {
 
1328     $discount_src = $price_source->discount_from_source("");
 
1329     $discount_src->discount($item->discount);
 
1331     $discount_src = $price_source->best_discount
 
1332                   ? $price_source->best_discount
 
1333                   : $price_source->discount_from_source("");
 
1334     $discount_src->discount(0) if !$price_source->best_discount;
 
1338   $new_attr{part}                   = $part;
 
1339   $new_attr{description}            = $part->description     if ! $item->description;
 
1340   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1341   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1342   $new_attr{sellprice}              = $price_src->price;
 
1343   $new_attr{discount}               = $discount_src->discount;
 
1344   $new_attr{active_price_source}    = $price_src;
 
1345   $new_attr{active_discount_source} = $discount_src;
 
1346   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1347   $new_attr{project_id}             = $record->globalproject_id;
 
1348   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1350   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1351   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1352   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1353   $new_attr{custom_variables} = [];
 
1355   $item->assign_attributes(%new_attr);
 
1360 sub setup_order_from_cv {
 
1363   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id));
 
1365   $order->intnotes($order->customervendor->notes);
 
1367   if ($order->is_sales) {
 
1368     $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1369     $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1370                         ? $order->customer->taxincluded_checked
 
1371                         : $::myconfig{taxincluded_checked});
 
1376 # recalculate prices and taxes
 
1378 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1382   # bb: todo: currency later
 
1383   $self->order->currency_id($::instance_conf->get_currency_id());
 
1385   my %pat = $self->order->calculate_prices_and_taxes();
 
1386   $self->{taxes} = [];
 
1387   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
 
1388     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
 
1390     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
 
1391     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
 
1392                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
 
1396   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1399 # get data for saving, printing, ..., that is not changed in the form
 
1401 # Only cvars for now.
 
1402 sub get_unalterable_data {
 
1405   foreach my $item (@{ $self->order->items }) {
 
1406     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1407     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1408     foreach my $var (@{ $item->cvars_by_config }) {
 
1409       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1411     $item->parse_custom_variable_values;
 
1417 # And remove related files in the spool directory
 
1422   my $db     = $self->order->db;
 
1424   $db->with_transaction(
 
1426       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1427       $self->order->delete;
 
1428       my $spool = $::lx_office_conf{paths}->{spool};
 
1429       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1432   }) || push(@{$errors}, $db->error);
 
1439 # And delete items that are deleted in the form.
 
1444   my $db     = $self->order->db;
 
1446   $db->with_transaction(sub {
 
1447     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1448     $self->order->save(cascade => 1);
 
1451     if ($::form->{converted_from_oe_id}) {
 
1452       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1453       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1454         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1455         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1456         $src->link_to_record($self->order);
 
1458       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1460         foreach (@{ $self->order->items_sorted }) {
 
1461           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1463           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1464                                   from_id    => $from_id,
 
1465                                   to_table   => 'orderitems',
 
1473   }) || push(@{$errors}, $db->error);
 
1478 sub workflow_sales_or_purchase_order {
 
1482   my $errors = $self->save();
 
1484   if (scalar @{ $errors }) {
 
1485     $self->js->flash('error', $_) foreach @{ $errors };
 
1486     return $self->js->render();
 
1489   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1490                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1491                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1492                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1495   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1496   $self->{converted_from_oe_id} = delete $::form->{id};
 
1498   # set item ids to new fake id, to identify them as new items
 
1499   foreach my $item (@{$self->order->items_sorted}) {
 
1500     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1504   $::form->{type} = $destination_type;
 
1505   $self->type($self->init_type);
 
1506   $self->cv  ($self->init_cv);
 
1510   $self->get_unalterable_data();
 
1511   $self->pre_render();
 
1513   # trigger rendering values for second row/longdescription as hidden,
 
1514   # because they are loaded only on demand. So we need to keep the values
 
1516   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1517   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1521     title => $self->get_title_for('edit'),
 
1522     %{$self->{template_args}}
 
1530   $self->{all_taxzones}             = SL::DB::Manager::TaxZone->get_all_sorted();
 
1531   $self->{all_departments}          = SL::DB::Manager::Department->get_all_sorted();
 
1532   $self->{all_employees}            = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1535   $self->{all_salesmen}             = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1538   $self->{all_payment_terms}        = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1540   $self->{all_delivery_terms}       = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1541   $self->{current_employee_id}      = SL::DB::Manager::Employee->current->id;
 
1542   $self->{periodic_invoices_status} = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1543   $self->{order_probabilities}      = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1545   my $print_form = Form->new('');
 
1546   $print_form->{type}      = $self->type;
 
1547   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
 
1548   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
 
1549   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
 
1550     form => $print_form,
 
1551     options => {dialog_name_prefix => 'print_options.',
 
1555                 no_opendocument    => 0,
 
1559   foreach my $item (@{$self->order->orderitems}) {
 
1560     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1561     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1562     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1565   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1566     # calculate shipped qtys here to prevent calling calculate for every item via the items method
 
1567     SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects;
 
1570   if ($self->order->number && $::instance_conf->get_webdav) {
 
1571     my $webdav = SL::Webdav->new(
 
1572       type     => $self->type,
 
1573       number   => $self->order->number,
 
1575     my @all_objects = $webdav->get_all_objects;
 
1576     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1578                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1582   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config calculate_qty);
 
1583   $self->setup_edit_action_bar;
 
1586 sub setup_edit_action_bar {
 
1587   my ($self, %params) = @_;
 
1589   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
1590                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1591                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1593   for my $bar ($::request->layout->get('actionbar')) {
 
1598           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1599                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
1601           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1605           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1606           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1607           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1609       ], # end of combobox "Save"
 
1616           t8('Save and Sales Order'),
 
1617           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
1618           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
1619           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1622           t8('Save and Purchase Order'),
 
1623           submit   => [ '#order_form', { action => "Order/purchase_order" } ],
 
1624           only_if  => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
1625           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1628           t8('Save and Delivery Order'),
 
1629           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
1630                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1632           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1633           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
 
1636           t8('Save and Invoice'),
 
1637           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1638           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1640       ], # end of combobox "Workflow"
 
1647           t8('Save and print'),
 
1648           call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ],
 
1651           t8('Save and E-mail'),
 
1652           call => [ 'kivi.Order.email', $::instance_conf->get_order_warn_duplicate_parts ],
 
1655           t8('Download attachments of all parts'),
 
1656           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1657           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1658           only_if  => $::instance_conf->get_doc_storage,
 
1660       ], # end of combobox "Export"
 
1664         call     => [ 'kivi.Order.delete_order' ],
 
1665         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1666         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1667         only_if  => $deletion_allowed,
 
1674   my ($order, $pdf_ref, $params) = @_;
 
1678   my $print_form = Form->new('');
 
1679   $print_form->{type}        = $order->type;
 
1680   $print_form->{formname}    = $params->{formname} || $order->type;
 
1681   $print_form->{format}      = $params->{format}   || 'pdf';
 
1682   $print_form->{media}       = $params->{media}    || 'file';
 
1683   $print_form->{groupitems}  = $params->{groupitems};
 
1684   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
1686   $order->language($params->{language});
 
1687   $order->flatten_to_form($print_form, format_amounts => 1);
 
1691   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
1692     $template_ext  = 'odt';
 
1693     $template_type = 'OpenDocument';
 
1696   # search for the template
 
1697   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
1698     name        => $print_form->{formname},
 
1699     extension   => $template_ext,
 
1700     email       => $print_form->{media} eq 'email',
 
1701     language    => $params->{language},
 
1702     printer_id  => $print_form->{printer_id},  # todo
 
1705   if (!defined $template_file) {
 
1706     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);
 
1709   return @errors if scalar @errors;
 
1711   $print_form->throw_on_error(sub {
 
1713       $print_form->prepare_for_printing;
 
1715       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
1716         format        => $print_form->{format},
 
1717         template_type => $template_type,
 
1718         template      => $template_file,
 
1719         variables     => $print_form,
 
1720         variable_content_types => {
 
1721           longdescription => 'html',
 
1722           partnotes       => 'html',
 
1727     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
1733 sub get_files_for_email_dialog {
 
1736   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
1738   return %files if !$::instance_conf->get_doc_storage;
 
1740   if ($self->order->id) {
 
1741     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
1742     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
1743     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
1747     uniq_by { $_->{id} }
 
1749       +{ id         => $_->part->id,
 
1750          partnumber => $_->part->partnumber }
 
1751     } @{$self->order->items_sorted};
 
1753   foreach my $part (@parts) {
 
1754     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
1755     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
1758   foreach my $key (keys %files) {
 
1759     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
1765 sub make_periodic_invoices_config_from_yaml {
 
1766   my ($yaml_config) = @_;
 
1768   return if !$yaml_config;
 
1769   my $attr = SL::YAML::Load($yaml_config);
 
1770   return if 'HASH' ne ref $attr;
 
1771   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
1775 sub get_periodic_invoices_status {
 
1776   my ($self, $config) = @_;
 
1778   return                      if $self->type ne sales_order_type();
 
1779   return t8('not configured') if !$config;
 
1781   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
1782              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
1783              :                                                     die "Cannot get status of periodic invoices config";
 
1785   return $active ? t8('active') : t8('inactive');
 
1789   my ($self, $action) = @_;
 
1791   return '' if none { lc($action)} qw(add edit);
 
1794   # $::locale->text("Add Sales Order");
 
1795   # $::locale->text("Add Purchase Order");
 
1796   # $::locale->text("Add Quotation");
 
1797   # $::locale->text("Add Request for Quotation");
 
1798   # $::locale->text("Edit Sales Order");
 
1799   # $::locale->text("Edit Purchase Order");
 
1800   # $::locale->text("Edit Quotation");
 
1801   # $::locale->text("Edit Request for Quotation");
 
1803   $action = ucfirst(lc($action));
 
1804   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
1805        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
1806        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
1807        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
1811 sub sales_order_type {
 
1815 sub purchase_order_type {
 
1819 sub sales_quotation_type {
 
1823 sub request_quotation_type {
 
1824   'request_quotation';
 
1828   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
1829        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
1830        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
1831        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
1843 SL::Controller::Order - controller for orders
 
1847 This is a new form to enter orders, completely rewritten with the use
 
1848 of controller and java script techniques.
 
1850 The aim is to provide the user a better expirience and a faster flow
 
1851 of work. Also the code should be more readable, more reliable and
 
1860 One input row, so that input happens every time at the same place.
 
1864 Use of pickers where possible.
 
1868 Possibility to enter more than one item at once.
 
1872 Item list in a scrollable area, so that the workflow buttons stay at
 
1877 Reordering item rows with drag and drop is possible. Sorting item rows is
 
1878 possible (by partnumber, description, qty, sellprice and discount for now).
 
1882 No C<update> is necessary. All entries and calculations are managed
 
1883 with ajax-calls and the page does only reload on C<save>.
 
1887 User can see changes immediately, because of the use of java script
 
1898 =item * C<SL/Controller/Order.pm>
 
1902 =item * C<template/webpages/order/form.html>
 
1906 =item * C<template/webpages/order/tabs/basic_data.html>
 
1908 Main tab for basic_data.
 
1910 This is the only tab here for now. "linked records" and "webdav" tabs are
 
1911 reused from generic code.
 
1915 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
1917 For displaying information on business type
 
1919 =item * C<template/webpages/order/tabs/_item_input.html>
 
1921 The input line for items
 
1923 =item * C<template/webpages/order/tabs/_row.html>
 
1925 One row for already entered items
 
1927 =item * C<template/webpages/order/tabs/_tax_row.html>
 
1929 Displaying tax information
 
1931 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
 
1933 Dialog for entering more than one item at once
 
1935 =item * C<template/webpages/order/tabs/_multi_items_result.html>
 
1937 Results for the filter in the multi items dialog
 
1939 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
1941 Dialog for selecting price and discount sources
 
1945 =item * C<js/kivi.Order.js>
 
1947 java script functions
 
1959 =item * credit limit
 
1961 =item * more workflows (quotation, rfq)
 
1963 =item * price sources: little symbols showing better price / better discount
 
1965 =item * select units in input row?
 
1967 =item * custom shipto address
 
1969 =item * check for direct delivery (workflow sales order -> purchase order)
 
1971 =item * language / part translations
 
1973 =item * access rights
 
1975 =item * display weights
 
1981 =item * optional client/user behaviour
 
1983 (transactions has to be set - department has to be set -
 
1984  force project if enabled in client config - transport cost reminder)
 
1988 =head1 KNOWN BUGS AND CAVEATS
 
1994 Customer discount is not displayed as a valid discount in price source popup
 
1995 (this might be a bug in price sources)
 
1997 (I cannot reproduce this (Bernd))
 
2001 No indication that <shift>-up/down expands/collapses second row.
 
2005 Inline creation of parts is not currently supported
 
2009 Table header is not sticky in the scrolling area.
 
2013 Sorting does not include C<position>, neither does reordering.
 
2015 This behavior was implemented intentionally. But we can discuss, which behavior
 
2016 should be implemented.
 
2020 C<show_multi_items_dialog> does not use the currently inserted string for
 
2025 The language selected in print or email dialog is not saved when the order is saved.
 
2029 =head1 To discuss / Nice to have
 
2035 How to expand/collapse second row. Now it can be done clicking the icon or
 
2040 Possibility to change longdescription in input row?
 
2044 Possibility to select PriceSources in input row?
 
2048 This controller uses a (changed) copy of the template for the PriceSource
 
2049 dialog. Maybe there could be used one code source.
 
2053 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2054 form. This is not only a problem here, but also in all parts using the PTC.
 
2055 There exists a ticket and a patch. This patch should be testet.
 
2059 An indicator, if the actual inputs are saved (like in an
 
2060 editor or on text processing application).
 
2064 A warning when leaving the page without saveing unchanged inputs.
 
2071 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>