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   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
 
 987   my $callback     = $self->url_for(
 
 988     action       => 'return_from_create_part',
 
 989     type         => $self->type, # type is needed for check_auth on return
 
 990     previousform => $previousform,
 
 993   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.'));
 
 995   my @redirect_params = (
 
 996     controller => 'Part',
 
 998     part_type  => $::form->{add_item}->{create_part_type},
 
 999     callback   => $callback,
 
1003   $self->redirect_to(@redirect_params);
 
1006 sub action_return_from_create_part {
 
1009   $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
 
1011   $::auth->restore_form_from_session(delete $::form->{previousform});
 
1013   # set item ids to new fake id, to identify them as new items
 
1014   foreach my $item (@{$self->order->items_sorted}) {
 
1015     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1019   $self->get_unalterable_data();
 
1020   $self->pre_render();
 
1022   # trigger rendering values for second row/longdescription as hidden,
 
1023   # because they are loaded only on demand. So we need to keep the values
 
1025   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1026   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1030     title => $self->get_title_for('edit'),
 
1031     %{$self->{template_args}}
 
1036 # load the second row for one or more items
 
1038 # This action gets the html code for all items second rows by rendering a template for
 
1039 # the second row and sets the html code via client js.
 
1040 sub action_load_second_rows {
 
1043   $self->recalc() if $self->order->is_sales; # for margin calculation
 
1045   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1046     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1047     my $item = $self->order->items_sorted->[$idx];
 
1049     $self->js_load_second_row($item, $item_id, 0);
 
1052   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
1054   $self->js->render();
 
1057 # update description, notes and sellprice from master data
 
1058 sub action_update_row_from_master_data {
 
1061   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1062     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1063     my $item  = $self->order->items_sorted->[$idx];
 
1064     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1066     $item->description($texts->{description});
 
1067     $item->longdescription($texts->{longdescription});
 
1069     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1072     if ($item->part->is_assortment) {
 
1073     # add assortment items with price 0, as the components carry the price
 
1074       $price_src = $price_source->price_from_source("");
 
1075       $price_src->price(0);
 
1077       $price_src = $price_source->best_price
 
1078                  ? $price_source->best_price
 
1079                  : $price_source->price_from_source("");
 
1080       $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
 
1081       $price_src->price(0) if !$price_source->best_price;
 
1085     $item->sellprice($price_src->price);
 
1086     $item->active_price_source($price_src);
 
1089       ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
 
1090       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1091       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1092       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1094     if ($self->search_cvpartnumber) {
 
1095       $self->get_item_cvpartnumber($item);
 
1096       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1101   $self->js_redisplay_line_values;
 
1102   $self->js_redisplay_amounts_and_taxes;
 
1104   $self->js->render();
 
1107 sub js_load_second_row {
 
1108   my ($self, $item, $item_id, $do_parse) = @_;
 
1111     # Parse values from form (they are formated while rendering (template)).
 
1112     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1113     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1114     foreach my $var (@{ $item->cvars_by_config }) {
 
1115       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1117     $item->parse_custom_variable_values;
 
1120   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1123     ->html('#second_row_' . $item_id, $row_as_html)
 
1124     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1127 sub js_redisplay_line_values {
 
1130   my $is_sales = $self->order->is_sales;
 
1132   # sales orders with margins
 
1137        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1138        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1139        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1140       ]} @{ $self->order->items_sorted };
 
1144        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1145       ]} @{ $self->order->items_sorted };
 
1149     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1152 sub js_redisplay_amounts_and_taxes {
 
1155   if (scalar @{ $self->{taxes} }) {
 
1156     $self->js->show('#taxincluded_row_id');
 
1158     $self->js->hide('#taxincluded_row_id');
 
1161   if ($self->order->taxincluded) {
 
1162     $self->js->hide('#subtotal_row_id');
 
1164     $self->js->show('#subtotal_row_id');
 
1167   if ($self->order->is_sales) {
 
1168     my $is_neg = $self->order->marge_total < 0;
 
1170       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1171       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1172       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1173       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1174       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1175       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1176       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1177       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1181     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1182     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1183     ->remove('.tax_row')
 
1184     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1187 sub js_redisplay_cvpartnumbers {
 
1190   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1192   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1195     ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
 
1198 sub js_reset_order_and_item_ids_after_save {
 
1202     ->val('#id', $self->order->id)
 
1203     ->val('#converted_from_oe_id', '')
 
1204     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1207   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1208     next if !$self->order->items_sorted->[$idx]->id;
 
1209     next if $form_item_id !~ m{^new};
 
1211       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1212       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1213       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1217   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1224 sub init_valid_types {
 
1225   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1231   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1232     die "Not a valid type for order";
 
1235   $self->type($::form->{type});
 
1241   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1242          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1243          : die "Not a valid type for order";
 
1248 sub init_search_cvpartnumber {
 
1251   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1252   my $search_cvpartnumber;
 
1253   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1254   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1256   return $search_cvpartnumber;
 
1259 sub init_show_update_button {
 
1262   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1273 sub init_all_price_factors {
 
1274   SL::DB::Manager::PriceFactor->get_all;
 
1277 sub init_part_picker_classification_ids {
 
1279   my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
 
1281   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
 
1287   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1289   my $right   = $right_for->{ $self->type };
 
1290   $right    ||= 'DOES_NOT_EXIST';
 
1292   $::auth->assert($right);
 
1295 # build the selection box for contacts
 
1297 # Needed, if customer/vendor changed.
 
1298 sub build_contact_select {
 
1301   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1302     value_key  => 'cp_id',
 
1303     title_key  => 'full_name_dep',
 
1304     default    => $self->order->cp_id,
 
1306     style      => 'width: 300px',
 
1310 # build the selection box for shiptos
 
1312 # Needed, if customer/vendor changed.
 
1313 sub build_shipto_select {
 
1316   select_tag('order.shipto_id',
 
1317              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1318              value_key  => 'shipto_id',
 
1319              title_key  => 'displayable_id',
 
1320              default    => $self->order->shipto_id,
 
1322              style      => 'width: 300px',
 
1326 # build the inputs for the cusom shipto dialog
 
1328 # Needed, if customer/vendor changed.
 
1329 sub build_shipto_inputs {
 
1332   my $content = $self->p->render('common/_ship_to_dialog',
 
1333                                  vc_obj      => $self->order->customervendor,
 
1334                                  cs_obj      => $self->order->custom_shipto,
 
1335                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1336                                  id_selector => '#order_shipto_id');
 
1338   div_tag($content, id => 'shipto_inputs');
 
1341 # render the info line for business
 
1343 # Needed, if customer/vendor changed.
 
1344 sub build_business_info_row
 
1346   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1349 # build the rows for displaying taxes
 
1351 # Called if amounts where recalculated and redisplayed.
 
1352 sub build_tax_rows {
 
1356   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1357     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1359   return $rows_as_html;
 
1363 sub render_price_dialog {
 
1364   my ($self, $record_item) = @_;
 
1366   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1370       'kivi.io.price_chooser_dialog',
 
1371       t8('Available Prices'),
 
1372       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1377 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1378 #     $self->js->show('#dialog_flash_error');
 
1387   return if !$::form->{id};
 
1389   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1391   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1392   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1393   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1395   return $self->order;
 
1398 # load or create a new order object
 
1400 # And assign changes from the form to this object.
 
1401 # If the order is loaded from db, check if items are deleted in the form,
 
1402 # remove them form the object and collect them for removing from db on saving.
 
1403 # Then create/update items from form (via make_item) and add them.
 
1407   # add_items adds items to an order with no items for saving, but they cannot
 
1408   # be retrieved via items until the order is saved. Adding empty items to new
 
1409   # order here solves this problem.
 
1411   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1412   $order ||= SL::DB::Order->new(orderitems  => [],
 
1413                                 quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
 
1414                                 currency_id => $::instance_conf->get_currency_id(),);
 
1416   my $cv_id_method = $self->cv . '_id';
 
1417   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1418     $order->$cv_id_method($::form->{$cv_id_method});
 
1419     setup_order_from_cv($order);
 
1422   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1423   my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
 
1425   $order->assign_attributes(%{$::form->{order}});
 
1427   $self->setup_custom_shipto_from_form($order, $::form);
 
1429   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1430     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1431     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1434   # remove deleted items
 
1435   $self->item_ids_to_delete([]);
 
1436   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1437     my $item = $order->orderitems->[$idx];
 
1438     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1439       splice @{$order->orderitems}, $idx, 1;
 
1440       push @{$self->item_ids_to_delete}, $item->id;
 
1446   foreach my $form_attr (@{$form_orderitems}) {
 
1447     my $item = make_item($order, $form_attr);
 
1448     $item->position($pos);
 
1452   $order->add_items(grep {!$_->id} @items);
 
1457 # create or update items from form
 
1459 # Make item objects from form values. For items already existing read from db.
 
1460 # Create a new item else. And assign attributes.
 
1462   my ($record, $attr) = @_;
 
1465   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1467   my $is_new = !$item;
 
1469   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1470   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1471   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1472   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1474   $item->assign_attributes(%$attr);
 
1477     my $texts = get_part_texts($item->part, $record->language_id);
 
1478     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1479     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1480     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1488 # This is used to add one item
 
1490   my ($record, $attr) = @_;
 
1492   my $item = SL::DB::OrderItem->new;
 
1494   # Remove attributes where the user left or set the inputs empty.
 
1495   # So these attributes will be undefined and we can distinguish them
 
1496   # from zero later on.
 
1497   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1498     delete $attr->{$_} if $attr->{$_} eq '';
 
1501   $item->assign_attributes(%$attr);
 
1503   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1504   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1506   $item->unit($part->unit) if !$item->unit;
 
1509   if ( $part->is_assortment ) {
 
1510     # add assortment items with price 0, as the components carry the price
 
1511     $price_src = $price_source->price_from_source("");
 
1512     $price_src->price(0);
 
1513   } elsif (defined $item->sellprice) {
 
1514     $price_src = $price_source->price_from_source("");
 
1515     $price_src->price($item->sellprice);
 
1517     $price_src = $price_source->best_price
 
1518                ? $price_source->best_price
 
1519                : $price_source->price_from_source("");
 
1520     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
1521     $price_src->price(0) if !$price_source->best_price;
 
1525   if (defined $item->discount) {
 
1526     $discount_src = $price_source->discount_from_source("");
 
1527     $discount_src->discount($item->discount);
 
1529     $discount_src = $price_source->best_discount
 
1530                   ? $price_source->best_discount
 
1531                   : $price_source->discount_from_source("");
 
1532     $discount_src->discount(0) if !$price_source->best_discount;
 
1536   $new_attr{part}                   = $part;
 
1537   $new_attr{description}            = $part->description     if ! $item->description;
 
1538   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1539   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1540   $new_attr{sellprice}              = $price_src->price;
 
1541   $new_attr{discount}               = $discount_src->discount;
 
1542   $new_attr{active_price_source}    = $price_src;
 
1543   $new_attr{active_discount_source} = $discount_src;
 
1544   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1545   $new_attr{project_id}             = $record->globalproject_id;
 
1546   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1548   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1549   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1550   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1551   $new_attr{custom_variables} = [];
 
1553   my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1555   $item->assign_attributes(%new_attr, %{ $texts });
 
1560 sub setup_order_from_cv {
 
1563   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
 
1565   $order->intnotes($order->customervendor->notes);
 
1567   if ($order->is_sales) {
 
1568     $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1569     $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1570                         ? $order->customer->taxincluded_checked
 
1571                         : $::myconfig{taxincluded_checked});
 
1576 # setup custom shipto from form
 
1578 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1579 # with 'shiptocvar_'.
 
1580 # Mark it to be deleted if a shipto from master data is selected
 
1581 # (i.e. order has a shipto).
 
1582 # Else, update or create a new custom shipto. If the fields are empty, it
 
1583 # will not be saved on save.
 
1584 sub setup_custom_shipto_from_form {
 
1585   my ($self, $order, $form) = @_;
 
1587   if ($order->shipto) {
 
1588     $self->is_custom_shipto_to_delete(1);
 
1590     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1592     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1593     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1595     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1596     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1600 # recalculate prices and taxes
 
1602 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1606   my %pat = $self->order->calculate_prices_and_taxes();
 
1608   $self->{taxes} = [];
 
1609   foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
 
1610     my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
1612     push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
 
1613                                 netamount => $netamount,
 
1614                                 tax       => SL::DB::Tax->new(id => $tax_id)->load });
 
1616   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1619 # get data for saving, printing, ..., that is not changed in the form
 
1621 # Only cvars for now.
 
1622 sub get_unalterable_data {
 
1625   foreach my $item (@{ $self->order->items }) {
 
1626     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1627     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1628     foreach my $var (@{ $item->cvars_by_config }) {
 
1629       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1631     $item->parse_custom_variable_values;
 
1637 # And remove related files in the spool directory
 
1642   my $db     = $self->order->db;
 
1644   $db->with_transaction(
 
1646       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1647       $self->order->delete;
 
1648       my $spool = $::lx_office_conf{paths}->{spool};
 
1649       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1651       $self->save_history('DELETED');
 
1654   }) || push(@{$errors}, $db->error);
 
1661 # And delete items that are deleted in the form.
 
1666   my $db     = $self->order->db;
 
1668   $db->with_transaction(sub {
 
1669     # delete custom shipto if it is to be deleted or if it is empty
 
1670     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1671       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1672       $self->order->custom_shipto(undef);
 
1675     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1676     $self->order->save(cascade => 1);
 
1679     if ($::form->{converted_from_oe_id}) {
 
1680       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1681       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1682         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1683         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1684         $src->link_to_record($self->order);
 
1686       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1688         foreach (@{ $self->order->items_sorted }) {
 
1689           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1691           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1692                                   from_id    => $from_id,
 
1693                                   to_table   => 'orderitems',
 
1701     $self->save_history('SAVED');
 
1704   }) || push(@{$errors}, $db->error);
 
1709 sub workflow_sales_or_request_for_quotation {
 
1713   my $errors = $self->save();
 
1715   if (scalar @{ $errors }) {
 
1716     $self->js->flash('error', $_) for @{ $errors };
 
1717     return $self->js->render();
 
1720   my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
 
1722   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1723   $self->{converted_from_oe_id} = delete $::form->{id};
 
1725   # set item ids to new fake id, to identify them as new items
 
1726   foreach my $item (@{$self->order->items_sorted}) {
 
1727     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1731   $::form->{type} = $destination_type;
 
1732   $self->type($self->init_type);
 
1733   $self->cv  ($self->init_cv);
 
1737   $self->get_unalterable_data();
 
1738   $self->pre_render();
 
1740   # trigger rendering values for second row as hidden, because they
 
1741   # are loaded only on demand. So we need to keep the values from the
 
1743   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1747     title => $self->get_title_for('edit'),
 
1748     %{$self->{template_args}}
 
1752 sub workflow_sales_or_purchase_order {
 
1756   my $errors = $self->save();
 
1758   if (scalar @{ $errors }) {
 
1759     $self->js->flash('error', $_) foreach @{ $errors };
 
1760     return $self->js->render();
 
1763   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1764                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1765                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1766                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1769   # check for direct delivery
 
1770   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1772   if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
 
1773       && $::form->{use_shipto} && $self->order->shipto) {
 
1774     $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
 
1777   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1778   $self->{converted_from_oe_id} = delete $::form->{id};
 
1780   # set item ids to new fake id, to identify them as new items
 
1781   foreach my $item (@{$self->order->items_sorted}) {
 
1782     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1785   if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
 
1786     if ($::form->{use_shipto}) {
 
1787       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
1789       # remove any custom shipto if not wanted
 
1790       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1795   $::form->{type} = $destination_type;
 
1796   $self->type($self->init_type);
 
1797   $self->cv  ($self->init_cv);
 
1801   $self->get_unalterable_data();
 
1802   $self->pre_render();
 
1804   # trigger rendering values for second row as hidden, because they
 
1805   # are loaded only on demand. So we need to keep the values from the
 
1807   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1811     title => $self->get_title_for('edit'),
 
1812     %{$self->{template_args}}
 
1820   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1821   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
1822   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1823   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted();
 
1824   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1827   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1830   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1832   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1833   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1834   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1835   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1836   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1838   my $print_form = Form->new('');
 
1839   $print_form->{type}        = $self->type;
 
1840   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
1841   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
1842     form => $print_form,
 
1843     options => {dialog_name_prefix => 'print_options.',
 
1847                 no_opendocument    => 0,
 
1851   foreach my $item (@{$self->order->orderitems}) {
 
1852     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1853     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1854     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1857   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1858     # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
 
1859     # Do not use write_to_objects to prevent order->delivered to be set, because this should be
 
1860     # the value from db, which can be set manually or is set when linked delivery orders are saved.
 
1861     SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
 
1864   if ($self->order->number && $::instance_conf->get_webdav) {
 
1865     my $webdav = SL::Webdav->new(
 
1866       type     => $self->type,
 
1867       number   => $self->order->number,
 
1869     my @all_objects = $webdav->get_all_objects;
 
1870     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1872                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1876   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1878   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
1879                                                          edit_periodic_invoices_config calculate_qty kivi.Validator follow_up show_history);
 
1880   $self->setup_edit_action_bar;
 
1883 sub setup_edit_action_bar {
 
1884   my ($self, %params) = @_;
 
1886   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
1887                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1888                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1890   for my $bar ($::request->layout->get('actionbar')) {
 
1895           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1896                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
1898           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'] ],
 
1902           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1903           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1904           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1906       ], # end of combobox "Save"
 
1913           t8('Save and Quotation'),
 
1914           submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
 
1915           only_if  => (any { $self->type eq $_ } (sales_order_type())),
 
1919           submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
 
1920           only_if  => (any { $self->type eq $_ } (purchase_order_type())),
 
1923           t8('Save and Sales Order'),
 
1924           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
1925           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
1928           t8('Save and Purchase Order'),
 
1929           call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
 
1930           only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
1933           t8('Save and Delivery Order'),
 
1934           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
1935                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1937           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1938           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
 
1941           t8('Save and Invoice'),
 
1942           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1943           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1946           t8('Save and AP Transaction'),
 
1947           call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
1948           only_if   => (any { $self->type eq $_ } (purchase_order_type()))
 
1951       ], # end of combobox "Workflow"
 
1958           t8('Save and preview PDF'),
 
1959            call => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
1960                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1964           t8('Save and print'),
 
1965           call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
1966                                                      $::instance_conf->get_order_warn_no_deliverydate,
 
1970           t8('Save and E-mail'),
 
1971           id   => 'save_and_email_action',
 
1972           call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
1973                                                                      $::instance_conf->get_order_warn_no_deliverydate,
 
1975           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1978           t8('Download attachments of all parts'),
 
1979           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1980           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1981           only_if  => $::instance_conf->get_doc_storage,
 
1983       ], # end of combobox "Export"
 
1987         call     => [ 'kivi.Order.delete_order' ],
 
1988         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1989         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1990         only_if  => $deletion_allowed,
 
1999           call     => [ 'kivi.Order.follow_up_window' ],
 
2000           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2001           only_if  => $::auth->assert('productivity', 1),
 
2005           call     => [ 'set_history_window', $self->order->id, 'id' ],
 
2006           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
 
2008       ], # end of combobox "more"
 
2014   my ($order, $pdf_ref, $params) = @_;
 
2018   my $print_form = Form->new('');
 
2019   $print_form->{type}        = $order->type;
 
2020   $print_form->{formname}    = $params->{formname} || $order->type;
 
2021   $print_form->{format}      = $params->{format}   || 'pdf';
 
2022   $print_form->{media}       = $params->{media}    || 'file';
 
2023   $print_form->{groupitems}  = $params->{groupitems};
 
2024   $print_form->{printer_id}  = $params->{printer_id};
 
2025   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
2027   $order->language($params->{language});
 
2028   $order->flatten_to_form($print_form, format_amounts => 1);
 
2032   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
2033     $template_ext  = 'odt';
 
2034     $template_type = 'OpenDocument';
 
2037   # search for the template
 
2038   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
2039     name        => $print_form->{formname},
 
2040     extension   => $template_ext,
 
2041     email       => $print_form->{media} eq 'email',
 
2042     language    => $params->{language},
 
2043     printer_id  => $print_form->{printer_id},
 
2046   if (!defined $template_file) {
 
2047     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);
 
2050   return @errors if scalar @errors;
 
2052   $print_form->throw_on_error(sub {
 
2054       $print_form->prepare_for_printing;
 
2056       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
2057         format        => $print_form->{format},
 
2058         template_type => $template_type,
 
2059         template      => $template_file,
 
2060         variables     => $print_form,
 
2061         variable_content_types => {
 
2062           longdescription => 'html',
 
2063           partnotes       => 'html',
 
2068     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2074 sub get_files_for_email_dialog {
 
2077   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2079   return %files if !$::instance_conf->get_doc_storage;
 
2081   if ($self->order->id) {
 
2082     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2083     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2084     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2085     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2089     uniq_by { $_->{id} }
 
2091       +{ id         => $_->part->id,
 
2092          partnumber => $_->part->partnumber }
 
2093     } @{$self->order->items_sorted};
 
2095   foreach my $part (@parts) {
 
2096     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2097     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2100   foreach my $key (keys %files) {
 
2101     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2107 sub make_periodic_invoices_config_from_yaml {
 
2108   my ($yaml_config) = @_;
 
2110   return if !$yaml_config;
 
2111   my $attr = SL::YAML::Load($yaml_config);
 
2112   return if 'HASH' ne ref $attr;
 
2113   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
2117 sub get_periodic_invoices_status {
 
2118   my ($self, $config) = @_;
 
2120   return                      if $self->type ne sales_order_type();
 
2121   return t8('not configured') if !$config;
 
2123   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
2124              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
2125              :                                                     die "Cannot get status of periodic invoices config";
 
2127   return $active ? t8('active') : t8('inactive');
 
2131   my ($self, $action) = @_;
 
2133   return '' if none { lc($action)} qw(add edit);
 
2136   # $::locale->text("Add Sales Order");
 
2137   # $::locale->text("Add Purchase Order");
 
2138   # $::locale->text("Add Quotation");
 
2139   # $::locale->text("Add Request for Quotation");
 
2140   # $::locale->text("Edit Sales Order");
 
2141   # $::locale->text("Edit Purchase Order");
 
2142   # $::locale->text("Edit Quotation");
 
2143   # $::locale->text("Edit Request for Quotation");
 
2145   $action = ucfirst(lc($action));
 
2146   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
2147        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
2148        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
2149        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
2153 sub get_item_cvpartnumber {
 
2154   my ($self, $item) = @_;
 
2156   return if !$self->search_cvpartnumber;
 
2157   return if !$self->order->customervendor;
 
2159   if ($self->cv eq 'vendor') {
 
2160     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2161     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2162   } elsif ($self->cv eq 'customer') {
 
2163     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2164     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2168 sub get_part_texts {
 
2169   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2171   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2172   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2174     description     => $defaults{description}     // $part->description,
 
2175     longdescription => $defaults{longdescription} // $part->notes,
 
2178   return $texts unless $language_id;
 
2180   my $translation = SL::DB::Manager::Translation->get_first(
 
2182       parts_id    => $part->id,
 
2183       language_id => $language_id,
 
2186   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2187   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2192 sub sales_order_type {
 
2196 sub purchase_order_type {
 
2200 sub sales_quotation_type {
 
2204 sub request_quotation_type {
 
2205   'request_quotation';
 
2209   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
2210        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
2211        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
2212        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
2216 sub save_and_redirect_to {
 
2217   my ($self, %params) = @_;
 
2219   my $errors = $self->save();
 
2221   if (scalar @{ $errors }) {
 
2222     $self->js->flash('error', $_) foreach @{ $errors };
 
2223     return $self->js->render();
 
2226   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
2227            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
2228            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
2229            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
2231   flash_later('info', $text);
 
2233   $self->redirect_to(%params, id => $self->order->id);
 
2237   my ($self, $addition) = @_;
 
2239   my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
 
2240   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2242   SL::DB::History->new(
 
2243     trans_id    => $self->order->id,
 
2244     employee_id => SL::DB::Manager::Employee->current->id,
 
2245     what_done   => $self->order->type,
 
2246     snumbers    => $snumbers,
 
2247     addition    => $addition,
 
2251 sub store_pdf_to_webdav_and_filemanagement {
 
2252   my($order, $content, $filename) = @_;
 
2256   # copy file to webdav folder
 
2257   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2258     my $webdav = SL::Webdav->new(
 
2259       type     => $order->type,
 
2260       number   => $order->number,
 
2262     my $webdav_file = SL::Webdav::File->new(
 
2264       filename => $filename,
 
2267       $webdav_file->store(data => \$content);
 
2270       push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
 
2273   if ($order->id && $::instance_conf->get_doc_storage) {
 
2275       SL::File->save(object_id     => $order->id,
 
2276                      object_type   => $order->type,
 
2277                      mime_type     => 'application/pdf',
 
2278                      source        => 'created',
 
2279                      file_type     => 'document',
 
2280                      file_name     => $filename,
 
2281                      file_contents => $content);
 
2284       push @errors, t8('Storing PDF in storage backend failed: #1', $@);
 
2299 SL::Controller::Order - controller for orders
 
2303 This is a new form to enter orders, completely rewritten with the use
 
2304 of controller and java script techniques.
 
2306 The aim is to provide the user a better experience and a faster workflow. Also
 
2307 the code should be more readable, more reliable and better to maintain.
 
2315 One input row, so that input happens every time at the same place.
 
2319 Use of pickers where possible.
 
2323 Possibility to enter more than one item at once.
 
2327 Item list in a scrollable area, so that the workflow buttons stay at
 
2332 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2333 possible (by partnumber, description, qty, sellprice and discount for now).
 
2337 No C<update> is necessary. All entries and calculations are managed
 
2338 with ajax-calls and the page only reloads on C<save>.
 
2342 User can see changes immediately, because of the use of java script
 
2353 =item * C<SL/Controller/Order.pm>
 
2357 =item * C<template/webpages/order/form.html>
 
2361 =item * C<template/webpages/order/tabs/basic_data.html>
 
2363 Main tab for basic_data.
 
2365 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2366 reused from generic code.
 
2370 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
2372 For displaying information on business type
 
2374 =item * C<template/webpages/order/tabs/_item_input.html>
 
2376 The input line for items
 
2378 =item * C<template/webpages/order/tabs/_row.html>
 
2380 One row for already entered items
 
2382 =item * C<template/webpages/order/tabs/_tax_row.html>
 
2384 Displaying tax information
 
2386 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
2388 Dialog for selecting price and discount sources
 
2392 =item * C<js/kivi.Order.js>
 
2394 java script functions
 
2404 =item * price sources: little symbols showing better price / better discount
 
2406 =item * select units in input row?
 
2408 =item * check for direct delivery (workflow sales order -> purchase order)
 
2410 =item * access rights
 
2412 =item * display weights
 
2416 =item * optional client/user behaviour
 
2418 (transactions has to be set - department has to be set -
 
2419  force project if enabled in client config - transport cost reminder)
 
2423 =head1 KNOWN BUGS AND CAVEATS
 
2429 Customer discount is not displayed as a valid discount in price source popup
 
2430 (this might be a bug in price sources)
 
2432 (I cannot reproduce this (Bernd))
 
2436 No indication that <shift>-up/down expands/collapses second row.
 
2440 Inline creation of parts is not currently supported
 
2444 Table header is not sticky in the scrolling area.
 
2448 Sorting does not include C<position>, neither does reordering.
 
2450 This behavior was implemented intentionally. But we can discuss, which behavior
 
2451 should be implemented.
 
2455 =head1 To discuss / Nice to have
 
2461 How to expand/collapse second row. Now it can be done clicking the icon or
 
2466 Possibility to select PriceSources in input row?
 
2470 This controller uses a (changed) copy of the template for the PriceSource
 
2471 dialog. Maybe there could be used one code source.
 
2475 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2476 form. This is not only a problem here, but also in all parts using the PTC.
 
2477 There exists a ticket and a patch. This patch should be testet.
 
2481 An indicator, if the actual inputs are saved (like in an
 
2482 editor or on text processing application).
 
2486 A warning when leaving the page without saveing unchanged inputs.
 
2493 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>