1 package SL::Controller::Order;
 
   4 use parent qw(SL::Controller::Base);
 
   6 use SL::Helper::Flash qw(flash_later);
 
   7 use SL::Presenter::Tag qw(select_tag hidden_tag);
 
   8 use SL::Locale::String qw(t8);
 
   9 use SL::SessionFile::Random;
 
  13 use SL::Util qw(trim);
 
  20 use SL::DB::RecordLink;
 
  22 use SL::Helper::CreatePDF qw(:all);
 
  23 use SL::Helper::PrintOptions;
 
  25 use SL::Controller::Helper::GetModels;
 
  27 use List::Util qw(first);
 
  28 use List::UtilsBy qw(sort_by uniq_by);
 
  29 use List::MoreUtils qw(any none pairwise first_index);
 
  30 use English qw(-no_match_vars);
 
  34 use Rose::Object::MakeMethods::Generic
 
  36  scalar => [ qw(item_ids_to_delete) ],
 
  37  'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
 
  42 __PACKAGE__->run_before('_check_auth');
 
  44 __PACKAGE__->run_before('_recalc',
 
  45                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
 
  47 __PACKAGE__->run_before('_get_unalterable_data',
 
  48                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
 
  58   $self->order->transdate(DateTime->now_local());
 
  59   my $extra_days = $self->type eq _sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1;
 
  60   $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
 
  65     title => $self->_get_title_for('add'),
 
  66     %{$self->{template_args}}
 
  70 # edit an existing order
 
  79     title => $self->_get_title_for('edit'),
 
  80     %{$self->{template_args}}
 
  88   my $errors = $self->_delete();
 
  90   if (scalar @{ $errors }) {
 
  91     $self->js->flash('error', $_) foreach @{ $errors };
 
  92     return $self->js->render();
 
  95   my $text = $self->type eq _sales_order_type()       ? $::locale->text('The order has been deleted')
 
  96            : $self->type eq _purchase_order_type()    ? $::locale->text('The order has been deleted')
 
  97            : $self->type eq _sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
  98            : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
 100   flash_later('info', $text);
 
 102   my @redirect_params = (
 
 107   $self->redirect_to(@redirect_params);
 
 114   my $errors = $self->_save();
 
 116   if (scalar @{ $errors }) {
 
 117     $self->js->flash('error', $_) foreach @{ $errors };
 
 118     return $self->js->render();
 
 121   my $text = $self->type eq _sales_order_type()       ? $::locale->text('The order has been saved')
 
 122            : $self->type eq _purchase_order_type()    ? $::locale->text('The order has been saved')
 
 123            : $self->type eq _sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 124            : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 126   flash_later('info', $text);
 
 128   my @redirect_params = (
 
 131     id     => $self->order->id,
 
 134   $self->redirect_to(@redirect_params);
 
 137 # save the order as new document an open it for edit
 
 138 sub action_save_as_new {
 
 141   my $order = $self->order;
 
 144     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 145     return $self->js->render();
 
 148   # load order from db to check if values changed
 
 149   my $saved_order = SL::DB::Order->new(id => $order->id)->load;
 
 152   # Lets assign a new number if the user hasn't changed the previous one.
 
 153   # If it has been changed manually then use it as-is.
 
 154   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 156                         : trim($order->number);
 
 158   # Clear transdate unless changed
 
 159   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 160                         ? DateTime->today_local
 
 163   # Set new reqdate unless changed
 
 164   if ($order->reqdate == $saved_order->reqdate) {
 
 165     my $extra_days = $self->type eq _sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1;
 
 166     $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
 
 168     $new_attrs{reqdate} = $order->reqdate;
 
 172   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 174   # Create new record from current one
 
 175   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 177   # no linked records on save as new
 
 178   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 181   $self->action_save();
 
 186 # This is called if "print" is pressed in the print dialog.
 
 187 # If PDF creation was requested and succeeded, the pdf is stored in a session
 
 188 # file and the filename is stored as session value with an unique key. A
 
 189 # javascript function with this key is then called. This function calls the
 
 190 # download action below (action_download_pdf), which offers the file for
 
 195   my $format      = $::form->{print_options}->{format};
 
 196   my $media       = $::form->{print_options}->{media};
 
 197   my $formname    = $::form->{print_options}->{formname};
 
 198   my $copies      = $::form->{print_options}->{copies};
 
 199   my $groupitems  = $::form->{print_options}->{groupitems};
 
 202   if (none { $format eq $_ } qw(pdf)) {
 
 203     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 206   # only screen or printer by now
 
 207   if (none { $media eq $_ } qw(screen printer)) {
 
 208     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 212   $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 214   # create a form for generate_attachment_filename
 
 215   my $form = Form->new;
 
 216   $form->{ordnumber} = $self->order->ordnumber;
 
 217   $form->{type}      = $self->type;
 
 218   $form->{format}    = $format;
 
 219   $form->{formname}  = $formname;
 
 220   $form->{language}  = '_' . $language->template_code if $language;
 
 221   my $pdf_filename   = $form->generate_attachment_filename();
 
 224   my @errors = _create_pdf($self->order, \$pdf, { format     => $format,
 
 225                                                   formname   => $formname,
 
 226                                                   language   => $language,
 
 227                                                   groupitems => $groupitems });
 
 228   if (scalar @errors) {
 
 229     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 232   if ($media eq 'screen') {
 
 234     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 235     $sfile->fh->print($pdf);
 
 238     my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 239     $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
 
 242     ->run('kivi.Order.download_pdf', $pdf_filename, $key)
 
 243     ->flash('info', t8('The PDF has been created'));
 
 245   } elsif ($media eq 'printer') {
 
 247     my $printer_id = $::form->{print_options}->{printer_id};
 
 248     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 253     $self->js->flash('info', t8('The PDF has been printed'));
 
 256   # copy file to webdav folder
 
 257   if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
 
 258     my $webdav = SL::Webdav->new(
 
 260       number   => $self->order->ordnumber,
 
 262     my $webdav_file = SL::Webdav::File->new(
 
 264       filename => $pdf_filename,
 
 267       $webdav_file->store(data => \$pdf);
 
 270       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
 
 273   if ($self->order->ordnumber && $::instance_conf->get_doc_storage) {
 
 275       SL::File->save(object_id     => $self->order->id,
 
 276                      object_type   => $self->type,
 
 277                      mime_type     => 'application/pdf',
 
 279                      file_type     => 'document',
 
 280                      file_name     => $pdf_filename,
 
 281                      file_contents => $pdf);
 
 284       $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
 
 290 # offer pdf for download
 
 292 # It needs to get the key for the session value to get the pdf file.
 
 293 sub action_download_pdf {
 
 296   my $key = $::form->{key};
 
 297   my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
 
 298   return $self->send_file(
 
 300     type => 'application/pdf',
 
 301     name => $::form->{pdf_filename},
 
 305 # open the email dialog
 
 306 sub action_show_email_dialog {
 
 309   my $cv_method = $self->cv;
 
 311   if (!$self->order->$cv_method) {
 
 312     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'))
 
 317   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 318   $email_form->{to} ||= $self->order->$cv_method->email;
 
 319   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 320   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 321   # Todo: get addresses from shipto, if any
 
 323   my $form = Form->new;
 
 324   $form->{ordnumber} = $self->order->ordnumber;
 
 325   $form->{formname}  = $self->type;
 
 326   $form->{type}      = $self->type;
 
 327   $form->{language} = 'de';
 
 328   $form->{format}   = 'pdf';
 
 330   $email_form->{subject}             = $form->generate_email_subject();
 
 331   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 332   $email_form->{message}             = $form->generate_email_body();
 
 333   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 335   my %files = $self->_get_files_for_email_dialog();
 
 336   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 337                                   email_form  => $email_form,
 
 338                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
 
 340                                   is_customer => $self->cv eq 'customer',
 
 344       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 351 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 352 sub action_send_email {
 
 355   my $email_form  = delete $::form->{email_form};
 
 356   my %field_names = (to => 'email');
 
 358   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 360   # for Form::cleanup which may be called in Form::send_email
 
 361   $::form->{cwd}    = getcwd();
 
 362   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 364   $::form->{media}  = 'email';
 
 366   if (($::form->{attachment_policy} // '') eq 'normal') {
 
 368     $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 371     my @errors = _create_pdf($self->order, \$pdf, {media      => $::form->{media},
 
 372                                                    format     => $::form->{print_options}->{format},
 
 373                                                    formname   => $::form->{print_options}->{formname},
 
 374                                                    language   => $language,
 
 375                                                    groupitems => $::form->{print_options}->{groupitems}});
 
 376     if (scalar @errors) {
 
 377       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
 
 380     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 381     $sfile->fh->print($pdf);
 
 384     $::form->{tmpfile} = $sfile->file_name;
 
 385     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 388   $::form->send_email(\%::myconfig, 'pdf');
 
 391   my $intnotes = $self->order->intnotes;
 
 392   $intnotes   .= "\n\n" if $self->order->intnotes;
 
 393   $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 394   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 395   $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 396   $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 397   $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 398   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 399   $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
 402       ->val('#order_intnotes', $intnotes)
 
 403       ->run('kivi.Order.close_email_dialog')
 
 404       ->flash('info', t8('The email has been sent.'))
 
 408 # open the periodic invoices config dialog
 
 410 # If there are values in the form (i.e. dialog was opened before),
 
 411 # then use this values. Create new ones, else.
 
 412 sub action_show_periodic_invoices_config_dialog {
 
 415   my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 416   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 417   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 418                                                    order_value_periodicity => 'p', # = same as periodicity
 
 419                                                    start_date_as_date      => $::form->{transdate} || $::form->current_date,
 
 420                                                    extend_automatically_by => 12,
 
 422                                                    email_subject           => GenericTranslations->get(
 
 423                                                                                 language_id      => $::form->{language_id},
 
 424                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 425                                                    email_body              => GenericTranslations->get(
 
 426                                                                                 language_id      => $::form->{language_id},
 
 427                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 429   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 430   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 432   $::form->get_lists(printers => "ALL_PRINTERS",
 
 433                      charts   => { key       => 'ALL_CHARTS',
 
 434                                    transdate => 'current_date' });
 
 436   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 438   if ($::form->{customer_id}) {
 
 439     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 442   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 444                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 445                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 450 # assign the values of the periodic invoices config dialog
 
 451 # as yaml in the hidden tag and set the status.
 
 452 sub action_assign_periodic_invoices_config {
 
 455   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 457   my $config = { active                  => $::form->{active}     ? 1 : 0,
 
 458                  terminated              => $::form->{terminated} ? 1 : 0,
 
 459                  direct_debit            => $::form->{direct_debit} ? 1 : 0,
 
 460                  periodicity             => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 461                  order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 462                  start_date_as_date      => $::form->{start_date_as_date},
 
 463                  end_date_as_date        => $::form->{end_date_as_date},
 
 464                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 465                  print                   => $::form->{print} ? 1 : 0,
 
 466                  printer_id              => $::form->{print} ? $::form->{printer_id} * 1 : undef,
 
 467                  copies                  => $::form->{copies} * 1 ? $::form->{copies} : 1,
 
 468                  extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
 
 469                  ar_chart_id             => $::form->{ar_chart_id} * 1,
 
 470                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 471                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 472                  email_recipient_address    => $::form->{email_recipient_address},
 
 473                  email_sender               => $::form->{email_sender},
 
 474                  email_subject              => $::form->{email_subject},
 
 475                  email_body                 => $::form->{email_body},
 
 478   my $periodic_invoices_config = YAML::Dump($config);
 
 480   my $status = $self->_get_periodic_invoices_status($config);
 
 483     ->remove('#order_periodic_invoices_config')
 
 484     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 485     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 486     ->html('#periodic_invoices_status', $status)
 
 487     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 491 sub action_get_has_active_periodic_invoices {
 
 494   my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 495   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 497   my $has_active_periodic_invoices =
 
 498        $self->type eq _sales_order_type()
 
 501     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 502     && $config->get_previous_billed_period_start_date;
 
 504   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 507 # save the order and redirect to the frontend subroutine for a new
 
 509 sub action_save_and_delivery_order {
 
 512   my $errors = $self->_save();
 
 514   if (scalar @{ $errors }) {
 
 515     $self->js->flash('error', $_) foreach @{ $errors };
 
 516     return $self->js->render();
 
 519   my $text = $self->type eq _sales_order_type()       ? $::locale->text('The order has been saved')
 
 520            : $self->type eq _purchase_order_type()    ? $::locale->text('The order has been saved')
 
 521            : $self->type eq _sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 522            : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 524   flash_later('info', $text);
 
 526   my @redirect_params = (
 
 527     controller => 'oe.pl',
 
 528     action     => 'oe_delivery_order_from_order',
 
 529     id         => $self->order->id,
 
 532   $self->redirect_to(@redirect_params);
 
 535 # save the order and redirect to the frontend subroutine for a new
 
 537 sub action_save_and_invoice {
 
 540   my $errors = $self->_save();
 
 542   if (scalar @{ $errors }) {
 
 543     $self->js->flash('error', $_) foreach @{ $errors };
 
 544     return $self->js->render();
 
 547   my $text = $self->type eq _sales_order_type()       ? $::locale->text('The order has been saved')
 
 548            : $self->type eq _purchase_order_type()    ? $::locale->text('The order has been saved')
 
 549            : $self->type eq _sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 550            : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 552   flash_later('info', $text);
 
 554   my @redirect_params = (
 
 555     controller => 'oe.pl',
 
 556     action     => 'oe_invoice_from_order',
 
 557     id         => $self->order->id,
 
 560   $self->redirect_to(@redirect_params);
 
 563 # workflow from sales quotation to sales order
 
 564 sub action_sales_order {
 
 565   $_[0]->_workflow_sales_or_purchase_order();
 
 568 # workflow from rfq to purchase order
 
 569 sub action_purchase_order {
 
 570   $_[0]->_workflow_sales_or_purchase_order();
 
 573 # set form elements in respect to a changed customer or vendor
 
 575 # This action is called on an change of the customer/vendor picker.
 
 576 sub action_customer_vendor_changed {
 
 579   my $cv_method = $self->cv;
 
 581   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 582     $self->js->show('#cp_row');
 
 584     $self->js->hide('#cp_row');
 
 587   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 588     $self->js->show('#shipto_row');
 
 590     $self->js->hide('#shipto_row');
 
 593   $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
 
 595   if ($self->order->is_sales) {
 
 596     $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
 
 597                               ? $self->order->$cv_method->taxincluded_checked
 
 598                               : $::myconfig{taxincluded_checked});
 
 599     $self->js->val('#order_salesman_id', $self->order->$cv_method->salesman_id);
 
 602   $self->order->payment_id($self->order->$cv_method->payment_id);
 
 603   $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
 
 608     ->replaceWith('#order_cp_id',            $self->build_contact_select)
 
 609     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
 
 610     ->replaceWith('#business_info_row',      $self->build_business_info_row)
 
 611     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
 
 612     ->val(        '#order_taxincluded',      $self->order->taxincluded)
 
 613     ->val(        '#order_payment_id',       $self->order->payment_id)
 
 614     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
 
 615     ->val(        '#order_intnotes',         $self->order->$cv_method->notes)
 
 616     ->focus(      '#order_' . $self->cv . '_id');
 
 618   $self->_js_redisplay_amounts_and_taxes;
 
 622 # called if a unit in an existing item row is changed
 
 623 sub action_unit_changed {
 
 626   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 627   my $item = $self->order->items_sorted->[$idx];
 
 629   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 630   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 635     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 636   $self->_js_redisplay_line_values;
 
 637   $self->_js_redisplay_amounts_and_taxes;
 
 641 # add an item row for a new item entered in the input row
 
 642 sub action_add_item {
 
 645   my $form_attr = $::form->{add_item};
 
 647   return unless $form_attr->{parts_id};
 
 649   my $item = _new_item($self->order, $form_attr);
 
 651   $self->order->add_items($item);
 
 655   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 656   my $row_as_html = $self->p->render('order/tabs/_row',
 
 660                                      ALL_PRICE_FACTORS => $self->all_price_factors
 
 664     ->append('#row_table_id', $row_as_html);
 
 666   if ( $item->part->is_assortment ) {
 
 667     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 668     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 669       my $attr = { parts_id => $assortment_item->parts_id,
 
 670                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 671                    unit     => $assortment_item->unit,
 
 672                    description => $assortment_item->part->description,
 
 674       my $item = _new_item($self->order, $attr);
 
 676       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 677       $item->discount(1) unless $assortment_item->charge;
 
 679       $self->order->add_items( $item );
 
 681       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 682       my $row_as_html = $self->p->render('order/tabs/_row',
 
 686                                          ALL_PRICE_FACTORS => $self->all_price_factors
 
 689         ->append('#row_table_id', $row_as_html);
 
 694     ->val('.add_item_input', '')
 
 695     ->run('kivi.Order.init_row_handlers')
 
 696     ->run('kivi.Order.row_table_scroll_down')
 
 697     ->run('kivi.Order.renumber_positions')
 
 698     ->focus('#add_item_parts_id_name');
 
 700   $self->_js_redisplay_amounts_and_taxes;
 
 704 # open the dialog for entering multiple items at once
 
 705 sub action_show_multi_items_dialog {
 
 706   require SL::DB::PartsGroup;
 
 707   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
 
 708                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
 
 711 # update the filter results in the multi item dialog
 
 712 sub action_multi_items_update_result {
 
 715   $::form->{multi_items}->{filter}->{obsolete} = 0;
 
 717   my $count = $_[0]->multi_items_models->count;
 
 720     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
 
 721     $_[0]->render($text, { layout => 0 });
 
 722   } elsif ($count > $max_count) {
 
 723     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
 
 724     $_[0]->render($text, { layout => 0 });
 
 726     my $multi_items = $_[0]->multi_items_models->get;
 
 727     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
 
 728                   multi_items => $multi_items);
 
 732 # add item rows for multiple items at once
 
 733 sub action_add_multi_items {
 
 736   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
 
 737   return $self->js->render() unless scalar @form_attr;
 
 740   foreach my $attr (@form_attr) {
 
 741     my $item = _new_item($self->order, $attr);
 
 743     if ( $item->part->is_assortment ) {
 
 744       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 745         my $attr = { parts_id => $assortment_item->parts_id,
 
 746                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 747                      unit     => $assortment_item->unit,
 
 748                      description => $assortment_item->part->description,
 
 750         my $item = _new_item($self->order, $attr);
 
 752         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 753         $item->discount(1) unless $assortment_item->charge;
 
 758   $self->order->add_items(@items);
 
 762   foreach my $item (@items) {
 
 763     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 764     my $row_as_html = $self->p->render('order/tabs/_row',
 
 768                                        ALL_PRICE_FACTORS => $self->all_price_factors
 
 771     $self->js->append('#row_table_id', $row_as_html);
 
 775     ->run('kivi.Order.close_multi_items_dialog')
 
 776     ->run('kivi.Order.init_row_handlers')
 
 777     ->run('kivi.Order.row_table_scroll_down')
 
 778     ->run('kivi.Order.renumber_positions')
 
 779     ->focus('#add_item_parts_id_name');
 
 781   $self->_js_redisplay_amounts_and_taxes;
 
 785 # recalculate all linetotals, amounts and taxes and redisplay them
 
 786 sub action_recalc_amounts_and_taxes {
 
 791   $self->_js_redisplay_line_values;
 
 792   $self->_js_redisplay_amounts_and_taxes;
 
 796 # redisplay item rows if they are sorted by an attribute
 
 797 sub action_reorder_items {
 
 801     partnumber  => sub { $_[0]->part->partnumber },
 
 802     description => sub { $_[0]->description },
 
 803     qty         => sub { $_[0]->qty },
 
 804     sellprice   => sub { $_[0]->sellprice },
 
 805     discount    => sub { $_[0]->discount },
 
 808   my $method = $sort_keys{$::form->{order_by}};
 
 809   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
 810   if ($::form->{sort_dir}) {
 
 811     @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 813     @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 816     ->run('kivi.Order.redisplay_items', \@to_sort)
 
 820 # show the popup to choose a price/discount source
 
 821 sub action_price_popup {
 
 824   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 825   my $item = $self->order->items_sorted->[$idx];
 
 827   $self->render_price_dialog($item);
 
 830 # get the longdescription for an item if the dialog to enter/change the
 
 831 # longdescription was opened and the longdescription is empty
 
 833 # If this item is new, get the longdescription from Part.
 
 834 # Otherwise get it from OrderItem.
 
 835 sub action_get_item_longdescription {
 
 838   if ($::form->{item_id}) {
 
 839     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
 
 840   } elsif ($::form->{parts_id}) {
 
 841     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
 
 843   $_[0]->render(\ $longdescription, { type => 'text' });
 
 846 # load the second row for one or more items
 
 848 # This action gets the html code for all items second rows by rendering a template for
 
 849 # the second row and sets the html code via client js.
 
 850 sub action_load_second_rows {
 
 853   $self->_recalc() if $self->order->is_sales; # for margin calculation
 
 855   foreach my $item_id (@{ $::form->{item_ids} }) {
 
 856     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
 857     my $item = $self->order->items_sorted->[$idx];
 
 859     $self->_js_load_second_row($item, $item_id, 0);
 
 862   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
 867 sub _js_load_second_row {
 
 868   my ($self, $item, $item_id, $do_parse) = @_;
 
 871     # Parse values from form (they are formated while rendering (template)).
 
 872     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
 873     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
 874     foreach my $var (@{ $item->cvars_by_config }) {
 
 875       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
 877     $item->parse_custom_variable_values;
 
 880   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
 883     ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
 
 884     ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
 
 887 sub _js_redisplay_line_values {
 
 890   my $is_sales = $self->order->is_sales;
 
 892   # sales orders with margins
 
 897        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
 898        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
 899        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
 900       ]} @{ $self->order->items_sorted };
 
 904        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
 905       ]} @{ $self->order->items_sorted };
 
 909     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
 912 sub _js_redisplay_amounts_and_taxes {
 
 915   if (scalar @{ $self->{taxes} }) {
 
 916     $self->js->show('#taxincluded_row_id');
 
 918     $self->js->hide('#taxincluded_row_id');
 
 921   if ($self->order->taxincluded) {
 
 922     $self->js->hide('#subtotal_row_id');
 
 924     $self->js->show('#subtotal_row_id');
 
 928     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
 929     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
 931     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
 938 sub init_valid_types {
 
 939   [ _sales_order_type(), _purchase_order_type(), _sales_quotation_type(), _request_quotation_type() ];
 
 945   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
 946     die "Not a valid type for order";
 
 949   $self->type($::form->{type});
 
 955   my $cv = (any { $self->type eq $_ } (_sales_order_type(),    _sales_quotation_type()))   ? 'customer'
 
 956          : (any { $self->type eq $_ } (_purchase_order_type(), _request_quotation_type())) ? 'vendor'
 
 957          : die "Not a valid type for order";
 
 970 # model used to filter/display the parts in the multi-items dialog
 
 971 sub init_multi_items_models {
 
 972   SL::Controller::Helper::GetModels->new(
 
 975     with_objects   => [ qw(unit_obj) ],
 
 976     disable_plugin => 'paginated',
 
 977     source         => $::form->{multi_items},
 
 983       partnumber  => t8('Partnumber'),
 
 984       description => t8('Description')}
 
 988 sub init_all_price_factors {
 
 989   SL::DB::Manager::PriceFactor->get_all;
 
 995   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
 997   my $right   = $right_for->{ $self->type };
 
 998   $right    ||= 'DOES_NOT_EXIST';
 
1000   $::auth->assert($right);
 
1003 # build the selection box for contacts
 
1005 # Needed, if customer/vendor changed.
 
1006 sub build_contact_select {
 
1009   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1010     value_key  => 'cp_id',
 
1011     title_key  => 'full_name_dep',
 
1012     default    => $self->order->cp_id,
 
1014     style      => 'width: 300px',
 
1018 # build the selection box for shiptos
 
1020 # Needed, if customer/vendor changed.
 
1021 sub build_shipto_select {
 
1024   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
 
1025     value_key  => 'shipto_id',
 
1026     title_key  => 'displayable_id',
 
1027     default    => $self->order->shipto_id,
 
1029     style      => 'width: 300px',
 
1033 # render the info line for business
 
1035 # Needed, if customer/vendor changed.
 
1036 sub build_business_info_row
 
1038   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
 
1041 # build the rows for displaying taxes
 
1043 # Called if amounts where recalculated and redisplayed.
 
1044 sub build_tax_rows {
 
1048   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
1049     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
1051   return $rows_as_html;
 
1055 sub render_price_dialog {
 
1056   my ($self, $record_item) = @_;
 
1058   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1062       'kivi.io.price_chooser_dialog',
 
1063       t8('Available Prices'),
 
1064       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1069 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1070 #     $self->js->show('#dialog_flash_error');
 
1079   return if !$::form->{id};
 
1081   $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
 
1084 # load or create a new order object
 
1086 # And assign changes from the form to this object.
 
1087 # If the order is loaded from db, check if items are deleted in the form,
 
1088 # remove them form the object and collect them for removing from db on saving.
 
1089 # Then create/update items from form (via _make_item) and add them.
 
1093   # add_items adds items to an order with no items for saving, but they cannot
 
1094   # be retrieved via items until the order is saved. Adding empty items to new
 
1095   # order here solves this problem.
 
1097   $order   = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
 
1098   $order ||= SL::DB::Order->new(orderitems => [],
 
1099                                 quotation  => (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type())));
 
1101   my $form_orderitems               = delete $::form->{order}->{orderitems};
 
1102   my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
 
1104   $order->assign_attributes(%{$::form->{order}});
 
1106   my $periodic_invoices_config = _make_periodic_invoices_config_from_yaml($form_periodic_invoices_config);
 
1107   $order->periodic_invoices_config($periodic_invoices_config) if $periodic_invoices_config;
 
1109   # remove deleted items
 
1110   $self->item_ids_to_delete([]);
 
1111   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1112     my $item = $order->orderitems->[$idx];
 
1113     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1114       splice @{$order->orderitems}, $idx, 1;
 
1115       push @{$self->item_ids_to_delete}, $item->id;
 
1121   foreach my $form_attr (@{$form_orderitems}) {
 
1122     my $item = _make_item($order, $form_attr);
 
1123     $item->position($pos);
 
1127   $order->add_items(grep {!$_->id} @items);
 
1132 # create or update items from form
 
1134 # Make item objects from form values. For items already existing read from db.
 
1135 # Create a new item else. And assign attributes.
 
1137   my ($record, $attr) = @_;
 
1140   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1142   my $is_new = !$item;
 
1144   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1145   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1146   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1147   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1149   $item->assign_attributes(%$attr);
 
1150   $item->longdescription($item->part->notes)                     if $is_new && !defined $attr->{longdescription};
 
1151   $item->project_id($record->globalproject_id)                   if $is_new && !defined $attr->{project_id};
 
1152   $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
 
1159 # This is used to add one item
 
1161   my ($record, $attr) = @_;
 
1163   my $item = SL::DB::OrderItem->new;
 
1164   $item->assign_attributes(%$attr);
 
1166   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1167   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1169   $item->unit($part->unit) if !$item->unit;
 
1172   if ( $part->is_assortment ) {
 
1173     # add assortment items with price 0, as the components carry the price
 
1174     $price_src = $price_source->price_from_source("");
 
1175     $price_src->price(0);
 
1176   } elsif ($item->sellprice) {
 
1177     $price_src = $price_source->price_from_source("");
 
1178     $price_src->price($item->sellprice);
 
1180     $price_src = $price_source->best_price
 
1181            ? $price_source->best_price
 
1182            : $price_source->price_from_source("");
 
1183     $price_src->price(0) if !$price_source->best_price;
 
1187   if ($item->discount) {
 
1188     $discount_src = $price_source->discount_from_source("");
 
1189     $discount_src->discount($item->discount);
 
1191     $discount_src = $price_source->best_discount
 
1192                   ? $price_source->best_discount
 
1193                   : $price_source->discount_from_source("");
 
1194     $discount_src->discount(0) if !$price_source->best_discount;
 
1198   $new_attr{part}                   = $part;
 
1199   $new_attr{description}            = $part->description     if ! $item->description;
 
1200   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1201   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1202   $new_attr{sellprice}              = $price_src->price;
 
1203   $new_attr{discount}               = $discount_src->discount;
 
1204   $new_attr{active_price_source}    = $price_src;
 
1205   $new_attr{active_discount_source} = $discount_src;
 
1206   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1207   $new_attr{project_id}             = $record->globalproject_id;
 
1208   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1210   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1211   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1212   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1213   $new_attr{custom_variables} = [];
 
1215   $item->assign_attributes(%new_attr);
 
1220 # recalculate prices and taxes
 
1222 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1226   # bb: todo: currency later
 
1227   $self->order->currency_id($::instance_conf->get_currency_id());
 
1229   my %pat = $self->order->calculate_prices_and_taxes();
 
1230   $self->{taxes} = [];
 
1231   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
 
1232     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
 
1234     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
 
1235     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
 
1236                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
 
1240   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
 
1243 # get data for saving, printing, ..., that is not changed in the form
 
1245 # Only cvars for now.
 
1246 sub _get_unalterable_data {
 
1249   foreach my $item (@{ $self->order->items }) {
 
1250     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1251     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1252     foreach my $var (@{ $item->cvars_by_config }) {
 
1253       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1255     $item->parse_custom_variable_values;
 
1261 # And remove related files in the spool directory
 
1266   my $db     = $self->order->db;
 
1268   $db->with_transaction(
 
1270       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1271       $self->order->delete;
 
1272       my $spool = $::lx_office_conf{paths}->{spool};
 
1273       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1276   }) || push(@{$errors}, $db->error);
 
1283 # And delete items that are deleted in the form.
 
1288   my $db     = $self->order->db;
 
1290   $db->with_transaction(sub {
 
1291     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
 
1292     $self->order->save(cascade => 1);
 
1295     if ($::form->{converted_from_oe_id}) {
 
1296       SL::DB::Order->new(id => $::form->{converted_from_oe_id})->load->link_to_record($self->order);
 
1298       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1300         foreach (@{ $self->order->items_sorted }) {
 
1301           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1303           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1304                                   from_id    => $from_id,
 
1305                                   to_table   => 'orderitems',
 
1313   }) || push(@{$errors}, $db->error);
 
1318 sub _workflow_sales_or_purchase_order {
 
1321   my $destination_type = $::form->{type} eq _sales_quotation_type()   ? _sales_order_type()
 
1322                        : $::form->{type} eq _request_quotation_type() ? _purchase_order_type()
 
1325   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1326   $self->{converted_from_oe_id} = delete $::form->{id};
 
1329   $::form->{type} = $destination_type;
 
1334   $self->_get_unalterable_data();
 
1335   $self->_pre_render();
 
1337   # trigger rendering values for second row/longdescription as hidden,
 
1338   # because they are loaded only on demand. So we need to keep the values
 
1340   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1341   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1345     title => $self->_get_title_for('edit'),
 
1346     %{$self->{template_args}}
 
1354   $self->{all_taxzones}             = SL::DB::Manager::TaxZone->get_all_sorted();
 
1355   $self->{all_departments}          = SL::DB::Manager::Department->get_all_sorted();
 
1356   $self->{all_employees}            = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1359   $self->{all_salesmen}             = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1362   $self->{all_projects}             = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
 
1364                                                                         sort_by => 'projectnumber');
 
1365   $self->{all_payment_terms}        = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1367   $self->{all_delivery_terms}       = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1368   $self->{current_employee_id}      = SL::DB::Manager::Employee->current->id;
 
1369   $self->{periodic_invoices_status} = $self->_get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1370   $self->{order_probabilities}      = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1372   my $print_form = Form->new('');
 
1373   $print_form->{type}      = $self->type;
 
1374   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
 
1375   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
 
1376   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
 
1377     form => $print_form,
 
1378     options => {dialog_name_prefix => 'print_options.',
 
1382                 no_opendocument    => 1,
 
1386   foreach my $item (@{$self->order->orderitems}) {
 
1387     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1388     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1389     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1392   if ($self->order->ordnumber && $::instance_conf->get_webdav) {
 
1393     my $webdav = SL::Webdav->new(
 
1394       type     => $self->type,
 
1395       number   => $self->order->ordnumber,
 
1397     my @all_objects = $webdav->get_all_objects;
 
1398     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1400                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1404   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config);
 
1405   $self->_setup_edit_action_bar;
 
1408 sub _setup_edit_action_bar {
 
1409   my ($self, %params) = @_;
 
1411   my $deletion_allowed = (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type()))
 
1412                       || (($self->type eq _sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1413                       || (($self->type eq _purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1415   for my $bar ($::request->layout->get('actionbar')) {
 
1420           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts ],
 
1421           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1425           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1426           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1427           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1430           t8('Save and Delivery Order'),
 
1431           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
 
1432           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1433           only_if   => (any { $self->type eq $_ } (_sales_order_type(), _purchase_order_type()))
 
1436           t8('Save and Invoice'),
 
1437           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1438           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1440       ], # end of combobox "Save"
 
1448           submit   => [ '#order_form', { action => "Order/sales_order" } ],
 
1449           only_if  => (any { $self->type eq $_ } (_sales_quotation_type())),
 
1450           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1453           t8('Purchase Order'),
 
1454           submit   => [ '#order_form', { action => "Order/purchase_order" } ],
 
1455           only_if  => (any { $self->type eq $_ } (_request_quotation_type())),
 
1456           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1458       ], # end of combobox "Workflow"
 
1466           call => [ 'kivi.Order.show_print_options' ],
 
1470           call => [ 'kivi.Order.email' ],
 
1473           t8('Download attachments of all parts'),
 
1474           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1475           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1476           only_if  => $::instance_conf->get_doc_storage,
 
1478       ], # end of combobox "Export"
 
1482         call     => [ 'kivi.Order.delete_order' ],
 
1483         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1484         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1485         only_if  => $deletion_allowed,
 
1492   my ($order, $pdf_ref, $params) = @_;
 
1496   my $print_form = Form->new('');
 
1497   $print_form->{type}        = $order->type;
 
1498   $print_form->{formname}    = $params->{formname} || $order->type;
 
1499   $print_form->{format}      = $params->{format}   || 'pdf';
 
1500   $print_form->{media}       = $params->{media}    || 'file';
 
1501   $print_form->{groupitems}  = $params->{groupitems};
 
1502   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
1504   $order->language($params->{language});
 
1505   $order->flatten_to_form($print_form, format_amounts => 1);
 
1507   # search for the template
 
1508   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
1509     name        => $print_form->{formname},
 
1510     email       => $print_form->{media} eq 'email',
 
1511     language    => $params->{language},
 
1512     printer_id  => $print_form->{printer_id},  # todo
 
1515   if (!defined $template_file) {
 
1516     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);
 
1519   return @errors if scalar @errors;
 
1521   $print_form->throw_on_error(sub {
 
1523       $print_form->prepare_for_printing;
 
1525       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
1526         template  => $template_file,
 
1527         variables => $print_form,
 
1528         variable_content_types => {
 
1529           longdescription => 'html',
 
1530           partnotes       => 'html',
 
1535     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
 
1541 sub _get_files_for_email_dialog {
 
1544   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
1546   return %files if !$::instance_conf->get_doc_storage;
 
1548   if ($self->order->id) {
 
1549     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
1550     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
1551     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
1555     uniq_by { $_->{id} }
 
1557       +{ id         => $_->part->id,
 
1558          partnumber => $_->part->partnumber }
 
1559     } @{$self->order->items_sorted};
 
1561   foreach my $part (@parts) {
 
1562     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
1563     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
1566   foreach my $key (keys %files) {
 
1567     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
1573 sub _make_periodic_invoices_config_from_yaml {
 
1574   my ($yaml_config) = @_;
 
1576   return if !$yaml_config;
 
1577   my $attr = YAML::Load($yaml_config);
 
1578   return if 'HASH' ne ref $attr;
 
1579   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
1583 sub _get_periodic_invoices_status {
 
1584   my ($self, $config) = @_;
 
1586   return                      if $self->type ne _sales_order_type();
 
1587   return t8('not configured') if !$config;
 
1589   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
1590              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
1591              :                                                     die "Cannot get status of periodic invoices config";
 
1593   return $active ? t8('active') : t8('inactive');
 
1596 sub _get_title_for {
 
1597   my ($self, $action) = @_;
 
1599   return '' if none { lc($action)} qw(add edit);
 
1602   # $::locale->text("Add Sales Order");
 
1603   # $::locale->text("Add Purchase Order");
 
1604   # $::locale->text("Add Quotation");
 
1605   # $::locale->text("Add Request for Quotation");
 
1606   # $::locale->text("Edit Sales Order");
 
1607   # $::locale->text("Edit Purchase Order");
 
1608   # $::locale->text("Edit Quotation");
 
1609   # $::locale->text("Edit Request for Quotation");
 
1611   $action = ucfirst(lc($action));
 
1612   return $self->type eq _sales_order_type()       ? $::locale->text("$action Sales Order")
 
1613        : $self->type eq _purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
1614        : $self->type eq _sales_quotation_type()   ? $::locale->text("$action Quotation")
 
1615        : $self->type eq _request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
1619 sub _sales_order_type {
 
1623 sub _purchase_order_type {
 
1627 sub _sales_quotation_type {
 
1631 sub _request_quotation_type {
 
1632   'request_quotation';
 
1643 SL::Controller::Order - controller for orders
 
1647 This is a new form to enter orders, completely rewritten with the use
 
1648 of controller and java script techniques.
 
1650 The aim is to provide the user a better expirience and a faster flow
 
1651 of work. Also the code should be more readable, more reliable and
 
1660 One input row, so that input happens every time at the same place.
 
1664 Use of pickers where possible.
 
1668 Possibility to enter more than one item at once.
 
1672 Save order only on "save" (and "save and delivery order"-workflow). No
 
1673 hidden save on "print" or "email".
 
1677 Item list in a scrollable area, so that the workflow buttons stay at
 
1682 Reordering item rows with drag and drop is possible. Sorting item rows is
 
1683 possible (by partnumber, description, qty, sellprice and discount for now).
 
1687 No C<update> is necessary. All entries and calculations are managed
 
1688 with ajax-calls and the page does only reload on C<save>.
 
1692 User can see changes immediately, because of the use of java script
 
1703 =item * C<SL/Controller/Order.pm>
 
1707 =item * C<template/webpages/order/form.html>
 
1711 =item * C<template/webpages/order/tabs/basic_data.html>
 
1713 Main tab for basic_data.
 
1715 This is the only tab here for now. "linked records" and "webdav" tabs are
 
1716 reused from generic code.
 
1720 =item * C<template/webpages/order/tabs/_business_info_row.html>
 
1722 For displaying information on business type
 
1724 =item * C<template/webpages/order/tabs/_item_input.html>
 
1726 The input line for items
 
1728 =item * C<template/webpages/order/tabs/_row.html>
 
1730 One row for already entered items
 
1732 =item * C<template/webpages/order/tabs/_tax_row.html>
 
1734 Displaying tax information
 
1736 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
 
1738 Dialog for entering more than one item at once
 
1740 =item * C<template/webpages/order/tabs/_multi_items_result.html>
 
1742 Results for the filter in the multi items dialog
 
1744 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
1746 Dialog for selecting price and discount sources
 
1750 =item * C<js/kivi.Order.js>
 
1752 java script functions
 
1764 =item * customer/vendor details ('D'-button)
 
1766 =item * credit limit
 
1768 =item * more workflows (save as new, quotation, purchase order)
 
1770 =item * price sources: little symbols showing better price / better discount
 
1772 =item * select units in input row?
 
1774 =item * custom shipto address
 
1776 =item * language / part translations
 
1778 =item * access rights
 
1780 =item * display weights
 
1786 =item * optional client/user behaviour
 
1788 (transactions has to be set - department has to be set -
 
1789  force project if enabled in client config - transport cost reminder)
 
1793 =head1 KNOWN BUGS AND CAVEATS
 
1799 Customer discount is not displayed as a valid discount in price source popup
 
1800 (this might be a bug in price sources)
 
1802 (I cannot reproduce this (Bernd))
 
1806 No indication that <shift>-up/down expands/collapses second row.
 
1810 Inline creation of parts is not currently supported
 
1814 Table header is not sticky in the scrolling area.
 
1818 Sorting does not include C<position>, neither does reordering.
 
1820 This behavior was implemented intentionally. But we can discuss, which behavior
 
1821 should be implemented.
 
1825 C<show_multi_items_dialog> does not use the currently inserted string for
 
1830 The language selected in print or email dialog is not saved when the order is saved.
 
1834 =head1 To discuss / Nice to have
 
1840 How to expand/collapse second row. Now it can be done clicking the icon or
 
1845 Possibility to change longdescription in input row?
 
1849 Possibility to select PriceSources in input row?
 
1853 This controller uses a (changed) copy of the template for the PriceSource
 
1854 dialog. Maybe there could be used one code source.
 
1858 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
1859 form. This is not only a problem here, but also in all parts using the PTC.
 
1860 There exists a ticket and a patch. This patch should be testet.
 
1864 An indicator, if the actual inputs are saved (like in an
 
1865 editor or on text processing application).
 
1869 A warning when leaving the page without saveing unchanged inputs.
 
1873 Workflows for delivery order and invoice are in the menu "Save", because the
 
1874 order is saved before opening the new document form. Nevertheless perhaps these
 
1875 workflow buttons should be put under "Workflows".
 
1882 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>