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   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 427                                   email_form  => $email_form,
 
 428                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
 
 430                                   is_customer => $self->cv eq 'customer',
 
 434       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 441 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 442 sub action_send_email {
 
 445   my $errors = $self->save();
 
 447   if (scalar @{ $errors }) {
 
 448     $self->js->run('kivi.Order.close_email_dialog');
 
 449     $self->js->flash('error', $_) foreach @{ $errors };
 
 450     return $self->js->render();
 
 453   $self->js_reset_order_and_item_ids_after_save;
 
 455   my $email_form  = delete $::form->{email_form};
 
 456   my %field_names = (to => 'email');
 
 458   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 460   # for Form::cleanup which may be called in Form::send_email
 
 461   $::form->{cwd}    = getcwd();
 
 462   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 464   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 465   $::form->{media}  = 'email';
 
 467   if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
 
 469     my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
 
 470                                                     format     => $::form->{print_options}->{format},
 
 471                                                     formname   => $::form->{print_options}->{formname},
 
 472                                                     language   => $self->order->language,
 
 473                                                     printer_id => $::form->{print_options}->{printer_id},
 
 474                                                     groupitems => $::form->{print_options}->{groupitems}});
 
 475     if (scalar @errors) {
 
 476       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
 
 479     my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $::form->{attachment_filename});
 
 480     if (scalar @warnings) {
 
 481       flash_later('warning', $_) for @warnings;
 
 484     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 485     $sfile->fh->print($pdf);
 
 488     $::form->{tmpfile} = $sfile->file_name;
 
 489     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 492   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 493   $::form->send_email(\%::myconfig, 'pdf');
 
 496   my $intnotes = $self->order->intnotes;
 
 497   $intnotes   .= "\n\n" if $self->order->intnotes;
 
 498   $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 499   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 500   $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 501   $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 502   $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 503   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 504   $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
 506   $self->order->update_attributes(intnotes => $intnotes);
 
 508   $self->save_history('MAILED');
 
 510   flash_later('info', t8('The email has been sent.'));
 
 512   my @redirect_params = (
 
 515     id     => $self->order->id,
 
 518   $self->redirect_to(@redirect_params);
 
 521 # open the periodic invoices config dialog
 
 523 # If there are values in the form (i.e. dialog was opened before),
 
 524 # then use this values. Create new ones, else.
 
 525 sub action_show_periodic_invoices_config_dialog {
 
 528   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 529   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 530   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 531                                                    order_value_periodicity => 'p', # = same as periodicity
 
 532                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 533                                                    extend_automatically_by => 12,
 
 535                                                    email_subject           => GenericTranslations->get(
 
 536                                                                                 language_id      => $::form->{language_id},
 
 537                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 538                                                    email_body              => GenericTranslations->get(
 
 539                                                                                 language_id      => $::form->{language_id},
 
 540                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 542   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 543   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 545   $::form->get_lists(printers => "ALL_PRINTERS",
 
 546                      charts   => { key       => 'ALL_CHARTS',
 
 547                                    transdate => 'current_date' });
 
 549   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 551   if ($::form->{customer_id}) {
 
 552     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 553     $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail;
 
 556   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 558                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 559                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 564 # assign the values of the periodic invoices config dialog
 
 565 # as yaml in the hidden tag and set the status.
 
 566 sub action_assign_periodic_invoices_config {
 
 569   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 571   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 572                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 573                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 574                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 575                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 576                  start_date_as_date         => $::form->{start_date_as_date},
 
 577                  end_date_as_date           => $::form->{end_date_as_date},
 
 578                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 579                  print                      => $::form->{print}      ? 1                         : 0,
 
 580                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 581                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 582                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 583                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 584                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 585                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 586                  email_recipient_address    => $::form->{email_recipient_address},
 
 587                  email_sender               => $::form->{email_sender},
 
 588                  email_subject              => $::form->{email_subject},
 
 589                  email_body                 => $::form->{email_body},
 
 592   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 594   my $status = $self->get_periodic_invoices_status($config);
 
 597     ->remove('#order_periodic_invoices_config')
 
 598     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 599     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 600     ->html('#periodic_invoices_status', $status)
 
 601     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 605 sub action_get_has_active_periodic_invoices {
 
 608   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 609   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 611   my $has_active_periodic_invoices =
 
 612        $self->type eq sales_order_type()
 
 615     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 616     && $config->get_previous_billed_period_start_date;
 
 618   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 621 # save the order and redirect to the frontend subroutine for a new
 
 623 sub action_save_and_delivery_order {
 
 626   $self->save_and_redirect_to(
 
 627     controller => 'oe.pl',
 
 628     action     => 'oe_delivery_order_from_order',
 
 632 # save the order and redirect to the frontend subroutine for a new
 
 634 sub action_save_and_invoice {
 
 637   $self->save_and_redirect_to(
 
 638     controller => 'oe.pl',
 
 639     action     => 'oe_invoice_from_order',
 
 643 # workflow from sales order to sales quotation
 
 644 sub action_sales_quotation {
 
 645   $_[0]->workflow_sales_or_request_for_quotation();
 
 648 # workflow from sales order to sales quotation
 
 649 sub action_request_for_quotation {
 
 650   $_[0]->workflow_sales_or_request_for_quotation();
 
 653 # workflow from sales quotation to sales order
 
 654 sub action_sales_order {
 
 655   $_[0]->workflow_sales_or_purchase_order();
 
 658 # workflow from rfq to purchase order
 
 659 sub action_purchase_order {
 
 660   $_[0]->workflow_sales_or_purchase_order();
 
 663 # workflow from purchase order to ap transaction
 
 664 sub action_save_and_ap_transaction {
 
 667   $self->save_and_redirect_to(
 
 668     controller => 'ap.pl',
 
 669     action     => 'add_from_purchase_order',
 
 673 # set form elements in respect to a changed customer or vendor
 
 675 # This action is called on an change of the customer/vendor picker.
 
 676 sub action_customer_vendor_changed {
 
 679   setup_order_from_cv($self->order);
 
 682   my $cv_method = $self->cv;
 
 684   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 685     $self->js->show('#cp_row');
 
 687     $self->js->hide('#cp_row');
 
 690   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 691     $self->js->show('#shipto_selection');
 
 693     $self->js->hide('#shipto_selection');
 
 696   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 699     ->replaceWith('#order_cp_id',            $self->build_contact_select)
 
 700     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
 
 701     ->replaceWith('#shipto_inputs  ',        $self->build_shipto_inputs)
 
 702     ->replaceWith('#business_info_row',      $self->build_business_info_row)
 
 703     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
 
 704     ->val(        '#order_taxincluded',      $self->order->taxincluded)
 
 705     ->val(        '#order_currency_id',      $self->order->currency_id)
 
 706     ->val(        '#order_payment_id',       $self->order->payment_id)
 
 707     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
 
 708     ->val(        '#order_intnotes',         $self->order->intnotes)
 
 709     ->val(        '#order_language_id',      $self->order->$cv_method->language_id)
 
 710     ->focus(      '#order_' . $self->cv . '_id')
 
 711     ->run('kivi.Order.update_exchangerate');
 
 713   $self->js_redisplay_amounts_and_taxes;
 
 714   $self->js_redisplay_cvpartnumbers;
 
 718 # open the dialog for customer/vendor details
 
 719 sub action_show_customer_vendor_details_dialog {
 
 722   my $is_customer = 'customer' eq $::form->{vc};
 
 725     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 727     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 730   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 731   $details{discount_as_percent} = $cv->discount_as_percent;
 
 732   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 733   $details{business}            = $cv->business->description      if $cv->business;
 
 734   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 735   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 736   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 737   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 739   foreach my $entry (@{ $cv->shipto }) {
 
 740     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 742   foreach my $entry (@{ $cv->contacts }) {
 
 743     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 746   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 747                 is_customer => $is_customer,
 
 752 # called if a unit in an existing item row is changed
 
 753 sub action_unit_changed {
 
 756   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 757   my $item = $self->order->items_sorted->[$idx];
 
 759   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 760   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 765     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 766   $self->js_redisplay_line_values;
 
 767   $self->js_redisplay_amounts_and_taxes;
 
 771 # add an item row for a new item entered in the input row
 
 772 sub action_add_item {
 
 775   my $form_attr = $::form->{add_item};
 
 777   return unless $form_attr->{parts_id};
 
 779   my $item = new_item($self->order, $form_attr);
 
 781   $self->order->add_items($item);
 
 785   $self->get_item_cvpartnumber($item);
 
 787   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 788   my $row_as_html = $self->p->render('order/tabs/_row',
 
 794   if ($::form->{insert_before_item_id}) {
 
 796       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 799       ->append('#row_table_id', $row_as_html);
 
 802   if ( $item->part->is_assortment ) {
 
 803     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 804     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 805       my $attr = { parts_id => $assortment_item->parts_id,
 
 806                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 807                    unit     => $assortment_item->unit,
 
 808                    description => $assortment_item->part->description,
 
 810       my $item = new_item($self->order, $attr);
 
 812       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 813       $item->discount(1) unless $assortment_item->charge;
 
 815       $self->order->add_items( $item );
 
 817       $self->get_item_cvpartnumber($item);
 
 818       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 819       my $row_as_html = $self->p->render('order/tabs/_row',
 
 824       if ($::form->{insert_before_item_id}) {
 
 826           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 829           ->append('#row_table_id', $row_as_html);
 
 835     ->val('.add_item_input', '')
 
 836     ->run('kivi.Order.init_row_handlers')
 
 837     ->run('kivi.Order.renumber_positions')
 
 838     ->focus('#add_item_parts_id_name');
 
 840   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 842   $self->js_redisplay_amounts_and_taxes;
 
 846 # add item rows for multiple items at once
 
 847 sub action_add_multi_items {
 
 850   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
 
 851   return $self->js->render() unless scalar @form_attr;
 
 854   foreach my $attr (@form_attr) {
 
 855     my $item = new_item($self->order, $attr);
 
 857     if ( $item->part->is_assortment ) {
 
 858       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 859         my $attr = { parts_id => $assortment_item->parts_id,
 
 860                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 861                      unit     => $assortment_item->unit,
 
 862                      description => $assortment_item->part->description,
 
 864         my $item = new_item($self->order, $attr);
 
 866         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 867         $item->discount(1) unless $assortment_item->charge;
 
 872   $self->order->add_items(@items);
 
 876   foreach my $item (@items) {
 
 877     $self->get_item_cvpartnumber($item);
 
 878     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 879     my $row_as_html = $self->p->render('order/tabs/_row',
 
 885     if ($::form->{insert_before_item_id}) {
 
 887         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 890         ->append('#row_table_id', $row_as_html);
 
 895     ->run('kivi.Part.close_picker_dialogs')
 
 896     ->run('kivi.Order.init_row_handlers')
 
 897     ->run('kivi.Order.renumber_positions')
 
 898     ->focus('#add_item_parts_id_name');
 
 900   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 902   $self->js_redisplay_amounts_and_taxes;
 
 906 # recalculate all linetotals, amounts and taxes and redisplay them
 
 907 sub action_recalc_amounts_and_taxes {
 
 912   $self->js_redisplay_line_values;
 
 913   $self->js_redisplay_amounts_and_taxes;
 
 917 sub action_update_exchangerate {
 
 921     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
 922     currency_name => $self->order->currency->name,
 
 923     exchangerate  => $self->order->daily_exchangerate_as_null_number,
 
 926   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
 929 # redisplay item rows if they are sorted by an attribute
 
 930 sub action_reorder_items {
 
 934     partnumber   => sub { $_[0]->part->partnumber },
 
 935     description  => sub { $_[0]->description },
 
 936     qty          => sub { $_[0]->qty },
 
 937     sellprice    => sub { $_[0]->sellprice },
 
 938     discount     => sub { $_[0]->discount },
 
 939     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
 942   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
 944   my $method = $sort_keys{$::form->{order_by}};
 
 945   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
 946   if ($::form->{sort_dir}) {
 
 947     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 948       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
 950       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 953     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 954       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
 956       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 960     ->run('kivi.Order.redisplay_items', \@to_sort)
 
 964 # show the popup to choose a price/discount source
 
 965 sub action_price_popup {
 
 968   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 969   my $item = $self->order->items_sorted->[$idx];
 
 971   $self->render_price_dialog($item);
 
 974 # load the second row for one or more items
 
 976 # This action gets the html code for all items second rows by rendering a template for
 
 977 # the second row and sets the html code via client js.
 
 978 sub action_load_second_rows {
 
 981   $self->recalc() if $self->order->is_sales; # for margin calculation
 
 983   foreach my $item_id (@{ $::form->{item_ids} }) {
 
 984     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
 985     my $item = $self->order->items_sorted->[$idx];
 
 987     $self->js_load_second_row($item, $item_id, 0);
 
 990   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
 995 # update description, notes and sellprice from master data
 
 996 sub action_update_row_from_master_data {
 
 999   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1000     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1001     my $item  = $self->order->items_sorted->[$idx];
 
1002     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1004     $item->description($texts->{description});
 
1005     $item->longdescription($texts->{longdescription});
 
1007     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1010     if ($item->part->is_assortment) {
 
1011     # add assortment items with price 0, as the components carry the price
 
1012       $price_src = $price_source->price_from_source("");
 
1013       $price_src->price(0);
 
1015       $price_src = $price_source->best_price
 
1016                  ? $price_source->best_price
 
1017                  : $price_source->price_from_source("");
 
1018       $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
 
1019       $price_src->price(0) if !$price_source->best_price;
 
1023     $item->sellprice($price_src->price);
 
1024     $item->active_price_source($price_src);
 
1027       ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
 
1028       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1029       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1030       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1032     if ($self->search_cvpartnumber) {
 
1033       $self->get_item_cvpartnumber($item);
 
1034       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1039   $self->js_redisplay_line_values;
 
1040   $self->js_redisplay_amounts_and_taxes;
 
1042   $self->js->render();
 
1045 sub js_load_second_row {
 
1046   my ($self, $item, $item_id, $do_parse) = @_;
 
1049     # Parse values from form (they are formated while rendering (template)).
 
1050     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1051     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1052     foreach my $var (@{ $item->cvars_by_config }) {
 
1053       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1055     $item->parse_custom_variable_values;
 
1058   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1061     ->html('#second_row_' . $item_id, $row_as_html)
 
1062     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1065 sub js_redisplay_line_values {
 
1068   my $is_sales = $self->order->is_sales;
 
1070   # sales orders with margins
 
1075        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1076        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1077        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1078       ]} @{ $self->order->items_sorted };
 
1082        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1083       ]} @{ $self->order->items_sorted };
 
1087     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1090 sub js_redisplay_amounts_and_taxes {
 
1093   if (scalar @{ $self->{taxes} }) {
 
1094     $self->js->show('#taxincluded_row_id');
 
1096     $self->js->hide('#taxincluded_row_id');
 
1099   if ($self->order->taxincluded) {
 
1100     $self->js->hide('#subtotal_row_id');
 
1102     $self->js->show('#subtotal_row_id');
 
1105   if ($self->order->is_sales) {
 
1106     my $is_neg = $self->order->marge_total < 0;
 
1108       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1109       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1110       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1111       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1112       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1113       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1114       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1115       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1119     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1120     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1121     ->remove('.tax_row')
 
1122     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1125 sub js_redisplay_cvpartnumbers {
 
1128   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1130   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1133     ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
 
1136 sub js_reset_order_and_item_ids_after_save {
 
1140     ->val('#id', $self->order->id)
 
1141     ->val('#converted_from_oe_id', '')
 
1142     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1145   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1146     next if !$self->order->items_sorted->[$idx]->id;
 
1147     next if $form_item_id !~ m{^new};
 
1149       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1150       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1151       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1155   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1162 sub init_valid_types {
 
1163   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1169   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1170     die "Not a valid type for order";
 
1173   $self->type($::form->{type});
 
1179   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1180          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1181          : die "Not a valid type for order";
 
1186 sub init_search_cvpartnumber {
 
1189   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1190   my $search_cvpartnumber;
 
1191   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1192   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1194   return $search_cvpartnumber;
 
1197 sub init_show_update_button {
 
1200   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1211 sub init_all_price_factors {
 
1212   SL::DB::Manager::PriceFactor->get_all;
 
1215 sub init_part_picker_classification_ids {
 
1217   my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
 
1219   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
 
1225   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1227   my $right   = $right_for->{ $self->type };
 
1228   $right    ||= 'DOES_NOT_EXIST';
 
1230   $::auth->assert($right);
 
1233 # build the selection box for contacts
 
1235 # Needed, if customer/vendor changed.
 
1236 sub build_contact_select {
 
1239   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1240     value_key  => 'cp_id',
 
1241     title_key  => 'full_name_dep',
 
1242     default    => $self->order->cp_id,
 
1244     style      => 'width: 300px',
 
1248 # build the selection box for shiptos
 
1250 # Needed, if customer/vendor changed.
 
1251 sub build_shipto_select {
 
1254   select_tag('order.shipto_id',
 
1255              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1256              value_key  => 'shipto_id',
 
1257              title_key  => 'displayable_id',
 
1258              default    => $self->order->shipto_id,
 
1260              style      => 'width: 300px',
 
1264 # build the inputs for the cusom shipto dialog
 
1266 # Needed, if customer/vendor changed.
 
1267 sub build_shipto_inputs {
 
1270   my $content = $self->p->render('common/_ship_to_dialog',
 
1271                                  vc_obj      => $self->order->customervendor,
 
1272                                  cs_obj      => $self->order->custom_shipto,
 
1273                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1274                                  id_selector => '#order_shipto_id');
 
1276   div_tag($content, id => 'shipto_inputs');
 
1279 # render the info line for business
 
1281 # Needed, if customer/vendor changed.
 
1282 sub build_business_info_row
 
1284   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1287 # build the rows for displaying taxes
 
1289 # Called if amounts where recalculated and redisplayed.
 
1290 sub build_tax_rows {
 
1294   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1295     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1297   return $rows_as_html;
 
1301 sub render_price_dialog {
 
1302   my ($self, $record_item) = @_;
 
1304   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1308       'kivi.io.price_chooser_dialog',
 
1309       t8('Available Prices'),
 
1310       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1315 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1316 #     $self->js->show('#dialog_flash_error');
 
1325   return if !$::form->{id};
 
1327   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1329   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1330   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1331   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1333   return $self->order;
 
1336 # load or create a new order object
 
1338 # And assign changes from the form to this object.
 
1339 # If the order is loaded from db, check if items are deleted in the form,
 
1340 # remove them form the object and collect them for removing from db on saving.
 
1341 # Then create/update items from form (via make_item) and add them.
 
1345   # add_items adds items to an order with no items for saving, but they cannot
 
1346   # be retrieved via items until the order is saved. Adding empty items to new
 
1347   # order here solves this problem.
 
1349   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1350   $order ||= SL::DB::Order->new(orderitems  => [],
 
1351                                 quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
 
1352                                 currency_id => $::instance_conf->get_currency_id(),);
 
1354   my $cv_id_method = $self->cv . '_id';
 
1355   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1356     $order->$cv_id_method($::form->{$cv_id_method});
 
1357     setup_order_from_cv($order);
 
1360   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1361   my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
 
1363   $order->assign_attributes(%{$::form->{order}});
 
1365   $self->setup_custom_shipto_from_form($order, $::form);
 
1367   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1368     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1369     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1372   # remove deleted items
 
1373   $self->item_ids_to_delete([]);
 
1374   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1375     my $item = $order->orderitems->[$idx];
 
1376     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1377       splice @{$order->orderitems}, $idx, 1;
 
1378       push @{$self->item_ids_to_delete}, $item->id;
 
1384   foreach my $form_attr (@{$form_orderitems}) {
 
1385     my $item = make_item($order, $form_attr);
 
1386     $item->position($pos);
 
1390   $order->add_items(grep {!$_->id} @items);
 
1395 # create or update items from form
 
1397 # Make item objects from form values. For items already existing read from db.
 
1398 # Create a new item else. And assign attributes.
 
1400   my ($record, $attr) = @_;
 
1403   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1405   my $is_new = !$item;
 
1407   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1408   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1409   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1410   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1412   $item->assign_attributes(%$attr);
 
1415     my $texts = get_part_texts($item->part, $record->language_id);
 
1416     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1417     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1418     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1426 # This is used to add one item
 
1428   my ($record, $attr) = @_;
 
1430   my $item = SL::DB::OrderItem->new;
 
1432   # Remove attributes where the user left or set the inputs empty.
 
1433   # So these attributes will be undefined and we can distinguish them
 
1434   # from zero later on.
 
1435   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1436     delete $attr->{$_} if $attr->{$_} eq '';
 
1439   $item->assign_attributes(%$attr);
 
1441   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1442   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1444   $item->unit($part->unit) if !$item->unit;
 
1447   if ( $part->is_assortment ) {
 
1448     # add assortment items with price 0, as the components carry the price
 
1449     $price_src = $price_source->price_from_source("");
 
1450     $price_src->price(0);
 
1451   } elsif (defined $item->sellprice) {
 
1452     $price_src = $price_source->price_from_source("");
 
1453     $price_src->price($item->sellprice);
 
1455     $price_src = $price_source->best_price
 
1456                ? $price_source->best_price
 
1457                : $price_source->price_from_source("");
 
1458     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
1459     $price_src->price(0) if !$price_source->best_price;
 
1463   if (defined $item->discount) {
 
1464     $discount_src = $price_source->discount_from_source("");
 
1465     $discount_src->discount($item->discount);
 
1467     $discount_src = $price_source->best_discount
 
1468                   ? $price_source->best_discount
 
1469                   : $price_source->discount_from_source("");
 
1470     $discount_src->discount(0) if !$price_source->best_discount;
 
1474   $new_attr{part}                   = $part;
 
1475   $new_attr{description}            = $part->description     if ! $item->description;
 
1476   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1477   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1478   $new_attr{sellprice}              = $price_src->price;
 
1479   $new_attr{discount}               = $discount_src->discount;
 
1480   $new_attr{active_price_source}    = $price_src;
 
1481   $new_attr{active_discount_source} = $discount_src;
 
1482   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1483   $new_attr{project_id}             = $record->globalproject_id;
 
1484   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1486   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1487   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1488   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1489   $new_attr{custom_variables} = [];
 
1491   my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1493   $item->assign_attributes(%new_attr, %{ $texts });
 
1498 sub setup_order_from_cv {
 
1501   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
 
1503   $order->intnotes($order->customervendor->notes);
 
1505   if ($order->is_sales) {
 
1506     $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1507     $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1508                         ? $order->customer->taxincluded_checked
 
1509                         : $::myconfig{taxincluded_checked});
 
1514 # setup custom shipto from form
 
1516 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1517 # with 'shiptocvar_'.
 
1518 # Mark it to be deleted if a shipto from master data is selected
 
1519 # (i.e. order has a shipto).
 
1520 # Else, update or create a new custom shipto. If the fields are empty, it
 
1521 # will not be saved on save.
 
1522 sub setup_custom_shipto_from_form {
 
1523   my ($self, $order, $form) = @_;
 
1525   if ($order->shipto) {
 
1526     $self->is_custom_shipto_to_delete(1);
 
1528     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1530     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1531     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1533     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1534     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1538 # recalculate prices and taxes
 
1540 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1544   my %pat = $self->order->calculate_prices_and_taxes();
 
1546   $self->{taxes} = [];
 
1547   foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
 
1548     my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
1550     push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
 
1551                                 netamount => $netamount,
 
1552                                 tax       => SL::DB::Tax->new(id => $tax_id)->load });
 
1554   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1557 # get data for saving, printing, ..., that is not changed in the form
 
1559 # Only cvars for now.
 
1560 sub get_unalterable_data {
 
1563   foreach my $item (@{ $self->order->items }) {
 
1564     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1565     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1566     foreach my $var (@{ $item->cvars_by_config }) {
 
1567       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1569     $item->parse_custom_variable_values;
 
1575 # And remove related files in the spool directory
 
1580   my $db     = $self->order->db;
 
1582   $db->with_transaction(
 
1584       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1585       $self->order->delete;
 
1586       my $spool = $::lx_office_conf{paths}->{spool};
 
1587       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1589       $self->save_history('DELETED');
 
1592   }) || push(@{$errors}, $db->error);
 
1599 # And delete items that are deleted in the form.
 
1604   my $db     = $self->order->db;
 
1606   $db->with_transaction(sub {
 
1607     # delete custom shipto if it is to be deleted or if it is empty
 
1608     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1609       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1610       $self->order->custom_shipto(undef);
 
1613     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1614     $self->order->save(cascade => 1);
 
1617     if ($::form->{converted_from_oe_id}) {
 
1618       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1619       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1620         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1621         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1622         $src->link_to_record($self->order);
 
1624       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1626         foreach (@{ $self->order->items_sorted }) {
 
1627           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1629           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1630                                   from_id    => $from_id,
 
1631                                   to_table   => 'orderitems',
 
1639     $self->save_history('SAVED');
 
1642   }) || push(@{$errors}, $db->error);
 
1647 sub workflow_sales_or_request_for_quotation {
 
1651   my $errors = $self->save();
 
1653   if (scalar @{ $errors }) {
 
1654     $self->js->flash('error', $_) for @{ $errors };
 
1655     return $self->js->render();
 
1658   my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
 
1660   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1661   $self->{converted_from_oe_id} = delete $::form->{id};
 
1663   # set item ids to new fake id, to identify them as new items
 
1664   foreach my $item (@{$self->order->items_sorted}) {
 
1665     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1669   $::form->{type} = $destination_type;
 
1670   $self->type($self->init_type);
 
1671   $self->cv  ($self->init_cv);
 
1675   $self->get_unalterable_data();
 
1676   $self->pre_render();
 
1678   # trigger rendering values for second row as hidden, because they
 
1679   # are loaded only on demand. So we need to keep the values from the
 
1681   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1685     title => $self->get_title_for('edit'),
 
1686     %{$self->{template_args}}
 
1690 sub workflow_sales_or_purchase_order {
 
1694   my $errors = $self->save();
 
1696   if (scalar @{ $errors }) {
 
1697     $self->js->flash('error', $_) foreach @{ $errors };
 
1698     return $self->js->render();
 
1701   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1702                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1703                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1704                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1707   # check for direct delivery
 
1708   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1710   if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
 
1711       && $::form->{use_shipto} && $self->order->shipto) {
 
1712     $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
 
1715   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1716   $self->{converted_from_oe_id} = delete $::form->{id};
 
1718   # set item ids to new fake id, to identify them as new items
 
1719   foreach my $item (@{$self->order->items_sorted}) {
 
1720     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1723   if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
 
1724     if ($::form->{use_shipto}) {
 
1725       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
1727       # remove any custom shipto if not wanted
 
1728       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1733   $::form->{type} = $destination_type;
 
1734   $self->type($self->init_type);
 
1735   $self->cv  ($self->init_cv);
 
1739   $self->get_unalterable_data();
 
1740   $self->pre_render();
 
1742   # trigger rendering values for second row as hidden, because they
 
1743   # are loaded only on demand. So we need to keep the values from the
 
1745   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1749     title => $self->get_title_for('edit'),
 
1750     %{$self->{template_args}}
 
1758   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1759   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
1760   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1761   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted();
 
1762   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1765   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1768   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1770   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1771   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1772   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1773   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1774   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1776   my $print_form = Form->new('');
 
1777   $print_form->{type}        = $self->type;
 
1778   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
1779   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
1780     form => $print_form,
 
1781     options => {dialog_name_prefix => 'print_options.',
 
1785                 no_opendocument    => 0,
 
1789   foreach my $item (@{$self->order->orderitems}) {
 
1790     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1791     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1792     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1795   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1796     # calculate shipped qtys here to prevent calling calculate for every item via the items method
 
1797     SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects;
 
1800   if ($self->order->number && $::instance_conf->get_webdav) {
 
1801     my $webdav = SL::Webdav->new(
 
1802       type     => $self->type,
 
1803       number   => $self->order->number,
 
1805     my @all_objects = $webdav->get_all_objects;
 
1806     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1808                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1812   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1814   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
1815                                                          edit_periodic_invoices_config calculate_qty kivi.Validator follow_up show_history);
 
1816   $self->setup_edit_action_bar;
 
1819 sub setup_edit_action_bar {
 
1820   my ($self, %params) = @_;
 
1822   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
1823                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1824                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1826   for my $bar ($::request->layout->get('actionbar')) {
 
1831           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1832                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
1834           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'] ],
 
1838           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1839           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1840           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1842       ], # end of combobox "Save"
 
1849           t8('Save and Quotation'),
 
1850           submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
 
1851           only_if  => (any { $self->type eq $_ } (sales_order_type())),
 
1855           submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
 
1856           only_if  => (any { $self->type eq $_ } (purchase_order_type())),
 
1859           t8('Save and Sales Order'),
 
1860           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
1861           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
1864           t8('Save and Purchase Order'),
 
1865           call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
 
1866           only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
1869           t8('Save and Delivery Order'),
 
1870           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
1871                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1873           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1874           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
 
1877           t8('Save and Invoice'),
 
1878           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1879           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1882           t8('Save and AP Transaction'),
 
1883           call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
1884           only_if   => (any { $self->type eq $_ } (purchase_order_type()))
 
1887       ], # end of combobox "Workflow"
 
1894           t8('Save and preview PDF'),
 
1895            call => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
1896                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
1900           t8('Save and print'),
 
1901           call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
1902                                                      $::instance_conf->get_order_warn_no_deliverydate,
 
1906           t8('Save and E-mail'),
 
1907           id   => 'save_and_email_action',
 
1908           call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
1909                                                                      $::instance_conf->get_order_warn_no_deliverydate,
 
1911           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1914           t8('Download attachments of all parts'),
 
1915           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1916           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1917           only_if  => $::instance_conf->get_doc_storage,
 
1919       ], # end of combobox "Export"
 
1923         call     => [ 'kivi.Order.delete_order' ],
 
1924         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1925         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1926         only_if  => $deletion_allowed,
 
1935           call     => [ 'kivi.Order.follow_up_window' ],
 
1936           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1937           only_if  => $::auth->assert('productivity', 1),
 
1941           call     => [ 'set_history_window', $self->order->id, 'id' ],
 
1942           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
 
1944       ], # end of combobox "more"
 
1950   my ($order, $pdf_ref, $params) = @_;
 
1954   my $print_form = Form->new('');
 
1955   $print_form->{type}        = $order->type;
 
1956   $print_form->{formname}    = $params->{formname} || $order->type;
 
1957   $print_form->{format}      = $params->{format}   || 'pdf';
 
1958   $print_form->{media}       = $params->{media}    || 'file';
 
1959   $print_form->{groupitems}  = $params->{groupitems};
 
1960   $print_form->{printer_id}  = $params->{printer_id};
 
1961   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
1963   $order->language($params->{language});
 
1964   $order->flatten_to_form($print_form, format_amounts => 1);
 
1968   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
1969     $template_ext  = 'odt';
 
1970     $template_type = 'OpenDocument';
 
1973   # search for the template
 
1974   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
1975     name        => $print_form->{formname},
 
1976     extension   => $template_ext,
 
1977     email       => $print_form->{media} eq 'email',
 
1978     language    => $params->{language},
 
1979     printer_id  => $print_form->{printer_id},
 
1982   if (!defined $template_file) {
 
1983     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);
 
1986   return @errors if scalar @errors;
 
1988   $print_form->throw_on_error(sub {
 
1990       $print_form->prepare_for_printing;
 
1992       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
1993         format        => $print_form->{format},
 
1994         template_type => $template_type,
 
1995         template      => $template_file,
 
1996         variables     => $print_form,
 
1997         variable_content_types => {
 
1998           longdescription => 'html',
 
1999           partnotes       => 'html',
 
2004     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2010 sub get_files_for_email_dialog {
 
2013   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2015   return %files if !$::instance_conf->get_doc_storage;
 
2017   if ($self->order->id) {
 
2018     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2019     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2020     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2021     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2025     uniq_by { $_->{id} }
 
2027       +{ id         => $_->part->id,
 
2028          partnumber => $_->part->partnumber }
 
2029     } @{$self->order->items_sorted};
 
2031   foreach my $part (@parts) {
 
2032     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2033     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2036   foreach my $key (keys %files) {
 
2037     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2043 sub make_periodic_invoices_config_from_yaml {
 
2044   my ($yaml_config) = @_;
 
2046   return if !$yaml_config;
 
2047   my $attr = SL::YAML::Load($yaml_config);
 
2048   return if 'HASH' ne ref $attr;
 
2049   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
2053 sub get_periodic_invoices_status {
 
2054   my ($self, $config) = @_;
 
2056   return                      if $self->type ne sales_order_type();
 
2057   return t8('not configured') if !$config;
 
2059   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
2060              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
2061              :                                                     die "Cannot get status of periodic invoices config";
 
2063   return $active ? t8('active') : t8('inactive');
 
2067   my ($self, $action) = @_;
 
2069   return '' if none { lc($action)} qw(add edit);
 
2072   # $::locale->text("Add Sales Order");
 
2073   # $::locale->text("Add Purchase Order");
 
2074   # $::locale->text("Add Quotation");
 
2075   # $::locale->text("Add Request for Quotation");
 
2076   # $::locale->text("Edit Sales Order");
 
2077   # $::locale->text("Edit Purchase Order");
 
2078   # $::locale->text("Edit Quotation");
 
2079   # $::locale->text("Edit Request for Quotation");
 
2081   $action = ucfirst(lc($action));
 
2082   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
2083        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
2084        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
2085        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
2089 sub get_item_cvpartnumber {
 
2090   my ($self, $item) = @_;
 
2092   return if !$self->search_cvpartnumber;
 
2093   return if !$self->order->customervendor;
 
2095   if ($self->cv eq 'vendor') {
 
2096     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2097     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2098   } elsif ($self->cv eq 'customer') {
 
2099     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2100     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2104 sub get_part_texts {
 
2105   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2107   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2108   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2110     description     => $defaults{description}     // $part->description,
 
2111     longdescription => $defaults{longdescription} // $part->notes,
 
2114   return $texts unless $language_id;
 
2116   my $translation = SL::DB::Manager::Translation->get_first(
 
2118       parts_id    => $part->id,
 
2119       language_id => $language_id,
 
2122   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2123   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2128 sub sales_order_type {
 
2132 sub purchase_order_type {
 
2136 sub sales_quotation_type {
 
2140 sub request_quotation_type {
 
2141   'request_quotation';
 
2145   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
2146        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
2147        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
2148        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
2152 sub save_and_redirect_to {
 
2153   my ($self, %params) = @_;
 
2155   my $errors = $self->save();
 
2157   if (scalar @{ $errors }) {
 
2158     $self->js->flash('error', $_) foreach @{ $errors };
 
2159     return $self->js->render();
 
2162   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
2163            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
2164            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
2165            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
2167   flash_later('info', $text);
 
2169   $self->redirect_to(%params, id => $self->order->id);
 
2173   my ($self, $addition) = @_;
 
2175   my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
 
2176   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2178   SL::DB::History->new(
 
2179     trans_id    => $self->order->id,
 
2180     employee_id => SL::DB::Manager::Employee->current->id,
 
2181     what_done   => $self->order->type,
 
2182     snumbers    => $snumbers,
 
2183     addition    => $addition,
 
2187 sub store_pdf_to_webdav_and_filemanagement {
 
2188   my($order, $content, $filename) = @_;
 
2192   # copy file to webdav folder
 
2193   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2194     my $webdav = SL::Webdav->new(
 
2195       type     => $order->type,
 
2196       number   => $order->number,
 
2198     my $webdav_file = SL::Webdav::File->new(
 
2200       filename => $filename,
 
2203       $webdav_file->store(data => \$content);
 
2206       push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
 
2209   if ($order->id && $::instance_conf->get_doc_storage) {
 
2211       SL::File->save(object_id     => $order->id,
 
2212                      object_type   => $order->type,
 
2213                      mime_type     => 'application/pdf',
 
2214                      source        => 'created',
 
2215                      file_type     => 'document',
 
2216                      file_name     => $filename,
 
2217                      file_contents => $content);
 
2220       push @errors, t8('Storing PDF in storage backend failed: #1', $@);
 
2235 SL::Controller::Order - controller for orders
 
2239 This is a new form to enter orders, completely rewritten with the use
 
2240 of controller and java script techniques.
 
2242 The aim is to provide the user a better experience and a faster workflow. Also
 
2243 the code should be more readable, more reliable and better to maintain.
 
2251 One input row, so that input happens every time at the same place.
 
2255 Use of pickers where possible.
 
2259 Possibility to enter more than one item at once.
 
2263 Item list in a scrollable area, so that the workflow buttons stay at
 
2268 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2269 possible (by partnumber, description, qty, sellprice and discount for now).
 
2273 No C<update> is necessary. All entries and calculations are managed
 
2274 with ajax-calls and the page only reloads on C<save>.
 
2278 User can see changes immediately, because of the use of java script
 
2289 =item * C<SL/Controller/Order.pm>
 
2293 =item * C<template/webpages/order/form.html>
 
2297 =item * C<template/webpages/order/tabs/basic_data.html>
 
2299 Main tab for basic_data.
 
2301 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2302 reused from generic code.
 
2306 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
2308 For displaying information on business type
 
2310 =item * C<template/webpages/order/tabs/_item_input.html>
 
2312 The input line for items
 
2314 =item * C<template/webpages/order/tabs/_row.html>
 
2316 One row for already entered items
 
2318 =item * C<template/webpages/order/tabs/_tax_row.html>
 
2320 Displaying tax information
 
2322 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
2324 Dialog for selecting price and discount sources
 
2328 =item * C<js/kivi.Order.js>
 
2330 java script functions
 
2340 =item * price sources: little symbols showing better price / better discount
 
2342 =item * select units in input row?
 
2344 =item * check for direct delivery (workflow sales order -> purchase order)
 
2346 =item * access rights
 
2348 =item * display weights
 
2352 =item * optional client/user behaviour
 
2354 (transactions has to be set - department has to be set -
 
2355  force project if enabled in client config - transport cost reminder)
 
2359 =head1 KNOWN BUGS AND CAVEATS
 
2365 Customer discount is not displayed as a valid discount in price source popup
 
2366 (this might be a bug in price sources)
 
2368 (I cannot reproduce this (Bernd))
 
2372 No indication that <shift>-up/down expands/collapses second row.
 
2376 Inline creation of parts is not currently supported
 
2380 Table header is not sticky in the scrolling area.
 
2384 Sorting does not include C<position>, neither does reordering.
 
2386 This behavior was implemented intentionally. But we can discuss, which behavior
 
2387 should be implemented.
 
2391 =head1 To discuss / Nice to have
 
2397 How to expand/collapse second row. Now it can be done clicking the icon or
 
2402 Possibility to select PriceSources in input row?
 
2406 This controller uses a (changed) copy of the template for the PriceSource
 
2407 dialog. Maybe there could be used one code source.
 
2411 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2412 form. This is not only a problem here, but also in all parts using the PTC.
 
2413 There exists a ticket and a patch. This patch should be testet.
 
2417 An indicator, if the actual inputs are saved (like in an
 
2418 editor or on text processing application).
 
2422 A warning when leaving the page without saveing unchanged inputs.
 
2429 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>