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 div_tag);
 
   8 use SL::Locale::String qw(t8);
 
   9 use SL::SessionFile::Random;
 
  14 use SL::Util qw(trim);
 
  21 use SL::DB::PartClassification;
 
  22 use SL::DB::PartsGroup;
 
  25 use SL::DB::RecordLink;
 
  27 use SL::DB::Translation;
 
  29 use SL::Helper::CreatePDF qw(:all);
 
  30 use SL::Helper::PrintOptions;
 
  31 use SL::Helper::ShippedQty;
 
  32 use SL::Helper::UserPreferences::PositionsScrollbar;
 
  33 use SL::Helper::UserPreferences::UpdatePositions;
 
  35 use SL::Controller::Helper::GetModels;
 
  37 use List::Util qw(first sum0);
 
  38 use List::UtilsBy qw(sort_by uniq_by);
 
  39 use List::MoreUtils qw(any none pairwise first_index);
 
  40 use English qw(-no_match_vars);
 
  45 use Rose::Object::MakeMethods::Generic
 
  47  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 
  48  'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
 
  53 __PACKAGE__->run_before('check_auth');
 
  55 __PACKAGE__->run_before('recalc',
 
  56                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
 
  59 __PACKAGE__->run_before('get_unalterable_data',
 
  60                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
 
  71   $self->order->transdate(DateTime->now_local());
 
  72   my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
  73                    $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
  75   if (   ($self->type eq sales_order_type()     &&  $::instance_conf->get_deliverydate_on)
 
  76       || ($self->type eq sales_quotation_type() &&  $::instance_conf->get_reqdate_on)
 
  77       && (!$self->order->reqdate)) {
 
  78     $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
 
  85     title => $self->get_title_for('add'),
 
  86     %{$self->{template_args}}
 
  90 # edit an existing order
 
  98     # this is to edit an order from an unsaved order object
 
 100     # set item ids to new fake id, to identify them as new items
 
 101     foreach my $item (@{$self->order->items_sorted}) {
 
 102       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 104     # trigger rendering values for second row as hidden, because they
 
 105     # are loaded only on demand. So we need to keep the values from
 
 107     $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
 114     title => $self->get_title_for('edit'),
 
 115     %{$self->{template_args}}
 
 119 # edit a collective order (consisting of one or more existing orders)
 
 120 sub action_edit_collective {
 
 124   my @multi_ids = map {
 
 125     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 126   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 128   # fall back to add if no ids are given
 
 129   if (scalar @multi_ids == 0) {
 
 134   # fall back to save as new if only one id is given
 
 135   if (scalar @multi_ids == 1) {
 
 136     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 137     $self->action_save_as_new();
 
 141   # make new order from given orders
 
 142   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 143   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 144   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 146   $self->action_edit();
 
 153   my $errors = $self->delete();
 
 155   if (scalar @{ $errors }) {
 
 156     $self->js->flash('error', $_) foreach @{ $errors };
 
 157     return $self->js->render();
 
 160   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 161            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 162            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 163            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 165   flash_later('info', $text);
 
 167   my @redirect_params = (
 
 172   $self->redirect_to(@redirect_params);
 
 179   my $errors = $self->save();
 
 181   if (scalar @{ $errors }) {
 
 182     $self->js->flash('error', $_) foreach @{ $errors };
 
 183     return $self->js->render();
 
 186   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 187            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 188            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 189            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 191   flash_later('info', $text);
 
 193   my @redirect_params = (
 
 196     id     => $self->order->id,
 
 199   $self->redirect_to(@redirect_params);
 
 202 # save the order as new document an open it for edit
 
 203 sub action_save_as_new {
 
 206   my $order = $self->order;
 
 209     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 210     return $self->js->render();
 
 213   # load order from db to check if values changed
 
 214   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 217   # Lets assign a new number if the user hasn't changed the previous one.
 
 218   # If it has been changed manually then use it as-is.
 
 219   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 221                         : trim($order->number);
 
 223   # Clear transdate unless changed
 
 224   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 225                         ? DateTime->today_local
 
 228   # Set new reqdate unless changed if it is enabled in client config
 
 229   if ($order->reqdate == $saved_order->reqdate) {
 
 230     my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
 231                      $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
 233     if (   ($self->type eq sales_order_type()     &&  !$::instance_conf->get_deliverydate_on)
 
 234         || ($self->type eq sales_quotation_type() &&  !$::instance_conf->get_reqdate_on)) {
 
 235       $new_attrs{reqdate} = '';
 
 237       $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 240     $new_attrs{reqdate} = $order->reqdate;
 
 244   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 246   # Create new record from current one
 
 247   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 249   # no linked records on save as new
 
 250   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 253   $self->action_save();
 
 258 # This is called if "print" is pressed in the print dialog.
 
 259 # If PDF creation was requested and succeeded, the pdf is offered for download
 
 260 # via send_file (which uses ajax in this case).
 
 264   my $errors = $self->save();
 
 266   if (scalar @{ $errors }) {
 
 267     $self->js->flash('error', $_) foreach @{ $errors };
 
 268     return $self->js->render();
 
 271   $self->js_reset_order_and_item_ids_after_save;
 
 273   my $format      = $::form->{print_options}->{format};
 
 274   my $media       = $::form->{print_options}->{media};
 
 275   my $formname    = $::form->{print_options}->{formname};
 
 276   my $copies      = $::form->{print_options}->{copies};
 
 277   my $groupitems  = $::form->{print_options}->{groupitems};
 
 278   my $printer_id  = $::form->{print_options}->{printer_id};
 
 280   # only pdf and opendocument by now
 
 281   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
 
 282     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 285   # only screen or printer by now
 
 286   if (none { $media eq $_ } qw(screen printer)) {
 
 287     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 290   # create a form for generate_attachment_filename
 
 291   my $form   = Form->new;
 
 292   $form->{$self->nr_key()}  = $self->order->number;
 
 293   $form->{type}             = $self->type;
 
 294   $form->{format}           = $format;
 
 295   $form->{formname}         = $formname;
 
 296   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 297   my $pdf_filename          = $form->generate_attachment_filename();
 
 300   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
 
 301                                                    formname   => $formname,
 
 302                                                    language   => $self->order->language,
 
 303                                                    printer_id => $printer_id,
 
 304                                                    groupitems => $groupitems });
 
 305   if (scalar @errors) {
 
 306     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 309   if ($media eq 'screen') {
 
 311     $self->js->flash('info', t8('The PDF has been created'));
 
 314       type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 315       name         => $pdf_filename,
 
 319   } elsif ($media eq 'printer') {
 
 321     my $printer_id = $::form->{print_options}->{printer_id};
 
 322     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 327     $self->js->flash('info', t8('The PDF has been printed'));
 
 330   my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $pdf_filename);
 
 331   if (scalar @warnings) {
 
 332     $self->js->flash('warning', $_) for @warnings;
 
 335   $self->save_history('PRINTED');
 
 338     ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
 
 341 sub action_preview_pdf {
 
 344   my $errors = $self->save();
 
 345   if (scalar @{ $errors }) {
 
 346     $self->js->flash('error', $_) foreach @{ $errors };
 
 347     return $self->js->render();
 
 350   $self->js_reset_order_and_item_ids_after_save;
 
 353   my $media       = 'screen';
 
 354   my $formname    = $self->type;
 
 357   # create a form for generate_attachment_filename
 
 358   my $form   = Form->new;
 
 359   $form->{$self->nr_key()}  = $self->order->number;
 
 360   $form->{type}             = $self->type;
 
 361   $form->{format}           = $format;
 
 362   $form->{formname}         = $formname;
 
 363   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 364   my $pdf_filename          = $form->generate_attachment_filename();
 
 367   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
 
 368                                                    formname   => $formname,
 
 369                                                    language   => $self->order->language,
 
 371   if (scalar @errors) {
 
 372     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 374   $self->save_history('PREVIEWED');
 
 375   $self->js->flash('info', t8('The PDF has been previewed'));
 
 379     type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 380     name         => $pdf_filename,
 
 385 # open the email dialog
 
 386 sub action_save_and_show_email_dialog {
 
 389   my $errors = $self->save();
 
 391   if (scalar @{ $errors }) {
 
 392     $self->js->flash('error', $_) foreach @{ $errors };
 
 393     return $self->js->render();
 
 396   my $cv_method = $self->cv;
 
 398   if (!$self->order->$cv_method) {
 
 399     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'))
 
 404   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 405   $email_form->{to} ||= $self->order->$cv_method->email;
 
 406   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 407   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 408   # Todo: get addresses from shipto, if any
 
 410   my $form = Form->new;
 
 411   $form->{$self->nr_key()}  = $self->order->number;
 
 412   $form->{cusordnumber}     = $self->order->cusordnumber;
 
 413   $form->{formname}         = $self->type;
 
 414   $form->{type}             = $self->type;
 
 415   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 416   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
 
 417   $form->{format}           = 'pdf';
 
 418   $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
 420   $email_form->{subject}             = $form->generate_email_subject();
 
 421   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 422   $email_form->{message}             = $form->generate_email_body();
 
 423   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 425   my %files = $self->get_files_for_email_dialog();
 
 426   $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
 
 427   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 428                                   email_form  => $email_form,
 
 429                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
 
 431                                   is_customer => $self->cv eq 'customer',
 
 432                                   ALL_EMPLOYEES => $self->{all_employees},
 
 436       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 443 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 444 sub action_send_email {
 
 447   my $errors = $self->save();
 
 449   if (scalar @{ $errors }) {
 
 450     $self->js->run('kivi.Order.close_email_dialog');
 
 451     $self->js->flash('error', $_) foreach @{ $errors };
 
 452     return $self->js->render();
 
 455   $self->js_reset_order_and_item_ids_after_save;
 
 457   my $email_form  = delete $::form->{email_form};
 
 458   my %field_names = (to => 'email');
 
 460   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 462   # for Form::cleanup which may be called in Form::send_email
 
 463   $::form->{cwd}    = getcwd();
 
 464   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 466   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 467   $::form->{media}  = 'email';
 
 469   if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
 
 471     my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
 
 472                                                     format     => $::form->{print_options}->{format},
 
 473                                                     formname   => $::form->{print_options}->{formname},
 
 474                                                     language   => $self->order->language,
 
 475                                                     printer_id => $::form->{print_options}->{printer_id},
 
 476                                                     groupitems => $::form->{print_options}->{groupitems}});
 
 477     if (scalar @errors) {
 
 478       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
 
 481     my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $::form->{attachment_filename});
 
 482     if (scalar @warnings) {
 
 483       flash_later('warning', $_) for @warnings;
 
 486     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 487     $sfile->fh->print($pdf);
 
 490     $::form->{tmpfile} = $sfile->file_name;
 
 491     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 494   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 495   $::form->send_email(\%::myconfig, 'pdf');
 
 498   my $intnotes = $self->order->intnotes;
 
 499   $intnotes   .= "\n\n" if $self->order->intnotes;
 
 500   $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 501   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 502   $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 503   $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 504   $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 505   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 506   $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
 508   $self->order->update_attributes(intnotes => $intnotes);
 
 510   $self->save_history('MAILED');
 
 512   flash_later('info', t8('The email has been sent.'));
 
 514   my @redirect_params = (
 
 517     id     => $self->order->id,
 
 520   $self->redirect_to(@redirect_params);
 
 523 # open the periodic invoices config dialog
 
 525 # If there are values in the form (i.e. dialog was opened before),
 
 526 # then use this values. Create new ones, else.
 
 527 sub action_show_periodic_invoices_config_dialog {
 
 530   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 531   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 532   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 533                                                    order_value_periodicity => 'p', # = same as periodicity
 
 534                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 535                                                    extend_automatically_by => 12,
 
 537                                                    email_subject           => GenericTranslations->get(
 
 538                                                                                 language_id      => $::form->{language_id},
 
 539                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 540                                                    email_body              => GenericTranslations->get(
 
 541                                                                                 language_id      => $::form->{language_id},
 
 542                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 544   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 545   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 547   $::form->get_lists(printers => "ALL_PRINTERS",
 
 548                      charts   => { key       => 'ALL_CHARTS',
 
 549                                    transdate => 'current_date' });
 
 551   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 553   if ($::form->{customer_id}) {
 
 554     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 555     my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
 
 556     $::form->{postal_invoice}                  = $customer_object->postal_invoice;
 
 557     $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
 
 558     $config->send_email(0) if $::form->{postal_invoice};
 
 561   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 563                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 564                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 569 # assign the values of the periodic invoices config dialog
 
 570 # as yaml in the hidden tag and set the status.
 
 571 sub action_assign_periodic_invoices_config {
 
 574   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 576   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 577                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 578                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 579                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 580                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 581                  start_date_as_date         => $::form->{start_date_as_date},
 
 582                  end_date_as_date           => $::form->{end_date_as_date},
 
 583                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 584                  print                      => $::form->{print}      ? 1                         : 0,
 
 585                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 586                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 587                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 588                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 589                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 590                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 591                  email_recipient_address    => $::form->{email_recipient_address},
 
 592                  email_sender               => $::form->{email_sender},
 
 593                  email_subject              => $::form->{email_subject},
 
 594                  email_body                 => $::form->{email_body},
 
 597   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 599   my $status = $self->get_periodic_invoices_status($config);
 
 602     ->remove('#order_periodic_invoices_config')
 
 603     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 604     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 605     ->html('#periodic_invoices_status', $status)
 
 606     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 610 sub action_get_has_active_periodic_invoices {
 
 613   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 614   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 616   my $has_active_periodic_invoices =
 
 617        $self->type eq sales_order_type()
 
 620     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 621     && $config->get_previous_billed_period_start_date;
 
 623   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 626 # save the order and redirect to the frontend subroutine for a new
 
 628 sub action_save_and_delivery_order {
 
 631   $self->save_and_redirect_to(
 
 632     controller => 'oe.pl',
 
 633     action     => 'oe_delivery_order_from_order',
 
 637 # save the order and redirect to the frontend subroutine for a new
 
 639 sub action_save_and_invoice {
 
 642   $self->save_and_redirect_to(
 
 643     controller => 'oe.pl',
 
 644     action     => 'oe_invoice_from_order',
 
 648 # workflow from sales order to sales quotation
 
 649 sub action_sales_quotation {
 
 650   $_[0]->workflow_sales_or_request_for_quotation();
 
 653 # workflow from sales order to sales quotation
 
 654 sub action_request_for_quotation {
 
 655   $_[0]->workflow_sales_or_request_for_quotation();
 
 658 # workflow from sales quotation to sales order
 
 659 sub action_sales_order {
 
 660   $_[0]->workflow_sales_or_purchase_order();
 
 663 # workflow from rfq to purchase order
 
 664 sub action_purchase_order {
 
 665   $_[0]->workflow_sales_or_purchase_order();
 
 668 # workflow from purchase order to ap transaction
 
 669 sub action_save_and_ap_transaction {
 
 672   $self->save_and_redirect_to(
 
 673     controller => 'ap.pl',
 
 674     action     => 'add_from_purchase_order',
 
 678 # set form elements in respect to a changed customer or vendor
 
 680 # This action is called on an change of the customer/vendor picker.
 
 681 sub action_customer_vendor_changed {
 
 684   setup_order_from_cv($self->order);
 
 687   my $cv_method = $self->cv;
 
 689   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 690     $self->js->show('#cp_row');
 
 692     $self->js->hide('#cp_row');
 
 695   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 696     $self->js->show('#shipto_selection');
 
 698     $self->js->hide('#shipto_selection');
 
 701   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 704     ->replaceWith('#order_cp_id',            $self->build_contact_select)
 
 705     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
 
 706     ->replaceWith('#shipto_inputs  ',        $self->build_shipto_inputs)
 
 707     ->replaceWith('#business_info_row',      $self->build_business_info_row)
 
 708     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
 
 709     ->val(        '#order_taxincluded',      $self->order->taxincluded)
 
 710     ->val(        '#order_currency_id',      $self->order->currency_id)
 
 711     ->val(        '#order_payment_id',       $self->order->payment_id)
 
 712     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
 
 713     ->val(        '#order_intnotes',         $self->order->intnotes)
 
 714     ->val(        '#order_language_id',      $self->order->$cv_method->language_id)
 
 715     ->focus(      '#order_' . $self->cv . '_id')
 
 716     ->run('kivi.Order.update_exchangerate');
 
 718   $self->js_redisplay_amounts_and_taxes;
 
 719   $self->js_redisplay_cvpartnumbers;
 
 723 # open the dialog for customer/vendor details
 
 724 sub action_show_customer_vendor_details_dialog {
 
 727   my $is_customer = 'customer' eq $::form->{vc};
 
 730     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 732     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 735   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 736   $details{discount_as_percent} = $cv->discount_as_percent;
 
 737   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 738   $details{business}            = $cv->business->description      if $cv->business;
 
 739   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 740   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 741   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 742   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 744   foreach my $entry (@{ $cv->shipto }) {
 
 745     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 747   foreach my $entry (@{ $cv->contacts }) {
 
 748     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 751   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 752                 is_customer => $is_customer,
 
 757 # called if a unit in an existing item row is changed
 
 758 sub action_unit_changed {
 
 761   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 762   my $item = $self->order->items_sorted->[$idx];
 
 764   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 765   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 770     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 771   $self->js_redisplay_line_values;
 
 772   $self->js_redisplay_amounts_and_taxes;
 
 776 # add an item row for a new item entered in the input row
 
 777 sub action_add_item {
 
 780   delete $::form->{add_item}->{create_part_type};
 
 782   my $form_attr = $::form->{add_item};
 
 784   return unless $form_attr->{parts_id};
 
 786   my $item = new_item($self->order, $form_attr);
 
 788   $self->order->add_items($item);
 
 792   $self->get_item_cvpartnumber($item);
 
 794   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 795   my $row_as_html = $self->p->render('order/tabs/_row',
 
 801   if ($::form->{insert_before_item_id}) {
 
 803       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 806       ->append('#row_table_id', $row_as_html);
 
 809   if ( $item->part->is_assortment ) {
 
 810     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 811     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 812       my $attr = { parts_id => $assortment_item->parts_id,
 
 813                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 814                    unit     => $assortment_item->unit,
 
 815                    description => $assortment_item->part->description,
 
 817       my $item = new_item($self->order, $attr);
 
 819       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 820       $item->discount(1) unless $assortment_item->charge;
 
 822       $self->order->add_items( $item );
 
 824       $self->get_item_cvpartnumber($item);
 
 825       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 826       my $row_as_html = $self->p->render('order/tabs/_row',
 
 831       if ($::form->{insert_before_item_id}) {
 
 833           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 836           ->append('#row_table_id', $row_as_html);
 
 842     ->val('.add_item_input', '')
 
 843     ->run('kivi.Order.init_row_handlers')
 
 844     ->run('kivi.Order.renumber_positions')
 
 845     ->focus('#add_item_parts_id_name');
 
 847   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 849   $self->js_redisplay_amounts_and_taxes;
 
 853 # add item rows for multiple items at once
 
 854 sub action_add_multi_items {
 
 857   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
 
 858   return $self->js->render() unless scalar @form_attr;
 
 861   foreach my $attr (@form_attr) {
 
 862     my $item = new_item($self->order, $attr);
 
 864     if ( $item->part->is_assortment ) {
 
 865       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 866         my $attr = { parts_id => $assortment_item->parts_id,
 
 867                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 868                      unit     => $assortment_item->unit,
 
 869                      description => $assortment_item->part->description,
 
 871         my $item = new_item($self->order, $attr);
 
 873         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 874         $item->discount(1) unless $assortment_item->charge;
 
 879   $self->order->add_items(@items);
 
 883   foreach my $item (@items) {
 
 884     $self->get_item_cvpartnumber($item);
 
 885     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 886     my $row_as_html = $self->p->render('order/tabs/_row',
 
 892     if ($::form->{insert_before_item_id}) {
 
 894         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 897         ->append('#row_table_id', $row_as_html);
 
 902     ->run('kivi.Part.close_picker_dialogs')
 
 903     ->run('kivi.Order.init_row_handlers')
 
 904     ->run('kivi.Order.renumber_positions')
 
 905     ->focus('#add_item_parts_id_name');
 
 907   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 909   $self->js_redisplay_amounts_and_taxes;
 
 913 # recalculate all linetotals, amounts and taxes and redisplay them
 
 914 sub action_recalc_amounts_and_taxes {
 
 919   $self->js_redisplay_line_values;
 
 920   $self->js_redisplay_amounts_and_taxes;
 
 924 sub action_update_exchangerate {
 
 928     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
 929     currency_name => $self->order->currency->name,
 
 930     exchangerate  => $self->order->daily_exchangerate_as_null_number,
 
 933   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
 936 # redisplay item rows if they are sorted by an attribute
 
 937 sub action_reorder_items {
 
 941     partnumber   => sub { $_[0]->part->partnumber },
 
 942     description  => sub { $_[0]->description },
 
 943     qty          => sub { $_[0]->qty },
 
 944     sellprice    => sub { $_[0]->sellprice },
 
 945     discount     => sub { $_[0]->discount },
 
 946     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
 949   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
 951   my $method = $sort_keys{$::form->{order_by}};
 
 952   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
 953   if ($::form->{sort_dir}) {
 
 954     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 955       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
 957       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 960     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 961       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
 963       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 967     ->run('kivi.Order.redisplay_items', \@to_sort)
 
 971 # show the popup to choose a price/discount source
 
 972 sub action_price_popup {
 
 975   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 976   my $item = $self->order->items_sorted->[$idx];
 
 978   $self->render_price_dialog($item);
 
 981 # save the order in a session variable and redirect to the part controller
 
 982 sub action_create_part {
 
 985   $::lxdebug->dump(0, "bb: form", $::form);
 
 986   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
 
 988   my $callback     = $self->url_for(
 
 989     action       => 'return_from_create_part',
 
 990     type         => $self->type, # type is needed for check_auth on return
 
 991     previousform => $previousform,
 
 994   flash_later('info', t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.'));
 
 996   my @redirect_params = (
 
 997     controller => 'Part',
 
 999     part_type  => $::form->{add_item}->{create_part_type},
 
1000     callback   => $callback,
 
1004   $self->redirect_to(@redirect_params);
 
1007 sub action_return_from_create_part {
 
1010   $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
 
1012   $::auth->restore_form_from_session(delete $::form->{previousform});
 
1014   # set item ids to new fake id, to identify them as new items
 
1015   foreach my $item (@{$self->order->items_sorted}) {
 
1016     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1020   $self->get_unalterable_data();
 
1021   $self->pre_render();
 
1023   # trigger rendering values for second row/longdescription as hidden,
 
1024   # because they are loaded only on demand. So we need to keep the values
 
1026   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1027   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1031     title => $self->get_title_for('edit'),
 
1032     %{$self->{template_args}}
 
1037 # load the second row for one or more items
 
1039 # This action gets the html code for all items second rows by rendering a template for
 
1040 # the second row and sets the html code via client js.
 
1041 sub action_load_second_rows {
 
1044   $self->recalc() if $self->order->is_sales; # for margin calculation
 
1046   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1047     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1048     my $item = $self->order->items_sorted->[$idx];
 
1050     $self->js_load_second_row($item, $item_id, 0);
 
1053   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
1055   $self->js->render();
 
1058 # update description, notes and sellprice from master data
 
1059 sub action_update_row_from_master_data {
 
1062   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1063     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1064     my $item  = $self->order->items_sorted->[$idx];
 
1065     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1067     $item->description($texts->{description});
 
1068     $item->longdescription($texts->{longdescription});
 
1070     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1073     if ($item->part->is_assortment) {
 
1074     # add assortment items with price 0, as the components carry the price
 
1075       $price_src = $price_source->price_from_source("");
 
1076       $price_src->price(0);
 
1078       $price_src = $price_source->best_price
 
1079                  ? $price_source->best_price
 
1080                  : $price_source->price_from_source("");
 
1081       $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
 
1082       $price_src->price(0) if !$price_source->best_price;
 
1086     $item->sellprice($price_src->price);
 
1087     $item->active_price_source($price_src);
 
1090       ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
 
1091       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1092       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1093       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1095     if ($self->search_cvpartnumber) {
 
1096       $self->get_item_cvpartnumber($item);
 
1097       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1102   $self->js_redisplay_line_values;
 
1103   $self->js_redisplay_amounts_and_taxes;
 
1105   $self->js->render();
 
1108 sub js_load_second_row {
 
1109   my ($self, $item, $item_id, $do_parse) = @_;
 
1112     # Parse values from form (they are formated while rendering (template)).
 
1113     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1114     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1115     foreach my $var (@{ $item->cvars_by_config }) {
 
1116       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1118     $item->parse_custom_variable_values;
 
1121   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1124     ->html('#second_row_' . $item_id, $row_as_html)
 
1125     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1128 sub js_redisplay_line_values {
 
1131   my $is_sales = $self->order->is_sales;
 
1133   # sales orders with margins
 
1138        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1139        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1140        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1141       ]} @{ $self->order->items_sorted };
 
1145        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1146       ]} @{ $self->order->items_sorted };
 
1150     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1153 sub js_redisplay_amounts_and_taxes {
 
1156   if (scalar @{ $self->{taxes} }) {
 
1157     $self->js->show('#taxincluded_row_id');
 
1159     $self->js->hide('#taxincluded_row_id');
 
1162   if ($self->order->taxincluded) {
 
1163     $self->js->hide('#subtotal_row_id');
 
1165     $self->js->show('#subtotal_row_id');
 
1168   if ($self->order->is_sales) {
 
1169     my $is_neg = $self->order->marge_total < 0;
 
1171       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1172       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1173       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1174       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1175       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1176       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1177       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1178       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1182     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1183     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1184     ->remove('.tax_row')
 
1185     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1188 sub js_redisplay_cvpartnumbers {
 
1191   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1193   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1196     ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
 
1199 sub js_reset_order_and_item_ids_after_save {
 
1203     ->val('#id', $self->order->id)
 
1204     ->val('#converted_from_oe_id', '')
 
1205     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1208   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1209     next if !$self->order->items_sorted->[$idx]->id;
 
1210     next if $form_item_id !~ m{^new};
 
1212       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1213       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1214       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1218   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1225 sub init_valid_types {
 
1226   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1232   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1233     die "Not a valid type for order";
 
1236   $self->type($::form->{type});
 
1242   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1243          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1244          : die "Not a valid type for order";
 
1249 sub init_search_cvpartnumber {
 
1252   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1253   my $search_cvpartnumber;
 
1254   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1255   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1257   return $search_cvpartnumber;
 
1260 sub init_show_update_button {
 
1263   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1274 sub init_all_price_factors {
 
1275   SL::DB::Manager::PriceFactor->get_all;
 
1278 sub init_part_picker_classification_ids {
 
1280   my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
 
1282   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
 
1288   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1290   my $right   = $right_for->{ $self->type };
 
1291   $right    ||= 'DOES_NOT_EXIST';
 
1293   $::auth->assert($right);
 
1296 # build the selection box for contacts
 
1298 # Needed, if customer/vendor changed.
 
1299 sub build_contact_select {
 
1302   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1303     value_key  => 'cp_id',
 
1304     title_key  => 'full_name_dep',
 
1305     default    => $self->order->cp_id,
 
1307     style      => 'width: 300px',
 
1311 # build the selection box for shiptos
 
1313 # Needed, if customer/vendor changed.
 
1314 sub build_shipto_select {
 
1317   select_tag('order.shipto_id',
 
1318              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1319              value_key  => 'shipto_id',
 
1320              title_key  => 'displayable_id',
 
1321              default    => $self->order->shipto_id,
 
1323              style      => 'width: 300px',
 
1327 # build the inputs for the cusom shipto dialog
 
1329 # Needed, if customer/vendor changed.
 
1330 sub build_shipto_inputs {
 
1333   my $content = $self->p->render('common/_ship_to_dialog',
 
1334                                  vc_obj      => $self->order->customervendor,
 
1335                                  cs_obj      => $self->order->custom_shipto,
 
1336                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1337                                  id_selector => '#order_shipto_id');
 
1339   div_tag($content, id => 'shipto_inputs');
 
1342 # render the info line for business
 
1344 # Needed, if customer/vendor changed.
 
1345 sub build_business_info_row
 
1347   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1350 # build the rows for displaying taxes
 
1352 # Called if amounts where recalculated and redisplayed.
 
1353 sub build_tax_rows {
 
1357   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1358     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1360   return $rows_as_html;
 
1364 sub render_price_dialog {
 
1365   my ($self, $record_item) = @_;
 
1367   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1371       'kivi.io.price_chooser_dialog',
 
1372       t8('Available Prices'),
 
1373       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1378 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1379 #     $self->js->show('#dialog_flash_error');
 
1388   return if !$::form->{id};
 
1390   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1392   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1393   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1394   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1396   return $self->order;
 
1399 # load or create a new order object
 
1401 # And assign changes from the form to this object.
 
1402 # If the order is loaded from db, check if items are deleted in the form,
 
1403 # remove them form the object and collect them for removing from db on saving.
 
1404 # Then create/update items from form (via make_item) and add them.
 
1408   # add_items adds items to an order with no items for saving, but they cannot
 
1409   # be retrieved via items until the order is saved. Adding empty items to new
 
1410   # order here solves this problem.
 
1412   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1413   $order ||= SL::DB::Order->new(orderitems  => [],
 
1414                                 quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
 
1415                                 currency_id => $::instance_conf->get_currency_id(),);
 
1417   my $cv_id_method = $self->cv . '_id';
 
1418   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1419     $order->$cv_id_method($::form->{$cv_id_method});
 
1420     setup_order_from_cv($order);
 
1423   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1424   my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
 
1426   $order->assign_attributes(%{$::form->{order}});
 
1428   $self->setup_custom_shipto_from_form($order, $::form);
 
1430   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1431     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1432     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1435   # remove deleted items
 
1436   $self->item_ids_to_delete([]);
 
1437   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1438     my $item = $order->orderitems->[$idx];
 
1439     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1440       splice @{$order->orderitems}, $idx, 1;
 
1441       push @{$self->item_ids_to_delete}, $item->id;
 
1447   foreach my $form_attr (@{$form_orderitems}) {
 
1448     my $item = make_item($order, $form_attr);
 
1449     $item->position($pos);
 
1453   $order->add_items(grep {!$_->id} @items);
 
1458 # create or update items from form
 
1460 # Make item objects from form values. For items already existing read from db.
 
1461 # Create a new item else. And assign attributes.
 
1463   my ($record, $attr) = @_;
 
1466   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1468   my $is_new = !$item;
 
1470   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1471   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1472   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1473   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1475   $item->assign_attributes(%$attr);
 
1478     my $texts = get_part_texts($item->part, $record->language_id);
 
1479     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1480     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1481     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1489 # This is used to add one item
 
1491   my ($record, $attr) = @_;
 
1493   my $item = SL::DB::OrderItem->new;
 
1495   # Remove attributes where the user left or set the inputs empty.
 
1496   # So these attributes will be undefined and we can distinguish them
 
1497   # from zero later on.
 
1498   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1499     delete $attr->{$_} if $attr->{$_} eq '';
 
1502   $item->assign_attributes(%$attr);
 
1504   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1505   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1507   $item->unit($part->unit) if !$item->unit;
 
1510   if ( $part->is_assortment ) {
 
1511     # add assortment items with price 0, as the components carry the price
 
1512     $price_src = $price_source->price_from_source("");
 
1513     $price_src->price(0);
 
1514   } elsif (defined $item->sellprice) {
 
1515     $price_src = $price_source->price_from_source("");
 
1516     $price_src->price($item->sellprice);
 
1518     $price_src = $price_source->best_price
 
1519                ? $price_source->best_price
 
1520                : $price_source->price_from_source("");
 
1521     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
1522     $price_src->price(0) if !$price_source->best_price;
 
1526   if (defined $item->discount) {
 
1527     $discount_src = $price_source->discount_from_source("");
 
1528     $discount_src->discount($item->discount);
 
1530     $discount_src = $price_source->best_discount
 
1531                   ? $price_source->best_discount
 
1532                   : $price_source->discount_from_source("");
 
1533     $discount_src->discount(0) if !$price_source->best_discount;
 
1537   $new_attr{part}                   = $part;
 
1538   $new_attr{description}            = $part->description     if ! $item->description;
 
1539   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1540   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1541   $new_attr{sellprice}              = $price_src->price;
 
1542   $new_attr{discount}               = $discount_src->discount;
 
1543   $new_attr{active_price_source}    = $price_src;
 
1544   $new_attr{active_discount_source} = $discount_src;
 
1545   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1546   $new_attr{project_id}             = $record->globalproject_id;
 
1547   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1549   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1550   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1551   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1552   $new_attr{custom_variables} = [];
 
1554   my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1556   $item->assign_attributes(%new_attr, %{ $texts });
 
1561 sub setup_order_from_cv {
 
1564   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
 
1566   $order->intnotes($order->customervendor->notes);
 
1568   if ($order->is_sales) {
 
1569     $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1570     $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1571                         ? $order->customer->taxincluded_checked
 
1572                         : $::myconfig{taxincluded_checked});
 
1577 # setup custom shipto from form
 
1579 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1580 # with 'shiptocvar_'.
 
1581 # Mark it to be deleted if a shipto from master data is selected
 
1582 # (i.e. order has a shipto).
 
1583 # Else, update or create a new custom shipto. If the fields are empty, it
 
1584 # will not be saved on save.
 
1585 sub setup_custom_shipto_from_form {
 
1586   my ($self, $order, $form) = @_;
 
1588   if ($order->shipto) {
 
1589     $self->is_custom_shipto_to_delete(1);
 
1591     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1593     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1594     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1596     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1597     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1601 # recalculate prices and taxes
 
1603 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1607   my %pat = $self->order->calculate_prices_and_taxes();
 
1609   $self->{taxes} = [];
 
1610   foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
 
1611     my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
1613     push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
 
1614                                 netamount => $netamount,
 
1615                                 tax       => SL::DB::Tax->new(id => $tax_id)->load });
 
1617   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1620 # get data for saving, printing, ..., that is not changed in the form
 
1622 # Only cvars for now.
 
1623 sub get_unalterable_data {
 
1626   foreach my $item (@{ $self->order->items }) {
 
1627     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1628     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1629     foreach my $var (@{ $item->cvars_by_config }) {
 
1630       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1632     $item->parse_custom_variable_values;
 
1638 # And remove related files in the spool directory
 
1643   my $db     = $self->order->db;
 
1645   $db->with_transaction(
 
1647       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1648       $self->order->delete;
 
1649       my $spool = $::lx_office_conf{paths}->{spool};
 
1650       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1652       $self->save_history('DELETED');
 
1655   }) || push(@{$errors}, $db->error);
 
1662 # And delete items that are deleted in the form.
 
1667   my $db     = $self->order->db;
 
1669   $db->with_transaction(sub {
 
1670     # delete custom shipto if it is to be deleted or if it is empty
 
1671     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1672       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1673       $self->order->custom_shipto(undef);
 
1676     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1677     $self->order->save(cascade => 1);
 
1680     if ($::form->{converted_from_oe_id}) {
 
1681       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1682       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1683         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1684         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1685         $src->link_to_record($self->order);
 
1687       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1689         foreach (@{ $self->order->items_sorted }) {
 
1690           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1692           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1693                                   from_id    => $from_id,
 
1694                                   to_table   => 'orderitems',
 
1702     $self->save_history('SAVED');
 
1705   }) || push(@{$errors}, $db->error);
 
1710 sub workflow_sales_or_request_for_quotation {
 
1714   my $errors = $self->save();
 
1716   if (scalar @{ $errors }) {
 
1717     $self->js->flash('error', $_) for @{ $errors };
 
1718     return $self->js->render();
 
1721   my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
 
1723   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1724   $self->{converted_from_oe_id} = delete $::form->{id};
 
1726   # set item ids to new fake id, to identify them as new items
 
1727   foreach my $item (@{$self->order->items_sorted}) {
 
1728     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1732   $::form->{type} = $destination_type;
 
1733   $self->type($self->init_type);
 
1734   $self->cv  ($self->init_cv);
 
1738   $self->get_unalterable_data();
 
1739   $self->pre_render();
 
1741   # trigger rendering values for second row as hidden, because they
 
1742   # are loaded only on demand. So we need to keep the values from the
 
1744   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1748     title => $self->get_title_for('edit'),
 
1749     %{$self->{template_args}}
 
1753 sub workflow_sales_or_purchase_order {
 
1757   my $errors = $self->save();
 
1759   if (scalar @{ $errors }) {
 
1760     $self->js->flash('error', $_) foreach @{ $errors };
 
1761     return $self->js->render();
 
1764   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1765                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1766                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1767                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1770   # check for direct delivery
 
1771   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1773   if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
 
1774       && $::form->{use_shipto} && $self->order->shipto) {
 
1775     $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
 
1778   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1779   $self->{converted_from_oe_id} = delete $::form->{id};
 
1781   # set item ids to new fake id, to identify them as new items
 
1782   foreach my $item (@{$self->order->items_sorted}) {
 
1783     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1786   if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
 
1787     if ($::form->{use_shipto}) {
 
1788       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
1790       # remove any custom shipto if not wanted
 
1791       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1796   $::form->{type} = $destination_type;
 
1797   $self->type($self->init_type);
 
1798   $self->cv  ($self->init_cv);
 
1802   $self->get_unalterable_data();
 
1803   $self->pre_render();
 
1805   # trigger rendering values for second row as hidden, because they
 
1806   # are loaded only on demand. So we need to keep the values from the
 
1808   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1812     title => $self->get_title_for('edit'),
 
1813     %{$self->{template_args}}
 
1821   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1822   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
1823   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1824   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted();
 
1825   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1828   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1831   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1833   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1834   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1835   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1836   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1837   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1839   my $print_form = Form->new('');
 
1840   $print_form->{type}        = $self->type;
 
1841   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
1842   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
1843     form => $print_form,
 
1844     options => {dialog_name_prefix => 'print_options.',
 
1848                 no_opendocument    => 0,
 
1852   foreach my $item (@{$self->order->orderitems}) {
 
1853     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1854     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1855     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1858   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1859     # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
 
1860     # Do not use write_to_objects to prevent order->delivered to be set, because this should be
 
1861     # the value from db, which can be set manually or is set when linked delivery orders are saved.
 
1862     SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
 
1865   if ($self->order->number && $::instance_conf->get_webdav) {
 
1866     my $webdav = SL::Webdav->new(
 
1867       type     => $self->type,
 
1868       number   => $self->order->number,
 
1870     my @all_objects = $webdav->get_all_objects;
 
1871     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1873                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1877   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1879   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
1880                                                          edit_periodic_invoices_config calculate_qty kivi.Validator follow_up show_history);
 
1881   $self->setup_edit_action_bar;
 
1884 sub setup_edit_action_bar {
 
1885   my ($self, %params) = @_;
 
1887   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
1888                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1889                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1891   for my $bar ($::request->layout->get('actionbar')) {
 
1896           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1897                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
1899           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'] ],
 
1903           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1904           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1905           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1907       ], # end of combobox "Save"
 
1914           t8('Save and Quotation'),
 
1915           submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
 
1916           only_if  => (any { $self->type eq $_ } (sales_order_type())),
 
1920           submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
 
1921           only_if  => (any { $self->type eq $_ } (purchase_order_type())),
 
1924           t8('Save and Sales Order'),
 
1925           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
1926           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
1929           t8('Save and Purchase Order'),
 
1930           call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
 
1931           only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
1934           t8('Save and Delivery Order'),
 
1935           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
1936                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1938           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1939           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
 
1942           t8('Save and Invoice'),
 
1943           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1944           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1947           t8('Save and AP Transaction'),
 
1948           call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
1949           only_if   => (any { $self->type eq $_ } (purchase_order_type()))
 
1952       ], # end of combobox "Workflow"
 
1959           t8('Save and preview PDF'),
 
1960            call => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
1961                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1965           t8('Save and print'),
 
1966           call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
1967                                                      $::instance_conf->get_order_warn_no_deliverydate,
 
1971           t8('Save and E-mail'),
 
1972           id   => 'save_and_email_action',
 
1973           call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
1974                                                                      $::instance_conf->get_order_warn_no_deliverydate,
 
1976           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1979           t8('Download attachments of all parts'),
 
1980           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1981           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1982           only_if  => $::instance_conf->get_doc_storage,
 
1984       ], # end of combobox "Export"
 
1988         call     => [ 'kivi.Order.delete_order' ],
 
1989         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1990         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1991         only_if  => $deletion_allowed,
 
2000           call     => [ 'kivi.Order.follow_up_window' ],
 
2001           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2002           only_if  => $::auth->assert('productivity', 1),
 
2006           call     => [ 'set_history_window', $self->order->id, 'id' ],
 
2007           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
 
2009       ], # end of combobox "more"
 
2015   my ($order, $pdf_ref, $params) = @_;
 
2019   my $print_form = Form->new('');
 
2020   $print_form->{type}        = $order->type;
 
2021   $print_form->{formname}    = $params->{formname} || $order->type;
 
2022   $print_form->{format}      = $params->{format}   || 'pdf';
 
2023   $print_form->{media}       = $params->{media}    || 'file';
 
2024   $print_form->{groupitems}  = $params->{groupitems};
 
2025   $print_form->{printer_id}  = $params->{printer_id};
 
2026   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
2028   $order->language($params->{language});
 
2029   $order->flatten_to_form($print_form, format_amounts => 1);
 
2033   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
2034     $template_ext  = 'odt';
 
2035     $template_type = 'OpenDocument';
 
2038   # search for the template
 
2039   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
2040     name        => $print_form->{formname},
 
2041     extension   => $template_ext,
 
2042     email       => $print_form->{media} eq 'email',
 
2043     language    => $params->{language},
 
2044     printer_id  => $print_form->{printer_id},
 
2047   if (!defined $template_file) {
 
2048     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);
 
2051   return @errors if scalar @errors;
 
2053   $print_form->throw_on_error(sub {
 
2055       $print_form->prepare_for_printing;
 
2057       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
2058         format        => $print_form->{format},
 
2059         template_type => $template_type,
 
2060         template      => $template_file,
 
2061         variables     => $print_form,
 
2062         variable_content_types => {
 
2063           longdescription => 'html',
 
2064           partnotes       => 'html',
 
2069     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2075 sub get_files_for_email_dialog {
 
2078   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2080   return %files if !$::instance_conf->get_doc_storage;
 
2082   if ($self->order->id) {
 
2083     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2084     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2085     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2086     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2090     uniq_by { $_->{id} }
 
2092       +{ id         => $_->part->id,
 
2093          partnumber => $_->part->partnumber }
 
2094     } @{$self->order->items_sorted};
 
2096   foreach my $part (@parts) {
 
2097     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2098     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2101   foreach my $key (keys %files) {
 
2102     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2108 sub make_periodic_invoices_config_from_yaml {
 
2109   my ($yaml_config) = @_;
 
2111   return if !$yaml_config;
 
2112   my $attr = SL::YAML::Load($yaml_config);
 
2113   return if 'HASH' ne ref $attr;
 
2114   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
2118 sub get_periodic_invoices_status {
 
2119   my ($self, $config) = @_;
 
2121   return                      if $self->type ne sales_order_type();
 
2122   return t8('not configured') if !$config;
 
2124   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
2125              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
2126              :                                                     die "Cannot get status of periodic invoices config";
 
2128   return $active ? t8('active') : t8('inactive');
 
2132   my ($self, $action) = @_;
 
2134   return '' if none { lc($action)} qw(add edit);
 
2137   # $::locale->text("Add Sales Order");
 
2138   # $::locale->text("Add Purchase Order");
 
2139   # $::locale->text("Add Quotation");
 
2140   # $::locale->text("Add Request for Quotation");
 
2141   # $::locale->text("Edit Sales Order");
 
2142   # $::locale->text("Edit Purchase Order");
 
2143   # $::locale->text("Edit Quotation");
 
2144   # $::locale->text("Edit Request for Quotation");
 
2146   $action = ucfirst(lc($action));
 
2147   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
2148        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
2149        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
2150        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
2154 sub get_item_cvpartnumber {
 
2155   my ($self, $item) = @_;
 
2157   return if !$self->search_cvpartnumber;
 
2158   return if !$self->order->customervendor;
 
2160   if ($self->cv eq 'vendor') {
 
2161     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2162     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2163   } elsif ($self->cv eq 'customer') {
 
2164     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2165     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2169 sub get_part_texts {
 
2170   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2172   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2173   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2175     description     => $defaults{description}     // $part->description,
 
2176     longdescription => $defaults{longdescription} // $part->notes,
 
2179   return $texts unless $language_id;
 
2181   my $translation = SL::DB::Manager::Translation->get_first(
 
2183       parts_id    => $part->id,
 
2184       language_id => $language_id,
 
2187   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2188   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2193 sub sales_order_type {
 
2197 sub purchase_order_type {
 
2201 sub sales_quotation_type {
 
2205 sub request_quotation_type {
 
2206   'request_quotation';
 
2210   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
2211        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
2212        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
2213        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
2217 sub save_and_redirect_to {
 
2218   my ($self, %params) = @_;
 
2220   my $errors = $self->save();
 
2222   if (scalar @{ $errors }) {
 
2223     $self->js->flash('error', $_) foreach @{ $errors };
 
2224     return $self->js->render();
 
2227   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
2228            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
2229            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
2230            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
2232   flash_later('info', $text);
 
2234   $self->redirect_to(%params, id => $self->order->id);
 
2238   my ($self, $addition) = @_;
 
2240   my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
 
2241   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2243   SL::DB::History->new(
 
2244     trans_id    => $self->order->id,
 
2245     employee_id => SL::DB::Manager::Employee->current->id,
 
2246     what_done   => $self->order->type,
 
2247     snumbers    => $snumbers,
 
2248     addition    => $addition,
 
2252 sub store_pdf_to_webdav_and_filemanagement {
 
2253   my($order, $content, $filename) = @_;
 
2257   # copy file to webdav folder
 
2258   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2259     my $webdav = SL::Webdav->new(
 
2260       type     => $order->type,
 
2261       number   => $order->number,
 
2263     my $webdav_file = SL::Webdav::File->new(
 
2265       filename => $filename,
 
2268       $webdav_file->store(data => \$content);
 
2271       push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
 
2274   if ($order->id && $::instance_conf->get_doc_storage) {
 
2276       SL::File->save(object_id     => $order->id,
 
2277                      object_type   => $order->type,
 
2278                      mime_type     => 'application/pdf',
 
2279                      source        => 'created',
 
2280                      file_type     => 'document',
 
2281                      file_name     => $filename,
 
2282                      file_contents => $content);
 
2285       push @errors, t8('Storing PDF in storage backend failed: #1', $@);
 
2300 SL::Controller::Order - controller for orders
 
2304 This is a new form to enter orders, completely rewritten with the use
 
2305 of controller and java script techniques.
 
2307 The aim is to provide the user a better experience and a faster workflow. Also
 
2308 the code should be more readable, more reliable and better to maintain.
 
2316 One input row, so that input happens every time at the same place.
 
2320 Use of pickers where possible.
 
2324 Possibility to enter more than one item at once.
 
2328 Item list in a scrollable area, so that the workflow buttons stay at
 
2333 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2334 possible (by partnumber, description, qty, sellprice and discount for now).
 
2338 No C<update> is necessary. All entries and calculations are managed
 
2339 with ajax-calls and the page only reloads on C<save>.
 
2343 User can see changes immediately, because of the use of java script
 
2354 =item * C<SL/Controller/Order.pm>
 
2358 =item * C<template/webpages/order/form.html>
 
2362 =item * C<template/webpages/order/tabs/basic_data.html>
 
2364 Main tab for basic_data.
 
2366 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2367 reused from generic code.
 
2371 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
2373 For displaying information on business type
 
2375 =item * C<template/webpages/order/tabs/_item_input.html>
 
2377 The input line for items
 
2379 =item * C<template/webpages/order/tabs/_row.html>
 
2381 One row for already entered items
 
2383 =item * C<template/webpages/order/tabs/_tax_row.html>
 
2385 Displaying tax information
 
2387 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
2389 Dialog for selecting price and discount sources
 
2393 =item * C<js/kivi.Order.js>
 
2395 java script functions
 
2405 =item * price sources: little symbols showing better price / better discount
 
2407 =item * select units in input row?
 
2409 =item * check for direct delivery (workflow sales order -> purchase order)
 
2411 =item * access rights
 
2413 =item * display weights
 
2417 =item * optional client/user behaviour
 
2419 (transactions has to be set - department has to be set -
 
2420  force project if enabled in client config - transport cost reminder)
 
2424 =head1 KNOWN BUGS AND CAVEATS
 
2430 Customer discount is not displayed as a valid discount in price source popup
 
2431 (this might be a bug in price sources)
 
2433 (I cannot reproduce this (Bernd))
 
2437 No indication that <shift>-up/down expands/collapses second row.
 
2441 Inline creation of parts is not currently supported
 
2445 Table header is not sticky in the scrolling area.
 
2449 Sorting does not include C<position>, neither does reordering.
 
2451 This behavior was implemented intentionally. But we can discuss, which behavior
 
2452 should be implemented.
 
2456 =head1 To discuss / Nice to have
 
2462 How to expand/collapse second row. Now it can be done clicking the icon or
 
2467 Possibility to select PriceSources in input row?
 
2471 This controller uses a (changed) copy of the template for the PriceSource
 
2472 dialog. Maybe there could be used one code source.
 
2476 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2477 form. This is not only a problem here, but also in all parts using the PTC.
 
2478 There exists a ticket and a patch. This patch should be testet.
 
2482 An indicator, if the actual inputs are saved (like in an
 
2483 editor or on text processing application).
 
2487 A warning when leaving the page without saveing unchanged inputs.
 
2494 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>