1 package SL::Controller::Order;
 
   4 use parent qw(SL::Controller::Base);
 
   6 use SL::Helper::Flash qw(flash_later);
 
   8 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
 
   9 use SL::Locale::String qw(t8);
 
  10 use SL::SessionFile::Random;
 
  15 use SL::Util qw(trim);
 
  17 use SL::DB::AdditionalBillingAddress;
 
  24 use SL::DB::PartClassification;
 
  25 use SL::DB::PartsGroup;
 
  28 use SL::DB::RecordLink;
 
  29 use SL::DB::RequirementSpec;
 
  31 use SL::DB::Translation;
 
  33 use SL::Helper::CreatePDF qw(:all);
 
  34 use SL::Helper::PrintOptions;
 
  35 use SL::Helper::ShippedQty;
 
  36 use SL::Helper::UserPreferences::PositionsScrollbar;
 
  37 use SL::Helper::UserPreferences::UpdatePositions;
 
  39 use SL::Controller::Helper::GetModels;
 
  41 use List::Util qw(first sum0);
 
  42 use List::UtilsBy qw(sort_by uniq_by);
 
  43 use List::MoreUtils qw(any none pairwise first_index);
 
  44 use English qw(-no_match_vars);
 
  49 use Rose::Object::MakeMethods::Generic
 
  51  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 
  52  'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
 
  57 __PACKAGE__->run_before('check_auth');
 
  59 __PACKAGE__->run_before('check_auth_for_edit',
 
  60                         except => [ qw(edit show_customer_vendor_details_dialog price_popup load_second_rows) ]);
 
  62 __PACKAGE__->run_before('recalc',
 
  63                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
 
  66 __PACKAGE__->run_before('get_unalterable_data',
 
  67                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
 
  78   $self->order->transdate(DateTime->now_local());
 
  79   my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
  80                    $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
  82   if (   ($self->type eq sales_order_type()     &&  $::instance_conf->get_deliverydate_on)
 
  83       || ($self->type eq sales_quotation_type() &&  $::instance_conf->get_reqdate_on)
 
  84       && (!$self->order->reqdate)) {
 
  85     $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
 
  92     title => $self->get_title_for('add'),
 
  93     %{$self->{template_args}}
 
  97 # edit an existing order
 
 105     # this is to edit an order from an unsaved order object
 
 107     # set item ids to new fake id, to identify them as new items
 
 108     foreach my $item (@{$self->order->items_sorted}) {
 
 109       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 111     # trigger rendering values for second row as hidden, because they
 
 112     # are loaded only on demand. So we need to keep the values from
 
 114     $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
 121     title => $self->get_title_for('edit'),
 
 122     %{$self->{template_args}}
 
 126 # edit a collective order (consisting of one or more existing orders)
 
 127 sub action_edit_collective {
 
 131   my @multi_ids = map {
 
 132     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 133   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 135   # fall back to add if no ids are given
 
 136   if (scalar @multi_ids == 0) {
 
 141   # fall back to save as new if only one id is given
 
 142   if (scalar @multi_ids == 1) {
 
 143     $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
 
 144     $self->action_save_as_new();
 
 148   # make new order from given orders
 
 149   my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
 
 150   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 151   $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 153   $self->action_edit();
 
 160   my $errors = $self->delete();
 
 162   if (scalar @{ $errors }) {
 
 163     $self->js->flash('error', $_) foreach @{ $errors };
 
 164     return $self->js->render();
 
 167   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
 
 168            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
 
 169            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
 170            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 172   flash_later('info', $text);
 
 174   my @redirect_params = (
 
 179   $self->redirect_to(@redirect_params);
 
 186   my $errors = $self->save();
 
 188   if (scalar @{ $errors }) {
 
 189     $self->js->flash('error', $_) foreach @{ $errors };
 
 190     return $self->js->render();
 
 193   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
 194            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
 195            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 196            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 198   flash_later('info', $text);
 
 200   my @redirect_params = (
 
 203     id     => $self->order->id,
 
 206   $self->redirect_to(@redirect_params);
 
 209 # save the order as new document an open it for edit
 
 210 sub action_save_as_new {
 
 213   my $order = $self->order;
 
 216     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 217     return $self->js->render();
 
 220   # load order from db to check if values changed
 
 221   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 224   # Lets assign a new number if the user hasn't changed the previous one.
 
 225   # If it has been changed manually then use it as-is.
 
 226   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 228                         : trim($order->number);
 
 230   # Clear transdate unless changed
 
 231   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 232                         ? DateTime->today_local
 
 235   # Set new reqdate unless changed if it is enabled in client config
 
 236   if ($order->reqdate == $saved_order->reqdate) {
 
 237     my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
 
 238                      $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
 
 240     if (   ($self->type eq sales_order_type()     &&  !$::instance_conf->get_deliverydate_on)
 
 241         || ($self->type eq sales_quotation_type() &&  !$::instance_conf->get_reqdate_on)) {
 
 242       $new_attrs{reqdate} = '';
 
 244       $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 247     $new_attrs{reqdate} = $order->reqdate;
 
 251   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 253   # Create new record from current one
 
 254   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 256   # no linked records on save as new
 
 257   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 260   $self->action_save();
 
 265 # This is called if "print" is pressed in the print dialog.
 
 266 # If PDF creation was requested and succeeded, the pdf is offered for download
 
 267 # via send_file (which uses ajax in this case).
 
 271   my $errors = $self->save();
 
 273   if (scalar @{ $errors }) {
 
 274     $self->js->flash('error', $_) foreach @{ $errors };
 
 275     return $self->js->render();
 
 278   $self->js_reset_order_and_item_ids_after_save;
 
 280   my $format      = $::form->{print_options}->{format};
 
 281   my $media       = $::form->{print_options}->{media};
 
 282   my $formname    = $::form->{print_options}->{formname};
 
 283   my $copies      = $::form->{print_options}->{copies};
 
 284   my $groupitems  = $::form->{print_options}->{groupitems};
 
 285   my $printer_id  = $::form->{print_options}->{printer_id};
 
 287   # only PDF, OpenDocument & HTML for now
 
 288   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
 
 289     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 292   # only screen or printer by now
 
 293   if (none { $media eq $_ } qw(screen printer)) {
 
 294     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 297   # create a form for generate_attachment_filename
 
 298   my $form   = Form->new;
 
 299   $form->{$self->nr_key()}  = $self->order->number;
 
 300   $form->{type}             = $self->type;
 
 301   $form->{format}           = $format;
 
 302   $form->{formname}         = $formname;
 
 303   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 304   my $doc_filename          = $form->generate_attachment_filename();
 
 307   my @errors = $self->generate_doc(\$doc, { media      => $media,
 
 309                                             formname   => $formname,
 
 310                                             language   => $self->order->language,
 
 311                                             printer_id => $printer_id,
 
 312                                             groupitems => $groupitems });
 
 313   if (scalar @errors) {
 
 314     return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
 
 317   if ($media eq 'screen') {
 
 319     $self->js->flash('info', t8('The document has been created.'));
 
 322       type         => SL::MIME->mime_type_from_ext($doc_filename),
 
 323       name         => $doc_filename,
 
 327   } elsif ($media eq 'printer') {
 
 329     my $printer_id = $::form->{print_options}->{printer_id};
 
 330     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 335     $self->js->flash('info', t8('The document has been printed.'));
 
 338   my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
 
 339   if (scalar @warnings) {
 
 340     $self->js->flash('warning', $_) for @warnings;
 
 343   $self->save_history('PRINTED');
 
 346     ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
 
 349 sub action_preview_pdf {
 
 352   my $errors = $self->save();
 
 353   if (scalar @{ $errors }) {
 
 354     $self->js->flash('error', $_) foreach @{ $errors };
 
 355     return $self->js->render();
 
 358   $self->js_reset_order_and_item_ids_after_save;
 
 361   my $media       = 'screen';
 
 362   my $formname    = $self->type;
 
 365   # create a form for generate_attachment_filename
 
 366   my $form   = Form->new;
 
 367   $form->{$self->nr_key()}  = $self->order->number;
 
 368   $form->{type}             = $self->type;
 
 369   $form->{format}           = $format;
 
 370   $form->{formname}         = $formname;
 
 371   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 372   my $pdf_filename          = $form->generate_attachment_filename();
 
 375   my @errors = $self->generate_doc(\$pdf, { media      => $media,
 
 377                                             formname   => $formname,
 
 378                                             language   => $self->order->language,
 
 380   if (scalar @errors) {
 
 381     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 383   $self->save_history('PREVIEWED');
 
 384   $self->js->flash('info', t8('The PDF has been previewed'));
 
 388     type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 389     name         => $pdf_filename,
 
 394 # open the email dialog
 
 395 sub action_save_and_show_email_dialog {
 
 398   my $errors = $self->save();
 
 400   if (scalar @{ $errors }) {
 
 401     $self->js->flash('error', $_) foreach @{ $errors };
 
 402     return $self->js->render();
 
 405   my $cv_method = $self->cv;
 
 407   if (!$self->order->$cv_method) {
 
 408     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'))
 
 413   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 414   $email_form->{to} ||= $self->order->$cv_method->email;
 
 415   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 416   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 417   # Todo: get addresses from shipto, if any
 
 419   my $form = Form->new;
 
 420   $form->{$self->nr_key()}  = $self->order->number;
 
 421   $form->{cusordnumber}     = $self->order->cusordnumber;
 
 422   $form->{formname}         = $self->type;
 
 423   $form->{type}             = $self->type;
 
 424   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 425   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
 
 426   $form->{format}           = 'pdf';
 
 427   $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
 429   $email_form->{subject}             = $form->generate_email_subject();
 
 430   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 431   $email_form->{message}             = $form->generate_email_body();
 
 432   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 434   my %files = $self->get_files_for_email_dialog();
 
 436   my @employees_with_email = grep {
 
 437     my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
 
 438     $user && !!trim($user->get_config_value('email'));
 
 439   } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
 
 441   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 442                                   email_form    => $email_form,
 
 443                                   show_bcc      => $::auth->assert('email_bcc', 'may fail'),
 
 445                                   is_customer   => $self->cv eq 'customer',
 
 446                                   ALL_EMPLOYEES => \@employees_with_email,
 
 450       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 457 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 458 sub action_send_email {
 
 461   my $errors = $self->save();
 
 463   if (scalar @{ $errors }) {
 
 464     $self->js->run('kivi.Order.close_email_dialog');
 
 465     $self->js->flash('error', $_) foreach @{ $errors };
 
 466     return $self->js->render();
 
 469   $self->js_reset_order_and_item_ids_after_save;
 
 471   my $email_form  = delete $::form->{email_form};
 
 472   my %field_names = (to => 'email');
 
 474   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 476   # for Form::cleanup which may be called in Form::send_email
 
 477   $::form->{cwd}    = getcwd();
 
 478   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 480   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 481   $::form->{media}  = 'email';
 
 483   $::form->{attachment_policy} //= '';
 
 485   # Is an old file version available?
 
 487   if ($::form->{attachment_policy} eq 'old_file') {
 
 488     $attfile = SL::File->get_all(object_id     => $self->order->id,
 
 489                                  object_type   => $self->type,
 
 490                                  file_type     => 'document',
 
 491                                  print_variant => $::form->{formname});
 
 494   if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
 
 496     my @errors = $self->generate_doc(\$doc, {media      => $::form->{media},
 
 497                                              format     => $::form->{print_options}->{format},
 
 498                                              formname   => $::form->{print_options}->{formname},
 
 499                                              language   => $self->order->language,
 
 500                                              printer_id => $::form->{print_options}->{printer_id},
 
 501                                              groupitems => $::form->{print_options}->{groupitems}});
 
 502     if (scalar @errors) {
 
 503       return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
 
 506     my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename}, $::form->{formname});
 
 507     if (scalar @warnings) {
 
 508       flash_later('warning', $_) for @warnings;
 
 511     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 512     $sfile->fh->print($doc);
 
 515     $::form->{tmpfile} = $sfile->file_name;
 
 516     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 519   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 520   $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
 
 522   # internal notes unless no email journal
 
 523   unless ($::instance_conf->get_email_journal) {
 
 524     my $intnotes = $self->order->intnotes;
 
 525     $intnotes   .= "\n\n" if $self->order->intnotes;
 
 526     $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 527     $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 528     $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 529     $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 530     $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 531     $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 532     $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
 
 534     $self->order->update_attributes(intnotes => $intnotes);
 
 537   $self->save_history('MAILED');
 
 539   flash_later('info', t8('The email has been sent.'));
 
 541   my @redirect_params = (
 
 544     id     => $self->order->id,
 
 547   $self->redirect_to(@redirect_params);
 
 550 # open the periodic invoices config dialog
 
 552 # If there are values in the form (i.e. dialog was opened before),
 
 553 # then use this values. Create new ones, else.
 
 554 sub action_show_periodic_invoices_config_dialog {
 
 557   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 558   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 559   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 560                                                    order_value_periodicity => 'p', # = same as periodicity
 
 561                                                    start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
 
 562                                                    extend_automatically_by => 12,
 
 564                                                    email_subject           => GenericTranslations->get(
 
 565                                                                                 language_id      => $::form->{language_id},
 
 566                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 567                                                    email_body              => GenericTranslations->get(
 
 568                                                                                 language_id      => $::form->{language_id},
 
 569                                                                                 translation_type => "salutation_general")
 
 570                                                                             . GenericTranslations->get(
 
 571                                                                                 language_id      => $::form->{language_id},
 
 572                                                                                 translation_type => "salutation_punctuation_mark") . "\n\n"
 
 573                                                                             . GenericTranslations->get(
 
 574                                                                                 language_id      => $::form->{language_id},
 
 575                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 577   # for older configs, replace email preset text if not yet set.
 
 578   $config->email_subject(GenericTranslations->get(
 
 579                                               language_id      => $::form->{language_id},
 
 580                                               translation_type =>"preset_text_periodic_invoices_email_subject")
 
 581                         ) unless $config->email_subject;
 
 583   $config->email_body(GenericTranslations->get(
 
 584                                               language_id      => $::form->{language_id},
 
 585                                               translation_type => "salutation_general")
 
 586                     . GenericTranslations->get(
 
 587                                               language_id      => $::form->{language_id},
 
 588                                               translation_type => "salutation_punctuation_mark") . "\n\n"
 
 589                     . GenericTranslations->get(
 
 590                                               language_id      => $::form->{language_id},
 
 591                                               translation_type =>"preset_text_periodic_invoices_email_body")
 
 592                      ) unless $config->email_body;
 
 594   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 595   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 597   $::form->get_lists(printers => "ALL_PRINTERS",
 
 598                      charts   => { key       => 'ALL_CHARTS',
 
 599                                    transdate => 'current_date' });
 
 601   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 603   if ($::form->{customer_id}) {
 
 604     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 605     my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
 
 606     $::form->{postal_invoice}                  = $customer_object->postal_invoice;
 
 607     $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
 
 608     $config->send_email(0) if $::form->{postal_invoice};
 
 611   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 613                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 614                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 619 # assign the values of the periodic invoices config dialog
 
 620 # as yaml in the hidden tag and set the status.
 
 621 sub action_assign_periodic_invoices_config {
 
 624   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 626   my $config = { active                     => $::form->{active}       ? 1 : 0,
 
 627                  terminated                 => $::form->{terminated}   ? 1 : 0,
 
 628                  direct_debit               => $::form->{direct_debit} ? 1 : 0,
 
 629                  periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 630                  order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 631                  start_date_as_date         => $::form->{start_date_as_date},
 
 632                  end_date_as_date           => $::form->{end_date_as_date},
 
 633                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 634                  print                      => $::form->{print}      ? 1                         : 0,
 
 635                  printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
 
 636                  copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
 
 637                  extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
 
 638                  ar_chart_id                => $::form->{ar_chart_id} * 1,
 
 639                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 640                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 641                  email_recipient_address    => $::form->{email_recipient_address},
 
 642                  email_sender               => $::form->{email_sender},
 
 643                  email_subject              => $::form->{email_subject},
 
 644                  email_body                 => $::form->{email_body},
 
 647   my $periodic_invoices_config = SL::YAML::Dump($config);
 
 649   my $status = $self->get_periodic_invoices_status($config);
 
 652     ->remove('#order_periodic_invoices_config')
 
 653     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 654     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 655     ->html('#periodic_invoices_status', $status)
 
 656     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 660 sub action_get_has_active_periodic_invoices {
 
 663   my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 664   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 666   my $has_active_periodic_invoices =
 
 667        $self->type eq sales_order_type()
 
 670     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 671     && $config->get_previous_billed_period_start_date;
 
 673   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 676 # save the order and redirect to the frontend subroutine for a new
 
 678 sub action_save_and_delivery_order {
 
 681   $self->save_and_redirect_to(
 
 682     controller => 'oe.pl',
 
 683     action     => 'oe_delivery_order_from_order',
 
 687 sub action_save_and_supplier_delivery_order {
 
 690   $self->save_and_redirect_to(
 
 691     controller => 'controller.pl',
 
 692     action     => 'DeliveryOrder/add_from_order',
 
 693     type       => 'supplier_delivery_order',
 
 697 # save the order and redirect to the frontend subroutine for a new
 
 699 sub action_save_and_invoice {
 
 702   $self->save_and_redirect_to(
 
 703     controller => 'oe.pl',
 
 704     action     => 'oe_invoice_from_order',
 
 708 sub action_save_and_invoice_for_advance_payment {
 
 711   $self->save_and_redirect_to(
 
 712     controller       => 'oe.pl',
 
 713     action           => 'oe_invoice_from_order',
 
 714     new_invoice_type => 'invoice_for_advance_payment',
 
 718 sub action_save_and_final_invoice {
 
 721   $self->save_and_redirect_to(
 
 722     controller       => 'oe.pl',
 
 723     action           => 'oe_invoice_from_order',
 
 724     new_invoice_type => 'final_invoice',
 
 728 # workflow from sales order to sales quotation
 
 729 sub action_sales_quotation {
 
 730   $_[0]->workflow_sales_or_request_for_quotation();
 
 733 # workflow from sales order to sales quotation
 
 734 sub action_request_for_quotation {
 
 735   $_[0]->workflow_sales_or_request_for_quotation();
 
 738 # workflow from sales quotation to sales order
 
 739 sub action_sales_order {
 
 740   $_[0]->workflow_sales_or_purchase_order();
 
 743 # workflow from rfq to purchase order
 
 744 sub action_purchase_order {
 
 745   $_[0]->workflow_sales_or_purchase_order();
 
 748 # workflow from purchase order to ap transaction
 
 749 sub action_save_and_ap_transaction {
 
 752   $self->save_and_redirect_to(
 
 753     controller => 'ap.pl',
 
 754     action     => 'add_from_purchase_order',
 
 758 # set form elements in respect to a changed customer or vendor
 
 760 # This action is called on an change of the customer/vendor picker.
 
 761 sub action_customer_vendor_changed {
 
 764   setup_order_from_cv($self->order);
 
 767   my $cv_method = $self->cv;
 
 769   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 770     $self->js->show('#cp_row');
 
 772     $self->js->hide('#cp_row');
 
 775   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 776     $self->js->show('#shipto_selection');
 
 778     $self->js->hide('#shipto_selection');
 
 781   if ($cv_method eq 'customer') {
 
 782     my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
 
 783     $self->js->$show_hide('#billing_address_row');
 
 786   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 789     ->replaceWith('#order_cp_id',              $self->build_contact_select)
 
 790     ->replaceWith('#order_shipto_id',          $self->build_shipto_select)
 
 791     ->replaceWith('#shipto_inputs  ',          $self->build_shipto_inputs)
 
 792     ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
 
 793     ->replaceWith('#business_info_row',        $self->build_business_info_row)
 
 794     ->val(        '#order_taxzone_id',         $self->order->taxzone_id)
 
 795     ->val(        '#order_taxincluded',        $self->order->taxincluded)
 
 796     ->val(        '#order_currency_id',        $self->order->currency_id)
 
 797     ->val(        '#order_payment_id',         $self->order->payment_id)
 
 798     ->val(        '#order_delivery_term_id',   $self->order->delivery_term_id)
 
 799     ->val(        '#order_intnotes',           $self->order->intnotes)
 
 800     ->val(        '#order_language_id',        $self->order->$cv_method->language_id)
 
 801     ->focus(      '#order_' . $self->cv . '_id')
 
 802     ->run('kivi.Order.update_exchangerate');
 
 804   $self->js_redisplay_amounts_and_taxes;
 
 805   $self->js_redisplay_cvpartnumbers;
 
 809 # open the dialog for customer/vendor details
 
 810 sub action_show_customer_vendor_details_dialog {
 
 813   my $is_customer = 'customer' eq $::form->{vc};
 
 816     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 818     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 821   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 822   $details{discount_as_percent} = $cv->discount_as_percent;
 
 823   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 824   $details{business}            = $cv->business->description      if $cv->business;
 
 825   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 826   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 827   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 828   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 831     foreach my $entry (@{ $cv->additional_billing_addresses }) {
 
 832       push @{ $details{ADDITIONAL_BILLING_ADDRESSES} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 835   foreach my $entry (@{ $cv->shipto }) {
 
 836     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 838   foreach my $entry (@{ $cv->contacts }) {
 
 839     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 842   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 843                 is_customer => $is_customer,
 
 848 # called if a unit in an existing item row is changed
 
 849 sub action_unit_changed {
 
 852   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 853   my $item = $self->order->items_sorted->[$idx];
 
 855   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 856   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 861     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 862   $self->js_redisplay_line_values;
 
 863   $self->js_redisplay_amounts_and_taxes;
 
 867 # add an item row for a new item entered in the input row
 
 868 sub action_add_item {
 
 871   delete $::form->{add_item}->{create_part_type};
 
 873   my $form_attr = $::form->{add_item};
 
 875   return unless $form_attr->{parts_id};
 
 877   my $item = new_item($self->order, $form_attr);
 
 879   $self->order->add_items($item);
 
 883   $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);
 
 900   if ( $item->part->is_assortment ) {
 
 901     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 902     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 903       my $attr = { parts_id => $assortment_item->parts_id,
 
 904                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 905                    unit     => $assortment_item->unit,
 
 906                    description => $assortment_item->part->description,
 
 908       my $item = new_item($self->order, $attr);
 
 910       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 911       $item->discount(1) unless $assortment_item->charge;
 
 913       $self->order->add_items( $item );
 
 915       $self->get_item_cvpartnumber($item);
 
 916       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 917       my $row_as_html = $self->p->render('order/tabs/_row',
 
 922       if ($::form->{insert_before_item_id}) {
 
 924           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 927           ->append('#row_table_id', $row_as_html);
 
 933     ->val('.add_item_input', '')
 
 934     ->run('kivi.Order.init_row_handlers')
 
 935     ->run('kivi.Order.renumber_positions')
 
 936     ->focus('#add_item_parts_id_name');
 
 938   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 940   $self->js_redisplay_amounts_and_taxes;
 
 944 # add item rows for multiple items at once
 
 945 sub action_add_multi_items {
 
 948   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
 
 949   return $self->js->render() unless scalar @form_attr;
 
 952   foreach my $attr (@form_attr) {
 
 953     my $item = new_item($self->order, $attr);
 
 955     if ( $item->part->is_assortment ) {
 
 956       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 957         my $attr = { parts_id => $assortment_item->parts_id,
 
 958                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 959                      unit     => $assortment_item->unit,
 
 960                      description => $assortment_item->part->description,
 
 962         my $item = new_item($self->order, $attr);
 
 964         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 965         $item->discount(1) unless $assortment_item->charge;
 
 970   $self->order->add_items(@items);
 
 974   foreach my $item (@items) {
 
 975     $self->get_item_cvpartnumber($item);
 
 976     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 977     my $row_as_html = $self->p->render('order/tabs/_row',
 
 983     if ($::form->{insert_before_item_id}) {
 
 985         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 988         ->append('#row_table_id', $row_as_html);
 
 993     ->run('kivi.Part.close_picker_dialogs')
 
 994     ->run('kivi.Order.init_row_handlers')
 
 995     ->run('kivi.Order.renumber_positions')
 
 996     ->focus('#add_item_parts_id_name');
 
 998   $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
1000   $self->js_redisplay_amounts_and_taxes;
 
1001   $self->js->render();
 
1004 # recalculate all linetotals, amounts and taxes and redisplay them
 
1005 sub action_recalc_amounts_and_taxes {
 
1010   $self->js_redisplay_line_values;
 
1011   $self->js_redisplay_amounts_and_taxes;
 
1012   $self->js->render();
 
1015 sub action_update_exchangerate {
 
1019     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
1020     currency_name => $self->order->currency->name,
 
1021     exchangerate  => $self->order->daily_exchangerate_as_null_number,
 
1024   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
1027 # redisplay item rows if they are sorted by an attribute
 
1028 sub action_reorder_items {
 
1032     partnumber   => sub { $_[0]->part->partnumber },
 
1033     description  => sub { $_[0]->description },
 
1034     qty          => sub { $_[0]->qty },
 
1035     sellprice    => sub { $_[0]->sellprice },
 
1036     discount     => sub { $_[0]->discount },
 
1037     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
1040   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1042   my $method = $sort_keys{$::form->{order_by}};
 
1043   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
1044   if ($::form->{sort_dir}) {
 
1045     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1046       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
1048       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
1051     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
1052       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
1054       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
1058     ->run('kivi.Order.redisplay_items', \@to_sort)
 
1062 # show the popup to choose a price/discount source
 
1063 sub action_price_popup {
 
1066   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
1067   my $item = $self->order->items_sorted->[$idx];
 
1069   $self->render_price_dialog($item);
 
1072 # save the order in a session variable and redirect to the part controller
 
1073 sub action_create_part {
 
1076   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
 
1078   my $callback     = $self->url_for(
 
1079     action       => 'return_from_create_part',
 
1080     type         => $self->type, # type is needed for check_auth on return
 
1081     previousform => $previousform,
 
1084   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.'));
 
1086   my @redirect_params = (
 
1087     controller => 'Part',
 
1089     part_type  => $::form->{add_item}->{create_part_type},
 
1090     callback   => $callback,
 
1094   $self->redirect_to(@redirect_params);
 
1097 sub action_return_from_create_part {
 
1100   $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
 
1102   $::auth->restore_form_from_session(delete $::form->{previousform});
 
1104   # set item ids to new fake id, to identify them as new items
 
1105   foreach my $item (@{$self->order->items_sorted}) {
 
1106     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1110   $self->get_unalterable_data();
 
1111   $self->pre_render();
 
1113   # trigger rendering values for second row/longdescription as hidden,
 
1114   # because they are loaded only on demand. So we need to keep the values
 
1116   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1117   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1121     title => $self->get_title_for('edit'),
 
1122     %{$self->{template_args}}
 
1127 # load the second row for one or more items
 
1129 # This action gets the html code for all items second rows by rendering a template for
 
1130 # the second row and sets the html code via client js.
 
1131 sub action_load_second_rows {
 
1134   $self->recalc() if $self->order->is_sales; # for margin calculation
 
1136   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1137     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1138     my $item = $self->order->items_sorted->[$idx];
 
1140     $self->js_load_second_row($item, $item_id, 0);
 
1143   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
1145   $self->js->render();
 
1148 # update description, notes and sellprice from master data
 
1149 sub action_update_row_from_master_data {
 
1152   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1153     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1154     my $item  = $self->order->items_sorted->[$idx];
 
1155     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1157     $item->description($texts->{description});
 
1158     $item->longdescription($texts->{longdescription});
 
1160     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1163     if ($item->part->is_assortment) {
 
1164     # add assortment items with price 0, as the components carry the price
 
1165       $price_src = $price_source->price_from_source("");
 
1166       $price_src->price(0);
 
1168       $price_src = $price_source->best_price
 
1169                  ? $price_source->best_price
 
1170                  : $price_source->price_from_source("");
 
1171       $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
 
1172       $price_src->price(0) if !$price_source->best_price;
 
1176     $item->sellprice($price_src->price);
 
1177     $item->active_price_source($price_src);
 
1180       ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
 
1181       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1182       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1183       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1185     if ($self->search_cvpartnumber) {
 
1186       $self->get_item_cvpartnumber($item);
 
1187       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1192   $self->js_redisplay_line_values;
 
1193   $self->js_redisplay_amounts_and_taxes;
 
1195   $self->js->render();
 
1198 sub js_load_second_row {
 
1199   my ($self, $item, $item_id, $do_parse) = @_;
 
1202     # Parse values from form (they are formated while rendering (template)).
 
1203     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1204     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1205     foreach my $var (@{ $item->cvars_by_config }) {
 
1206       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1208     $item->parse_custom_variable_values;
 
1211   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1214     ->html('#second_row_' . $item_id, $row_as_html)
 
1215     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1218 sub js_redisplay_line_values {
 
1221   my $is_sales = $self->order->is_sales;
 
1223   # sales orders with margins
 
1228        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1229        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1230        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1231       ]} @{ $self->order->items_sorted };
 
1235        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1236       ]} @{ $self->order->items_sorted };
 
1240     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
1243 sub js_redisplay_amounts_and_taxes {
 
1246   if (scalar @{ $self->{taxes} }) {
 
1247     $self->js->show('#taxincluded_row_id');
 
1249     $self->js->hide('#taxincluded_row_id');
 
1252   if ($self->order->taxincluded) {
 
1253     $self->js->hide('#subtotal_row_id');
 
1255     $self->js->show('#subtotal_row_id');
 
1258   if ($self->order->is_sales) {
 
1259     my $is_neg = $self->order->marge_total < 0;
 
1261       ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
 
1262       ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
 
1263       ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
 
1264       ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
 
1265       ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
 
1266       ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
 
1267       ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
 
1268       ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
 
1272     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
1273     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
1274     ->remove('.tax_row')
 
1275     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
1278 sub js_redisplay_cvpartnumbers {
 
1281   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1283   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1286     ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
 
1289 sub js_reset_order_and_item_ids_after_save {
 
1293     ->val('#id', $self->order->id)
 
1294     ->val('#converted_from_oe_id', '')
 
1295     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1298   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1299     next if !$self->order->items_sorted->[$idx]->id;
 
1300     next if $form_item_id !~ m{^new};
 
1302       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1303       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1304       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1308   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1315 sub init_valid_types {
 
1316   [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
 
1322   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1323     die "Not a valid type for order";
 
1326   $self->type($::form->{type});
 
1332   my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
 
1333          : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
 
1334          : die "Not a valid type for order";
 
1339 sub init_search_cvpartnumber {
 
1342   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1343   my $search_cvpartnumber;
 
1344   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1345   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1347   return $search_cvpartnumber;
 
1350 sub init_show_update_button {
 
1353   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1364 sub init_all_price_factors {
 
1365   SL::DB::Manager::PriceFactor->get_all;
 
1368 sub init_part_picker_classification_ids {
 
1370   my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
 
1372   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
 
1378   my $right_for = { map { $_ => $_.'_edit' . ' | ' . $_.'_view' } @{$self->valid_types} };
 
1380   my $right   = $right_for->{ $self->type };
 
1381   $right    ||= 'DOES_NOT_EXIST';
 
1383   $::auth->assert($right);
 
1386 sub check_auth_for_edit {
 
1389   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
1391   my $right   = $right_for->{ $self->type };
 
1392   $right    ||= 'DOES_NOT_EXIST';
 
1394   $::auth->assert($right);
 
1397 # build the selection box for contacts
 
1399 # Needed, if customer/vendor changed.
 
1400 sub build_contact_select {
 
1403   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1404     value_key  => 'cp_id',
 
1405     title_key  => 'full_name_dep',
 
1406     default    => $self->order->cp_id,
 
1408     style      => 'width: 300px',
 
1412 # build the selection box for the additional billing address
 
1414 # Needed, if customer/vendor changed.
 
1415 sub build_billing_address_select {
 
1418   return '' if $self->cv ne 'customer';
 
1420   select_tag('order.billing_address_id',
 
1421              [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
 
1423              title_key  => 'displayable_id',
 
1424              default    => $self->order->billing_address_id,
 
1426              style      => 'width: 300px',
 
1430 # build the selection box for shiptos
 
1432 # Needed, if customer/vendor changed.
 
1433 sub build_shipto_select {
 
1436   select_tag('order.shipto_id',
 
1437              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1438              value_key  => 'shipto_id',
 
1439              title_key  => 'displayable_id',
 
1440              default    => $self->order->shipto_id,
 
1442              style      => 'width: 300px',
 
1446 # build the inputs for the cusom shipto dialog
 
1448 # Needed, if customer/vendor changed.
 
1449 sub build_shipto_inputs {
 
1452   my $content = $self->p->render('common/_ship_to_dialog',
 
1453                                  vc_obj      => $self->order->customervendor,
 
1454                                  cs_obj      => $self->order->custom_shipto,
 
1455                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1456                                  id_selector => '#order_shipto_id');
 
1458   div_tag($content, id => 'shipto_inputs');
 
1461 # render the info line for business
 
1463 # Needed, if customer/vendor changed.
 
1464 sub build_business_info_row
 
1466   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1469 # build the rows for displaying taxes
 
1471 # Called if amounts where recalculated and redisplayed.
 
1472 sub build_tax_rows {
 
1476   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1477     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1479   return $rows_as_html;
 
1483 sub render_price_dialog {
 
1484   my ($self, $record_item) = @_;
 
1486   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1490       'kivi.io.price_chooser_dialog',
 
1491       t8('Available Prices'),
 
1492       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1497 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1498 #     $self->js->show('#dialog_flash_error');
 
1507   return if !$::form->{id};
 
1509   $self->order(SL::DB::Order->new(id => $::form->{id})->load);
 
1511   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1512   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1513   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1515   return $self->order;
 
1518 # load or create a new order object
 
1520 # And assign changes from the form to this object.
 
1521 # If the order is loaded from db, check if items are deleted in the form,
 
1522 # remove them form the object and collect them for removing from db on saving.
 
1523 # Then create/update items from form (via make_item) and add them.
 
1527   # add_items adds items to an order with no items for saving, but they cannot
 
1528   # be retrieved via items until the order is saved. Adding empty items to new
 
1529   # order here solves this problem.
 
1531   $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1532   $order ||= SL::DB::Order->new(orderitems  => [],
 
1533                                 quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
 
1534                                 currency_id => $::instance_conf->get_currency_id(),);
 
1536   my $cv_id_method = $self->cv . '_id';
 
1537   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1538     $order->$cv_id_method($::form->{$cv_id_method});
 
1539     setup_order_from_cv($order);
 
1542   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1543   my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
 
1545   $order->assign_attributes(%{$::form->{order}});
 
1547   $self->setup_custom_shipto_from_form($order, $::form);
 
1549   if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
 
1550     my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
 
1551     $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
 
1554   # remove deleted items
 
1555   $self->item_ids_to_delete([]);
 
1556   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1557     my $item = $order->orderitems->[$idx];
 
1558     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1559       splice @{$order->orderitems}, $idx, 1;
 
1560       push @{$self->item_ids_to_delete}, $item->id;
 
1566   foreach my $form_attr (@{$form_orderitems}) {
 
1567     my $item = make_item($order, $form_attr);
 
1568     $item->position($pos);
 
1572   $order->add_items(grep {!$_->id} @items);
 
1577 # create or update items from form
 
1579 # Make item objects from form values. For items already existing read from db.
 
1580 # Create a new item else. And assign attributes.
 
1582   my ($record, $attr) = @_;
 
1585   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1587   my $is_new = !$item;
 
1589   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1590   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1591   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1592   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1594   $item->assign_attributes(%$attr);
 
1597     my $texts = get_part_texts($item->part, $record->language_id);
 
1598     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1599     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1600     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1608 # This is used to add one item
 
1610   my ($record, $attr) = @_;
 
1612   my $item = SL::DB::OrderItem->new;
 
1614   # Remove attributes where the user left or set the inputs empty.
 
1615   # So these attributes will be undefined and we can distinguish them
 
1616   # from zero later on.
 
1617   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1618     delete $attr->{$_} if $attr->{$_} eq '';
 
1621   $item->assign_attributes(%$attr);
 
1623   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1624   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1626   $item->unit($part->unit) if !$item->unit;
 
1629   if ( $part->is_assortment ) {
 
1630     # add assortment items with price 0, as the components carry the price
 
1631     $price_src = $price_source->price_from_source("");
 
1632     $price_src->price(0);
 
1633   } elsif (defined $item->sellprice) {
 
1634     $price_src = $price_source->price_from_source("");
 
1635     $price_src->price($item->sellprice);
 
1637     $price_src = $price_source->best_price
 
1638                ? $price_source->best_price
 
1639                : $price_source->price_from_source("");
 
1640     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
 
1641     $price_src->price(0) if !$price_source->best_price;
 
1645   if (defined $item->discount) {
 
1646     $discount_src = $price_source->discount_from_source("");
 
1647     $discount_src->discount($item->discount);
 
1649     $discount_src = $price_source->best_discount
 
1650                   ? $price_source->best_discount
 
1651                   : $price_source->discount_from_source("");
 
1652     $discount_src->discount(0) if !$price_source->best_discount;
 
1656   $new_attr{part}                   = $part;
 
1657   $new_attr{description}            = $part->description     if ! $item->description;
 
1658   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1659   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1660   $new_attr{sellprice}              = $price_src->price;
 
1661   $new_attr{discount}               = $discount_src->discount;
 
1662   $new_attr{active_price_source}    = $price_src;
 
1663   $new_attr{active_discount_source} = $discount_src;
 
1664   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1665   $new_attr{project_id}             = $record->globalproject_id;
 
1666   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1668   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1669   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1670   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1671   $new_attr{custom_variables} = [];
 
1673   my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1675   $item->assign_attributes(%new_attr, %{ $texts });
 
1680 sub setup_order_from_cv {
 
1683   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id language_id));
 
1685   $order->intnotes($order->customervendor->notes);
 
1687   return if !$order->is_sales;
 
1689   $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1690   $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1691                       ? $order->customer->taxincluded_checked
 
1692                       : $::myconfig{taxincluded_checked});
 
1694   my $address = $order->customer->default_billing_address;;
 
1695   $order->billing_address_id($address ? $address->id : undef);
 
1698 # setup custom shipto from form
 
1700 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1701 # with 'shiptocvar_'.
 
1702 # Mark it to be deleted if a shipto from master data is selected
 
1703 # (i.e. order has a shipto).
 
1704 # Else, update or create a new custom shipto. If the fields are empty, it
 
1705 # will not be saved on save.
 
1706 sub setup_custom_shipto_from_form {
 
1707   my ($self, $order, $form) = @_;
 
1709   if ($order->shipto) {
 
1710     $self->is_custom_shipto_to_delete(1);
 
1712     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1714     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1715     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1717     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1718     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1722 # recalculate prices and taxes
 
1724 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1728   my %pat = $self->order->calculate_prices_and_taxes();
 
1730   $self->{taxes} = [];
 
1731   foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
 
1732     my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
 
1734     push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
 
1735                                 netamount => $netamount,
 
1736                                 tax       => SL::DB::Tax->new(id => $tax_id)->load });
 
1738   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
 
1741 # get data for saving, printing, ..., that is not changed in the form
 
1743 # Only cvars for now.
 
1744 sub get_unalterable_data {
 
1747   foreach my $item (@{ $self->order->items }) {
 
1748     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1749     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1750     foreach my $var (@{ $item->cvars_by_config }) {
 
1751       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1753     $item->parse_custom_variable_values;
 
1759 # And remove related files in the spool directory
 
1764   my $db     = $self->order->db;
 
1766   $db->with_transaction(
 
1768       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1769       $self->order->delete;
 
1770       my $spool = $::lx_office_conf{paths}->{spool};
 
1771       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1773       $self->save_history('DELETED');
 
1776   }) || push(@{$errors}, $db->error);
 
1783 # And delete items that are deleted in the form.
 
1788   my $db     = $self->order->db;
 
1790   $db->with_transaction(sub {
 
1791     # delete custom shipto if it is to be deleted or if it is empty
 
1792     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1793       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1794       $self->order->custom_shipto(undef);
 
1797     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1798     $self->order->save(cascade => 1);
 
1801     if ($::form->{converted_from_oe_id}) {
 
1802       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1804       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1805         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1806         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
 
1807         $src->link_to_record($self->order);
 
1809       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1811         foreach (@{ $self->order->items_sorted }) {
 
1812           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1814           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1815                                   from_id    => $from_id,
 
1816                                   to_table   => 'orderitems',
 
1823       $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
 
1826     $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
 
1828     $self->save_history('SAVED');
 
1831   }) || push(@{$errors}, $db->error);
 
1836 sub workflow_sales_or_request_for_quotation {
 
1840   my $errors = $self->save();
 
1842   if (scalar @{ $errors }) {
 
1843     $self->js->flash('error', $_) for @{ $errors };
 
1844     return $self->js->render();
 
1847   my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
 
1849   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1850   $self->{converted_from_oe_id} = delete $::form->{id};
 
1852   # set item ids to new fake id, to identify them as new items
 
1853   foreach my $item (@{$self->order->items_sorted}) {
 
1854     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1858   $::form->{type} = $destination_type;
 
1859   $self->type($self->init_type);
 
1860   $self->cv  ($self->init_cv);
 
1864   $self->get_unalterable_data();
 
1865   $self->pre_render();
 
1867   # trigger rendering values for second row as hidden, because they
 
1868   # are loaded only on demand. So we need to keep the values from the
 
1870   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1874     title => $self->get_title_for('edit'),
 
1875     %{$self->{template_args}}
 
1879 sub workflow_sales_or_purchase_order {
 
1883   my $errors = $self->save();
 
1885   if (scalar @{ $errors }) {
 
1886     $self->js->flash('error', $_) foreach @{ $errors };
 
1887     return $self->js->render();
 
1890   my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
 
1891                        : $::form->{type} eq request_quotation_type() ? purchase_order_type()
 
1892                        : $::form->{type} eq purchase_order_type()    ? sales_order_type()
 
1893                        : $::form->{type} eq sales_order_type()       ? purchase_order_type()
 
1896   # check for direct delivery
 
1897   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1899   if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
 
1900       && $::form->{use_shipto} && $self->order->shipto) {
 
1901     $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
 
1904   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1905   $self->{converted_from_oe_id} = delete $::form->{id};
 
1907   # set item ids to new fake id, to identify them as new items
 
1908   foreach my $item (@{$self->order->items_sorted}) {
 
1909     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1912   if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
 
1913     if ($::form->{use_shipto}) {
 
1914       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
1916       # remove any custom shipto if not wanted
 
1917       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1922   $::form->{type} = $destination_type;
 
1923   $self->type($self->init_type);
 
1924   $self->cv  ($self->init_cv);
 
1928   $self->get_unalterable_data();
 
1929   $self->pre_render();
 
1931   # trigger rendering values for second row as hidden, because they
 
1932   # are loaded only on demand. So we need to keep the values from the
 
1934   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1938     title => $self->get_title_for('edit'),
 
1939     %{$self->{template_args}}
 
1947   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1948   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
1949   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1950   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted();
 
1951   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1954   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1957   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1959   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1960   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1961   $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1962   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1963   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1965   my $print_form = Form->new('');
 
1966   $print_form->{type}        = $self->type;
 
1967   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
1968   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
1969     form => $print_form,
 
1970     options => {dialog_name_prefix => 'print_options.',
 
1974                 no_opendocument    => 0,
 
1978   foreach my $item (@{$self->order->orderitems}) {
 
1979     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1980     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1981     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1984   if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
 
1985     # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
 
1986     # Do not use write_to_objects to prevent order->delivered to be set, because this should be
 
1987     # the value from db, which can be set manually or is set when linked delivery orders are saved.
 
1988     SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
 
1991   if ($self->order->number && $::instance_conf->get_webdav) {
 
1992     my $webdav = SL::Webdav->new(
 
1993       type     => $self->type,
 
1994       number   => $self->order->number,
 
1996     my @all_objects = $webdav->get_all_objects;
 
1997     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1999                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
2003   if (   (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
 
2004       && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
 
2005     $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
 
2008   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
2010   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
2011                                                          edit_periodic_invoices_config calculate_qty follow_up show_history);
 
2012   $self->setup_edit_action_bar;
 
2015 sub setup_edit_action_bar {
 
2016   my ($self, %params) = @_;
 
2018   my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
 
2019                       || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
2020                       || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
2022   my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
 
2023   my @req_cusordnumber   = qw(kivi.Order.check_cusordnumber_presence)           x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
 
2025   my $has_invoice_for_advance_payment;
 
2026   if ($self->order->id && $self->type eq sales_order_type()) {
 
2027     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2028     $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
 
2031   my $has_final_invoice;
 
2032   if ($self->order->id && $self->type eq sales_order_type()) {
 
2033     my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
 
2034     $has_final_invoice               = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
 
2037   my $right_for         = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
2038   my $right             = $right_for->{ $self->type };
 
2039   $right              ||= 'DOES_NOT_EXIST';
 
2040   my $may_edit_create   = $::auth->assert($right, 'may fail');
 
2042   for my $bar ($::request->layout->get('actionbar')) {
 
2047           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
2048                                                     $::instance_conf->get_order_warn_no_deliverydate,
 
2050           checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
 
2051                          @req_trans_cost_art, @req_cusordnumber,
 
2053           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2057           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
2058           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2059                          @req_trans_cost_art, @req_cusordnumber,
 
2061           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.')
 
2062                      : !$self->order->id ? t8('This object has not been saved yet.')
 
2065       ], # end of combobox "Save"
 
2072           t8('Save and Quotation'),
 
2073           submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
 
2074           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2075           only_if  => (any { $self->type eq $_ } (sales_order_type())),
 
2076           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2080           submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
 
2081           only_if  => (any { $self->type eq $_ } (purchase_order_type())),
 
2082           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2085           t8('Save and Sales Order'),
 
2086           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
2087           checks   => [ @req_trans_cost_art ],
 
2088           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
 
2089           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2092           t8('Save and Purchase Order'),
 
2093           call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
 
2094           checks    => [ @req_trans_cost_art, @req_cusordnumber ],
 
2095           only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
 
2096           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2099           t8('Save and Delivery Order'),
 
2100           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2101                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2103           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2104                          @req_trans_cost_art, @req_cusordnumber,
 
2106           only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())),
 
2107           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2110           t8('Save and Supplier Delivery Order'),
 
2111           call      => [ 'kivi.Order.save', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
2112                                                                        $::instance_conf->get_order_warn_no_deliverydate,
 
2114           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2115                          @req_trans_cost_art, @req_cusordnumber,
 
2117           only_if   => (any { $self->type eq $_ } (purchase_order_type())),
 
2118           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2121           t8('Save and Invoice'),
 
2122           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2123           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2124                          @req_trans_cost_art, @req_cusordnumber,
 
2126           disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
2129           ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
 
2130           call      => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::instance_conf->get_order_warn_duplicate_parts ],
 
2131           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2132                          @req_trans_cost_art, @req_cusordnumber,
 
2134           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2135                      : $has_final_invoice ? t8('This order has already a final invoice.')
 
2137           only_if   => (any { $self->type eq $_ } (sales_order_type())),
 
2140           t8('Save and Final Invoice'),
 
2141           call      => [ 'kivi.Order.save', 'save_and_final_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
2142           checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
 
2143                          @req_trans_cost_art, @req_cusordnumber,
 
2145           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2146                      : $has_final_invoice ? t8('This order has already a final invoice.')
 
2148           only_if   => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
 
2151           t8('Save and AP Transaction'),
 
2152           call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
2153           only_if   => (any { $self->type eq $_ } (purchase_order_type())),
 
2154           disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2157       ], # end of combobox "Workflow"
 
2164           t8('Save and preview PDF'),
 
2165           call     => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
2166                                                           $::instance_conf->get_order_warn_no_deliverydate,
 
2168           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2169           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2172           t8('Save and print'),
 
2173           call     => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
2174                                                          $::instance_conf->get_order_warn_no_deliverydate,
 
2176           checks   => [ @req_trans_cost_art, @req_cusordnumber ],
 
2177           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
 
2180           t8('Save and E-mail'),
 
2181           id       => 'save_and_email_action',
 
2182           call     => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
2183                                                                          $::instance_conf->get_order_warn_no_deliverydate,
 
2185           disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2186                     : !$self->order->id  ? t8('This object has not been saved yet.')
 
2190           t8('Download attachments of all parts'),
 
2191           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
2192           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
2193           only_if  => $::instance_conf->get_doc_storage,
 
2195       ], # end of combobox "Export"
 
2199         call     => [ 'kivi.Order.delete_order' ],
 
2200         confirm  => $::locale->text('Do you really want to delete this object?'),
 
2201         disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
 
2202                   : !$self->order->id  ? t8('This object has not been saved yet.')
 
2204         only_if  => $deletion_allowed,
 
2212   my ($self, $doc_ref, $params) = @_;
 
2214   my $order  = $self->order;
 
2217   my $print_form = Form->new('');
 
2218   $print_form->{type}        = $order->type;
 
2219   $print_form->{formname}    = $params->{formname} || $order->type;
 
2220   $print_form->{format}      = $params->{format}   || 'pdf';
 
2221   $print_form->{media}       = $params->{media}    || 'file';
 
2222   $print_form->{groupitems}  = $params->{groupitems};
 
2223   $print_form->{printer_id}  = $params->{printer_id};
 
2224   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
2226   $order->language($params->{language});
 
2227   $order->flatten_to_form($print_form, format_amounts => 1);
 
2231   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
2232     $template_ext  = 'odt';
 
2233     $template_type = 'OpenDocument';
 
2234   } elsif ($print_form->{format} =~ m{html}i) {
 
2235     $template_ext  = 'html';
 
2236     $template_type = 'HTML';
 
2239   # search for the template
 
2240   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
2241     name        => $print_form->{formname},
 
2242     extension   => $template_ext,
 
2243     email       => $print_form->{media} eq 'email',
 
2244     language    => $params->{language},
 
2245     printer_id  => $print_form->{printer_id},
 
2248   if (!defined $template_file) {
 
2249     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);
 
2252   return @errors if scalar @errors;
 
2254   $print_form->throw_on_error(sub {
 
2256       $print_form->prepare_for_printing;
 
2258       $$doc_ref = SL::Helper::CreatePDF->create_pdf(
 
2259         format        => $print_form->{format},
 
2260         template_type => $template_type,
 
2261         template      => $template_file,
 
2262         variables     => $print_form,
 
2263         variable_content_types => {
 
2264           longdescription => 'html',
 
2265           partnotes       => 'html',
 
2267           $::form->get_variable_content_types_for_cvars,
 
2271     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2277 sub get_files_for_email_dialog {
 
2280   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2282   return %files if !$::instance_conf->get_doc_storage;
 
2284   if ($self->order->id) {
 
2285     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2286     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2287     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2288     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2292     uniq_by { $_->{id} }
 
2294       +{ id         => $_->part->id,
 
2295          partnumber => $_->part->partnumber }
 
2296     } @{$self->order->items_sorted};
 
2298   foreach my $part (@parts) {
 
2299     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2300     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2303   foreach my $key (keys %files) {
 
2304     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2310 sub make_periodic_invoices_config_from_yaml {
 
2311   my ($yaml_config) = @_;
 
2313   return if !$yaml_config;
 
2314   my $attr = SL::YAML::Load($yaml_config);
 
2315   return if 'HASH' ne ref $attr;
 
2316   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
2320 sub get_periodic_invoices_status {
 
2321   my ($self, $config) = @_;
 
2323   return                      if $self->type ne sales_order_type();
 
2324   return t8('not configured') if !$config;
 
2326   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
2327              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
2328              :                                                     die "Cannot get status of periodic invoices config";
 
2330   return $active ? t8('active') : t8('inactive');
 
2334   my ($self, $action) = @_;
 
2336   return '' if none { lc($action)} qw(add edit);
 
2339   # $::locale->text("Add Sales Order");
 
2340   # $::locale->text("Add Purchase Order");
 
2341   # $::locale->text("Add Quotation");
 
2342   # $::locale->text("Add Request for Quotation");
 
2343   # $::locale->text("Edit Sales Order");
 
2344   # $::locale->text("Edit Purchase Order");
 
2345   # $::locale->text("Edit Quotation");
 
2346   # $::locale->text("Edit Request for Quotation");
 
2348   $action = ucfirst(lc($action));
 
2349   return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
 
2350        : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
2351        : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
 
2352        : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
2356 sub get_item_cvpartnumber {
 
2357   my ($self, $item) = @_;
 
2359   return if !$self->search_cvpartnumber;
 
2360   return if !$self->order->customervendor;
 
2362   if ($self->cv eq 'vendor') {
 
2363     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2364     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2365   } elsif ($self->cv eq 'customer') {
 
2366     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2367     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2371 sub get_part_texts {
 
2372   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2374   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2375   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2377     description     => $defaults{description}     // $part->description,
 
2378     longdescription => $defaults{longdescription} // $part->notes,
 
2381   return $texts unless $language_id;
 
2383   my $translation = SL::DB::Manager::Translation->get_first(
 
2385       parts_id    => $part->id,
 
2386       language_id => $language_id,
 
2389   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2390   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2395 sub sales_order_type {
 
2399 sub purchase_order_type {
 
2403 sub sales_quotation_type {
 
2407 sub request_quotation_type {
 
2408   'request_quotation';
 
2412   return $_[0]->type eq sales_order_type()       ? 'ordnumber'
 
2413        : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
 
2414        : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
 
2415        : $_[0]->type eq request_quotation_type() ? 'quonumber'
 
2419 sub save_and_redirect_to {
 
2420   my ($self, %params) = @_;
 
2422   my $errors = $self->save();
 
2424   if (scalar @{ $errors }) {
 
2425     $self->js->flash('error', $_) foreach @{ $errors };
 
2426     return $self->js->render();
 
2429   my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
 
2430            : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
 
2431            : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
2432            : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
 
2434   flash_later('info', $text);
 
2436   $self->redirect_to(%params, id => $self->order->id);
 
2440   my ($self, $addition) = @_;
 
2442   my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
 
2443   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2445   SL::DB::History->new(
 
2446     trans_id    => $self->order->id,
 
2447     employee_id => SL::DB::Manager::Employee->current->id,
 
2448     what_done   => $self->order->type,
 
2449     snumbers    => $snumbers,
 
2450     addition    => $addition,
 
2454 sub store_doc_to_webdav_and_filemanagement {
 
2455   my ($self, $content, $filename, $variant) = @_;
 
2457   my $order = $self->order;
 
2460   # copy file to webdav folder
 
2461   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2462     my $webdav = SL::Webdav->new(
 
2463       type     => $order->type,
 
2464       number   => $order->number,
 
2466     my $webdav_file = SL::Webdav::File->new(
 
2468       filename => $filename,
 
2471       $webdav_file->store(data => \$content);
 
2474       push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
 
2477   if ($order->id && $::instance_conf->get_doc_storage) {
 
2479       SL::File->save(object_id     => $order->id,
 
2480                      object_type   => $order->type,
 
2481                      mime_type     => SL::MIME->mime_type_from_ext($filename),
 
2482                      source        => 'created',
 
2483                      file_type     => 'document',
 
2484                      file_name     => $filename,
 
2485                      file_contents => $content,
 
2486                      print_variant => $variant);
 
2489       push @errors, t8('Storing the document in the storage backend failed: #1', $@);
 
2496 sub link_requirement_specs_linking_to_created_from_objects {
 
2497   my ($self, @converted_from_oe_ids) = @_;
 
2499   return unless @converted_from_oe_ids;
 
2501   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
 
2502   foreach my $rs_order (@{ $rs_orders }) {
 
2503     SL::DB::RequirementSpecOrder->new(
 
2504       order_id            => $self->order->id,
 
2505       requirement_spec_id => $rs_order->requirement_spec_id,
 
2506       version_id          => $rs_order->version_id,
 
2511 sub set_project_in_linked_requirement_specs {
 
2514   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
 
2515   foreach my $rs_order (@{ $rs_orders }) {
 
2516     next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
 
2518     $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
 
2530 SL::Controller::Order - controller for orders
 
2534 This is a new form to enter orders, completely rewritten with the use
 
2535 of controller and java script techniques.
 
2537 The aim is to provide the user a better experience and a faster workflow. Also
 
2538 the code should be more readable, more reliable and better to maintain.
 
2546 One input row, so that input happens every time at the same place.
 
2550 Use of pickers where possible.
 
2554 Possibility to enter more than one item at once.
 
2558 Item list in a scrollable area, so that the workflow buttons stay at
 
2563 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2564 possible (by partnumber, description, qty, sellprice and discount for now).
 
2568 No C<update> is necessary. All entries and calculations are managed
 
2569 with ajax-calls and the page only reloads on C<save>.
 
2573 User can see changes immediately, because of the use of java script
 
2584 =item * C<SL/Controller/Order.pm>
 
2588 =item * C<template/webpages/order/form.html>
 
2592 =item * C<template/webpages/order/tabs/basic_data.html>
 
2594 Main tab for basic_data.
 
2596 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2597 reused from generic code.
 
2601 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
2603 For displaying information on business type
 
2605 =item * C<template/webpages/order/tabs/_item_input.html>
 
2607 The input line for items
 
2609 =item * C<template/webpages/order/tabs/_row.html>
 
2611 One row for already entered items
 
2613 =item * C<template/webpages/order/tabs/_tax_row.html>
 
2615 Displaying tax information
 
2617 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
2619 Dialog for selecting price and discount sources
 
2623 =item * C<js/kivi.Order.js>
 
2625 java script functions
 
2635 =item * price sources: little symbols showing better price / better discount
 
2637 =item * select units in input row?
 
2639 =item * check for direct delivery (workflow sales order -> purchase order)
 
2641 =item * access rights
 
2643 =item * display weights
 
2647 =item * optional client/user behaviour
 
2649 (transactions has to be set - department has to be set -
 
2650  force project if enabled in client config)
 
2654 =head1 KNOWN BUGS AND CAVEATS
 
2660 Customer discount is not displayed as a valid discount in price source popup
 
2661 (this might be a bug in price sources)
 
2663 (I cannot reproduce this (Bernd))
 
2667 No indication that <shift>-up/down expands/collapses second row.
 
2671 Inline creation of parts is not currently supported
 
2675 Table header is not sticky in the scrolling area.
 
2679 Sorting does not include C<position>, neither does reordering.
 
2681 This behavior was implemented intentionally. But we can discuss, which behavior
 
2682 should be implemented.
 
2686 =head1 To discuss / Nice to have
 
2692 How to expand/collapse second row. Now it can be done clicking the icon or
 
2697 Possibility to select PriceSources in input row?
 
2701 This controller uses a (changed) copy of the template for the PriceSource
 
2702 dialog. Maybe there could be used one code source.
 
2706 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2707 form. This is not only a problem here, but also in all parts using the PTC.
 
2708 There exists a ticket and a patch. This patch should be testet.
 
2712 An indicator, if the actual inputs are saved (like in an
 
2713 editor or on text processing application).
 
2717 A warning when leaving the page without saveing unchanged inputs.
 
2724 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>