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;
 
  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_and_delivery_order save_and_invoice print create_pdf send_email) ]);
 
  47 __PACKAGE__->run_before('_get_unalterable_data',
 
  48                         only => [ qw(save save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
 
  58   $self->order->transdate(DateTime->now_local());
 
  59   $self->order->reqdate(DateTime->today_local->next_workday) if !$self->order->reqdate;
 
  64     title => $self->_get_title_for('add'),
 
  65     %{$self->{template_args}}
 
  69 # edit an existing order
 
  78     title => $self->_get_title_for('edit'),
 
  79     %{$self->{template_args}}
 
  87   my $errors = $self->_delete();
 
  89   if (scalar @{ $errors }) {
 
  90     $self->js->flash('error', $_) foreach @{ $errors };
 
  91     return $self->js->render();
 
  94   my $text = $self->type eq _sales_order_type()       ? $::locale->text('The order has been deleted')
 
  95            : $self->type eq _purchase_order_type()    ? $::locale->text('The order has been deleted')
 
  96            : $self->type eq _sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
 
  97            : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been deleted')
 
  99   flash_later('info', $text);
 
 101   my @redirect_params = (
 
 106   $self->redirect_to(@redirect_params);
 
 113   my $errors = $self->_save();
 
 115   if (scalar @{ $errors }) {
 
 116     $self->js->flash('error', $_) foreach @{ $errors };
 
 117     return $self->js->render();
 
 120   my $text = $self->type eq _sales_order_type()       ? $::locale->text('The order has been saved')
 
 121            : $self->type eq _purchase_order_type()    ? $::locale->text('The order has been saved')
 
 122            : $self->type eq _sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 123            : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 125   flash_later('info', $text);
 
 127   my @redirect_params = (
 
 130     id     => $self->order->id,
 
 133   $self->redirect_to(@redirect_params);
 
 138 # This is called if "print" is pressed in the print dialog.
 
 139 # If PDF creation was requested and succeeded, the pdf is stored in a session
 
 140 # file and the filename is stored as session value with an unique key. A
 
 141 # javascript function with this key is then called. This function calls the
 
 142 # download action below (action_download_pdf), which offers the file for
 
 147   my $format      = $::form->{print_options}->{format};
 
 148   my $media       = $::form->{print_options}->{media};
 
 149   my $formname    = $::form->{print_options}->{formname};
 
 150   my $copies      = $::form->{print_options}->{copies};
 
 151   my $groupitems  = $::form->{print_options}->{groupitems};
 
 154   if (none { $format eq $_ } qw(pdf)) {
 
 155     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 158   # only screen or printer by now
 
 159   if (none { $media eq $_ } qw(screen printer)) {
 
 160     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 164   $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 166   # create a form for generate_attachment_filename
 
 167   my $form = Form->new;
 
 168   $form->{ordnumber} = $self->order->ordnumber;
 
 169   $form->{type}      = $self->type;
 
 170   $form->{format}    = $format;
 
 171   $form->{formname}  = $formname;
 
 172   $form->{language}  = '_' . $language->template_code if $language;
 
 173   my $pdf_filename   = $form->generate_attachment_filename();
 
 176   my @errors = _create_pdf($self->order, \$pdf, { format     => $format,
 
 177                                                   formname   => $formname,
 
 178                                                   language   => $language,
 
 179                                                   groupitems => $groupitems });
 
 180   if (scalar @errors) {
 
 181     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 184   if ($media eq 'screen') {
 
 186     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 187     $sfile->fh->print($pdf);
 
 190     my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 191     $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
 
 194     ->run('kivi.Order.download_pdf', $pdf_filename, $key)
 
 195     ->flash('info', t8('The PDF has been created'));
 
 197   } elsif ($media eq 'printer') {
 
 199     my $printer_id = $::form->{print_options}->{printer_id};
 
 200     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 205     $self->js->flash('info', t8('The PDF has been printed'));
 
 208   # copy file to webdav folder
 
 209   if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
 
 210     my $webdav = SL::Webdav->new(
 
 212       number   => $self->order->ordnumber,
 
 214     my $webdav_file = SL::Webdav::File->new(
 
 216       filename => $pdf_filename,
 
 219       $webdav_file->store(data => \$pdf);
 
 222       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
 
 225   if ($self->order->ordnumber && $::instance_conf->get_doc_storage) {
 
 227       SL::File->save(object_id     => $self->order->id,
 
 228                      object_type   => $self->type,
 
 229                      mime_type     => 'application/pdf',
 
 231                      file_type     => 'document',
 
 232                      file_name     => $pdf_filename,
 
 233                      file_contents => $pdf);
 
 236       $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
 
 242 # offer pdf for download
 
 244 # It needs to get the key for the session value to get the pdf file.
 
 245 sub action_download_pdf {
 
 248   my $key = $::form->{key};
 
 249   my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
 
 250   return $self->send_file(
 
 252     type => 'application/pdf',
 
 253     name => $::form->{pdf_filename},
 
 257 # open the email dialog
 
 258 sub action_show_email_dialog {
 
 261   my $cv_method = $self->cv;
 
 263   if (!$self->order->$cv_method) {
 
 264     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'))
 
 269   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 270   $email_form->{to} ||= $self->order->$cv_method->email;
 
 271   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 272   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 273   # Todo: get addresses from shipto, if any
 
 275   my $form = Form->new;
 
 276   $form->{ordnumber} = $self->order->ordnumber;
 
 277   $form->{formname}  = $self->type;
 
 278   $form->{type}      = $self->type;
 
 279   $form->{language} = 'de';
 
 280   $form->{format}   = 'pdf';
 
 282   $email_form->{subject}             = $form->generate_email_subject();
 
 283   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 284   $email_form->{message}             = $form->generate_email_body();
 
 285   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
 287   my %files = $self->_get_files_for_email_dialog();
 
 288   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 289                                   email_form  => $email_form,
 
 290                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
 
 292                                   is_customer => $self->cv eq 'customer',
 
 296       ->run('kivi.Order.show_email_dialog', $dialog_html)
 
 303 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 304 sub action_send_email {
 
 307   my $email_form  = delete $::form->{email_form};
 
 308   my %field_names = (to => 'email');
 
 310   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 312   # for Form::cleanup which may be called in Form::send_email
 
 313   $::form->{cwd}    = getcwd();
 
 314   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 316   $::form->{media}  = 'email';
 
 318   if (($::form->{attachment_policy} // '') eq 'normal') {
 
 320     $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
 
 323     my @errors = _create_pdf($self->order, \$pdf, {media      => $::form->{media},
 
 324                                                    format     => $::form->{print_options}->{format},
 
 325                                                    formname   => $::form->{print_options}->{formname},
 
 326                                                    language   => $language,
 
 327                                                    groupitems => $::form->{print_options}->{groupitems}});
 
 328     if (scalar @errors) {
 
 329       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
 
 332     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 333     $sfile->fh->print($pdf);
 
 336     $::form->{tmpfile} = $sfile->file_name;
 
 337     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 340   $::form->send_email(\%::myconfig, 'pdf');
 
 343   my $intnotes = $self->order->intnotes;
 
 344   $intnotes   .= "\n\n" if $self->order->intnotes;
 
 345   $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 346   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 347   $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 348   $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 349   $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 350   $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 351   $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
 354       ->val('#order_intnotes', $intnotes)
 
 355       ->run('kivi.Order.close_email_dialog')
 
 356       ->flash('info', t8('The email has been sent.'))
 
 360 # open the periodic invoices config dialog
 
 362 # If there are values in the form (i.e. dialog was opened before),
 
 363 # then use this values. Create new ones, else.
 
 364 sub action_show_periodic_invoices_config_dialog {
 
 367   my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 368   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 369   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
 
 370                                                    order_value_periodicity => 'p', # = same as periodicity
 
 371                                                    start_date_as_date      => $::form->{transdate} || $::form->current_date,
 
 372                                                    extend_automatically_by => 12,
 
 374                                                    email_subject           => GenericTranslations->get(
 
 375                                                                                 language_id      => $::form->{language_id},
 
 376                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
 
 377                                                    email_body              => GenericTranslations->get(
 
 378                                                                                 language_id      => $::form->{language_id},
 
 379                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
 
 381   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
 
 382   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
 384   $::form->get_lists(printers => "ALL_PRINTERS",
 
 385                      charts   => { key       => 'ALL_CHARTS',
 
 386                                    transdate => 'current_date' });
 
 388   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
 
 390   if ($::form->{customer_id}) {
 
 391     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
 
 394   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
 
 396                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
 
 397                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
 
 402 # assign the values of the periodic invoices config dialog
 
 403 # as yaml in the hidden tag and set the status.
 
 404 sub action_assign_periodic_invoices_config {
 
 407   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
 
 409   my $config = { active                  => $::form->{active}     ? 1 : 0,
 
 410                  terminated              => $::form->{terminated} ? 1 : 0,
 
 411                  direct_debit            => $::form->{direct_debit} ? 1 : 0,
 
 412                  periodicity             => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
 
 413                  order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
 
 414                  start_date_as_date      => $::form->{start_date_as_date},
 
 415                  end_date_as_date        => $::form->{end_date_as_date},
 
 416                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
 
 417                  print                   => $::form->{print} ? 1 : 0,
 
 418                  printer_id              => $::form->{print} ? $::form->{printer_id} * 1 : undef,
 
 419                  copies                  => $::form->{copies} * 1 ? $::form->{copies} : 1,
 
 420                  extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
 
 421                  ar_chart_id             => $::form->{ar_chart_id} * 1,
 
 422                  send_email                 => $::form->{send_email} ? 1 : 0,
 
 423                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
 
 424                  email_recipient_address    => $::form->{email_recipient_address},
 
 425                  email_sender               => $::form->{email_sender},
 
 426                  email_subject              => $::form->{email_subject},
 
 427                  email_body                 => $::form->{email_body},
 
 430   my $periodic_invoices_config = YAML::Dump($config);
 
 432   my $status = $self->_get_periodic_invoices_status($config);
 
 435     ->remove('#order_periodic_invoices_config')
 
 436     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
 
 437     ->run('kivi.Order.close_periodic_invoices_config_dialog')
 
 438     ->html('#periodic_invoices_status', $status)
 
 439     ->flash('info', t8('The periodic invoices config has been assigned.'))
 
 443 sub action_get_has_active_periodic_invoices {
 
 446   my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
 
 447   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
 
 449   my $has_active_periodic_invoices =
 
 450        $self->type eq _sales_order_type()
 
 453     && (!$config->end_date || ($config->end_date > DateTime->today_local))
 
 454     && $config->get_previous_billed_period_start_date;
 
 456   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
 
 459 # save the order and redirect to the frontend subroutine for a new
 
 461 sub action_save_and_delivery_order {
 
 464   my $errors = $self->_save();
 
 466   if (scalar @{ $errors }) {
 
 467     $self->js->flash('error', $_) foreach @{ $errors };
 
 468     return $self->js->render();
 
 471   my $text = $self->type eq _sales_order_type()       ? $::locale->text('The order has been saved')
 
 472            : $self->type eq _purchase_order_type()    ? $::locale->text('The order has been saved')
 
 473            : $self->type eq _sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 474            : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 476   flash_later('info', $text);
 
 478   my @redirect_params = (
 
 479     controller => 'oe.pl',
 
 480     action     => 'oe_delivery_order_from_order',
 
 481     id         => $self->order->id,
 
 484   $self->redirect_to(@redirect_params);
 
 487 # save the order and redirect to the frontend subroutine for a new
 
 489 sub action_save_and_invoice {
 
 492   my $errors = $self->_save();
 
 494   if (scalar @{ $errors }) {
 
 495     $self->js->flash('error', $_) foreach @{ $errors };
 
 496     return $self->js->render();
 
 499   my $text = $self->type eq _sales_order_type()       ? $::locale->text('The order has been saved')
 
 500            : $self->type eq _purchase_order_type()    ? $::locale->text('The order has been saved')
 
 501            : $self->type eq _sales_quotation_type()   ? $::locale->text('The quotation has been saved')
 
 502            : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
 
 504   flash_later('info', $text);
 
 506   my @redirect_params = (
 
 507     controller => 'oe.pl',
 
 508     action     => 'oe_invoice_from_order',
 
 509     id         => $self->order->id,
 
 512   $self->redirect_to(@redirect_params);
 
 515 # workflow from sales quotation to sales order
 
 516 sub action_sales_order {
 
 517   $_[0]->_workflow_sales_or_purchase_order();
 
 520 # workflow from rfq to purchase order
 
 521 sub action_purchase_order {
 
 522   $_[0]->_workflow_sales_or_purchase_order();
 
 525 # set form elements in respect to a changed customer or vendor
 
 527 # This action is called on an change of the customer/vendor picker.
 
 528 sub action_customer_vendor_changed {
 
 531   my $cv_method = $self->cv;
 
 533   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 534     $self->js->show('#cp_row');
 
 536     $self->js->hide('#cp_row');
 
 539   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 540     $self->js->show('#shipto_row');
 
 542     $self->js->hide('#shipto_row');
 
 545   $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
 
 547   if ($self->order->is_sales) {
 
 548     $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
 
 549                               ? $self->order->$cv_method->taxincluded_checked
 
 550                               : $::myconfig{taxincluded_checked});
 
 551     $self->js->val('#order_salesman_id', $self->order->$cv_method->salesman_id);
 
 554   $self->order->payment_id($self->order->$cv_method->payment_id);
 
 555   $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
 
 560     ->replaceWith('#order_cp_id',            $self->build_contact_select)
 
 561     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
 
 562     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
 
 563     ->val(        '#order_taxincluded',      $self->order->taxincluded)
 
 564     ->val(        '#order_payment_id',       $self->order->payment_id)
 
 565     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
 
 566     ->val(        '#order_intnotes',         $self->order->$cv_method->notes)
 
 567     ->focus(      '#order_' . $self->cv . '_id');
 
 569   $self->_js_redisplay_amounts_and_taxes;
 
 573 # called if a unit in an existing item row is changed
 
 574 sub action_unit_changed {
 
 577   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 578   my $item = $self->order->items_sorted->[$idx];
 
 580   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 581   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 586     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 587   $self->_js_redisplay_line_values;
 
 588   $self->_js_redisplay_amounts_and_taxes;
 
 592 # add an item row for a new item entered in the input row
 
 593 sub action_add_item {
 
 596   my $form_attr = $::form->{add_item};
 
 598   return unless $form_attr->{parts_id};
 
 600   my $item = _new_item($self->order, $form_attr);
 
 602   $self->order->add_items($item);
 
 606   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 607   my $row_as_html = $self->p->render('order/tabs/_row',
 
 611                                      ALL_PRICE_FACTORS => $self->all_price_factors
 
 615     ->append('#row_table_id', $row_as_html);
 
 617   if ( $item->part->is_assortment ) {
 
 618     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 619     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 620       my $attr = { parts_id => $assortment_item->parts_id,
 
 621                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 622                    unit     => $assortment_item->unit,
 
 623                    description => $assortment_item->part->description,
 
 625       my $item = _new_item($self->order, $attr);
 
 627       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 628       $item->discount(1) unless $assortment_item->charge;
 
 630       $self->order->add_items( $item );
 
 632       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 633       my $row_as_html = $self->p->render('order/tabs/_row',
 
 637                                          ALL_PRICE_FACTORS => $self->all_price_factors
 
 640         ->append('#row_table_id', $row_as_html);
 
 645     ->val('.add_item_input', '')
 
 646     ->run('kivi.Order.init_row_handlers')
 
 647     ->run('kivi.Order.row_table_scroll_down')
 
 648     ->run('kivi.Order.renumber_positions')
 
 649     ->focus('#add_item_parts_id_name');
 
 651   $self->_js_redisplay_amounts_and_taxes;
 
 655 # open the dialog for entering multiple items at once
 
 656 sub action_show_multi_items_dialog {
 
 657   require SL::DB::PartsGroup;
 
 658   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
 
 659                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
 
 662 # update the filter results in the multi item dialog
 
 663 sub action_multi_items_update_result {
 
 666   $::form->{multi_items}->{filter}->{obsolete} = 0;
 
 668   my $count = $_[0]->multi_items_models->count;
 
 671     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
 
 672     $_[0]->render($text, { layout => 0 });
 
 673   } elsif ($count > $max_count) {
 
 674     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
 
 675     $_[0]->render($text, { layout => 0 });
 
 677     my $multi_items = $_[0]->multi_items_models->get;
 
 678     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
 
 679                   multi_items => $multi_items);
 
 683 # add item rows for multiple items at once
 
 684 sub action_add_multi_items {
 
 687   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
 
 688   return $self->js->render() unless scalar @form_attr;
 
 691   foreach my $attr (@form_attr) {
 
 692     my $item = _new_item($self->order, $attr);
 
 694     if ( $item->part->is_assortment ) {
 
 695       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 696         my $attr = { parts_id => $assortment_item->parts_id,
 
 697                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 698                      unit     => $assortment_item->unit,
 
 699                      description => $assortment_item->part->description,
 
 701         my $item = _new_item($self->order, $attr);
 
 703         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 704         $item->discount(1) unless $assortment_item->charge;
 
 709   $self->order->add_items(@items);
 
 713   foreach my $item (@items) {
 
 714     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 715     my $row_as_html = $self->p->render('order/tabs/_row',
 
 719                                        ALL_PRICE_FACTORS => $self->all_price_factors
 
 722     $self->js->append('#row_table_id', $row_as_html);
 
 726     ->run('kivi.Order.close_multi_items_dialog')
 
 727     ->run('kivi.Order.init_row_handlers')
 
 728     ->run('kivi.Order.row_table_scroll_down')
 
 729     ->run('kivi.Order.renumber_positions')
 
 730     ->focus('#add_item_parts_id_name');
 
 732   $self->_js_redisplay_amounts_and_taxes;
 
 736 # recalculate all linetotals, amounts and taxes and redisplay them
 
 737 sub action_recalc_amounts_and_taxes {
 
 742   $self->_js_redisplay_line_values;
 
 743   $self->_js_redisplay_amounts_and_taxes;
 
 747 # redisplay item rows if they are sorted by an attribute
 
 748 sub action_reorder_items {
 
 752     partnumber  => sub { $_[0]->part->partnumber },
 
 753     description => sub { $_[0]->description },
 
 754     qty         => sub { $_[0]->qty },
 
 755     sellprice   => sub { $_[0]->sellprice },
 
 756     discount    => sub { $_[0]->discount },
 
 759   my $method = $sort_keys{$::form->{order_by}};
 
 760   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
 761   if ($::form->{sort_dir}) {
 
 762     @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 764     @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 767     ->run('kivi.Order.redisplay_items', \@to_sort)
 
 771 # show the popup to choose a price/discount source
 
 772 sub action_price_popup {
 
 775   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 776   my $item = $self->order->items_sorted->[$idx];
 
 778   $self->render_price_dialog($item);
 
 781 # get the longdescription for an item if the dialog to enter/change the
 
 782 # longdescription was opened and the longdescription is empty
 
 784 # If this item is new, get the longdescription from Part.
 
 785 # Otherwise get it from OrderItem.
 
 786 sub action_get_item_longdescription {
 
 789   if ($::form->{item_id}) {
 
 790     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
 
 791   } elsif ($::form->{parts_id}) {
 
 792     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
 
 794   $_[0]->render(\ $longdescription, { type => 'text' });
 
 797 # load the second row for one or more items
 
 799 # This action gets the html code for all items second rows by rendering a template for
 
 800 # the second row and sets the html code via client js.
 
 801 sub action_load_second_rows {
 
 804   $self->_recalc() if $self->order->is_sales; # for margin calculation
 
 806   foreach my $item_id (@{ $::form->{item_ids} }) {
 
 807     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
 808     my $item = $self->order->items_sorted->[$idx];
 
 810     $self->_js_load_second_row($item, $item_id, 0);
 
 813   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
 818 sub _js_load_second_row {
 
 819   my ($self, $item, $item_id, $do_parse) = @_;
 
 822     # Parse values from form (they are formated while rendering (template)).
 
 823     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
 824     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
 825     foreach my $var (@{ $item->cvars_by_config }) {
 
 826       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
 828     $item->parse_custom_variable_values;
 
 831   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
 834     ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
 
 835     ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
 
 838 sub _js_redisplay_line_values {
 
 841   my $is_sales = $self->order->is_sales;
 
 843   # sales orders with margins
 
 848        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
 849        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
 850        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
 851       ]} @{ $self->order->items_sorted };
 
 855        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
 856       ]} @{ $self->order->items_sorted };
 
 860     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
 
 863 sub _js_redisplay_amounts_and_taxes {
 
 866   if (scalar @{ $self->{taxes} }) {
 
 867     $self->js->show('#taxincluded_row_id');
 
 869     $self->js->hide('#taxincluded_row_id');
 
 872   if ($self->order->taxincluded) {
 
 873     $self->js->hide('#subtotal_row_id');
 
 875     $self->js->show('#subtotal_row_id');
 
 879     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
 
 880     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
 
 882     ->insertBefore($self->build_tax_rows, '#amount_row_id');
 
 889 sub init_valid_types {
 
 890   [ _sales_order_type(), _purchase_order_type(), _sales_quotation_type(), _request_quotation_type() ];
 
 896   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
 897     die "Not a valid type for order";
 
 900   $self->type($::form->{type});
 
 906   my $cv = (any { $self->type eq $_ } (_sales_order_type(),    _sales_quotation_type()))   ? 'customer'
 
 907          : (any { $self->type eq $_ } (_purchase_order_type(), _request_quotation_type())) ? 'vendor'
 
 908          : die "Not a valid type for order";
 
 921 # model used to filter/display the parts in the multi-items dialog
 
 922 sub init_multi_items_models {
 
 923   SL::Controller::Helper::GetModels->new(
 
 926     with_objects   => [ qw(unit_obj) ],
 
 927     disable_plugin => 'paginated',
 
 928     source         => $::form->{multi_items},
 
 934       partnumber  => t8('Partnumber'),
 
 935       description => t8('Description')}
 
 939 sub init_all_price_factors {
 
 940   SL::DB::Manager::PriceFactor->get_all;
 
 946   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
 
 948   my $right   = $right_for->{ $self->type };
 
 949   $right    ||= 'DOES_NOT_EXIST';
 
 951   $::auth->assert($right);
 
 954 # build the selection box for contacts
 
 956 # Needed, if customer/vendor changed.
 
 957 sub build_contact_select {
 
 960   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
 961     value_key  => 'cp_id',
 
 962     title_key  => 'full_name_dep',
 
 963     default    => $self->order->cp_id,
 
 965     style      => 'width: 300px',
 
 969 # build the selection box for shiptos
 
 971 # Needed, if customer/vendor changed.
 
 972 sub build_shipto_select {
 
 975   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
 
 976     value_key  => 'shipto_id',
 
 977     title_key  => 'displayable_id',
 
 978     default    => $self->order->shipto_id,
 
 980     style      => 'width: 300px',
 
 984 # build the rows for displaying taxes
 
 986 # Called if amounts where recalculated and redisplayed.
 
 991   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
 
 992     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
 
 994   return $rows_as_html;
 
 998 sub render_price_dialog {
 
 999   my ($self, $record_item) = @_;
 
1001   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
 
1005       'kivi.io.price_chooser_dialog',
 
1006       t8('Available Prices'),
 
1007       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
 
1012 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
 
1013 #     $self->js->show('#dialog_flash_error');
 
1022   return if !$::form->{id};
 
1024   $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
 
1027 # load or create a new order object
 
1029 # And assign changes from the form to this object.
 
1030 # If the order is loaded from db, check if items are deleted in the form,
 
1031 # remove them form the object and collect them for removing from db on saving.
 
1032 # Then create/update items from form (via _make_item) and add them.
 
1036   # add_items adds items to an order with no items for saving, but they cannot
 
1037   # be retrieved via items until the order is saved. Adding empty items to new
 
1038   # order here solves this problem.
 
1040   $order   = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
 
1041   $order ||= SL::DB::Order->new(orderitems => [],
 
1042                                 quotation  => (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type())));
 
1044   my $form_orderitems               = delete $::form->{order}->{orderitems};
 
1045   my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
 
1047   $order->assign_attributes(%{$::form->{order}});
 
1049   my $periodic_invoices_config = _make_periodic_invoices_config_from_yaml($form_periodic_invoices_config);
 
1050   $order->periodic_invoices_config($periodic_invoices_config) if $periodic_invoices_config;
 
1052   # remove deleted items
 
1053   $self->item_ids_to_delete([]);
 
1054   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1055     my $item = $order->orderitems->[$idx];
 
1056     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1057       splice @{$order->orderitems}, $idx, 1;
 
1058       push @{$self->item_ids_to_delete}, $item->id;
 
1064   foreach my $form_attr (@{$form_orderitems}) {
 
1065     my $item = _make_item($order, $form_attr);
 
1066     $item->position($pos);
 
1070   $order->add_items(grep {!$_->id} @items);
 
1075 # create or update items from form
 
1077 # Make item objects from form values. For items already existing read from db.
 
1078 # Create a new item else. And assign attributes.
 
1080   my ($record, $attr) = @_;
 
1083   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1085   my $is_new = !$item;
 
1087   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1088   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1089   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1090   $item ||= SL::DB::OrderItem->new(custom_variables => []);
 
1092   $item->assign_attributes(%$attr);
 
1093   $item->longdescription($item->part->notes)                     if $is_new && !defined $attr->{longdescription};
 
1094   $item->project_id($record->globalproject_id)                   if $is_new && !defined $attr->{project_id};
 
1095   $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
 
1102 # This is used to add one item
 
1104   my ($record, $attr) = @_;
 
1106   my $item = SL::DB::OrderItem->new;
 
1107   $item->assign_attributes(%$attr);
 
1109   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1110   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1112   $item->unit($part->unit) if !$item->unit;
 
1115   if ( $part->is_assortment ) {
 
1116     # add assortment items with price 0, as the components carry the price
 
1117     $price_src = $price_source->price_from_source("");
 
1118     $price_src->price(0);
 
1119   } elsif ($item->sellprice) {
 
1120     $price_src = $price_source->price_from_source("");
 
1121     $price_src->price($item->sellprice);
 
1123     $price_src = $price_source->best_price
 
1124            ? $price_source->best_price
 
1125            : $price_source->price_from_source("");
 
1126     $price_src->price(0) if !$price_source->best_price;
 
1130   if ($item->discount) {
 
1131     $discount_src = $price_source->discount_from_source("");
 
1132     $discount_src->discount($item->discount);
 
1134     $discount_src = $price_source->best_discount
 
1135                   ? $price_source->best_discount
 
1136                   : $price_source->discount_from_source("");
 
1137     $discount_src->discount(0) if !$price_source->best_discount;
 
1141   $new_attr{part}                   = $part;
 
1142   $new_attr{description}            = $part->description     if ! $item->description;
 
1143   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1144   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1145   $new_attr{sellprice}              = $price_src->price;
 
1146   $new_attr{discount}               = $discount_src->discount;
 
1147   $new_attr{active_price_source}    = $price_src;
 
1148   $new_attr{active_discount_source} = $discount_src;
 
1149   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1150   $new_attr{project_id}             = $record->globalproject_id;
 
1151   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1153   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1154   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1155   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1156   $new_attr{custom_variables} = [];
 
1158   $item->assign_attributes(%new_attr);
 
1163 # recalculate prices and taxes
 
1165 # Using the PriceTaxCalculator. Store linetotals in the item objects.
 
1169   # bb: todo: currency later
 
1170   $self->order->currency_id($::instance_conf->get_currency_id());
 
1172   my %pat = $self->order->calculate_prices_and_taxes();
 
1173   $self->{taxes} = [];
 
1174   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
 
1175     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
 
1177     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
 
1178     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
 
1179                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
 
1183   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
 
1186 # get data for saving, printing, ..., that is not changed in the form
 
1188 # Only cvars for now.
 
1189 sub _get_unalterable_data {
 
1192   foreach my $item (@{ $self->order->items }) {
 
1193     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1194     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1195     foreach my $var (@{ $item->cvars_by_config }) {
 
1196       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1198     $item->parse_custom_variable_values;
 
1204 # And remove related files in the spool directory
 
1209   my $db     = $self->order->db;
 
1211   $db->with_transaction(
 
1213       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1214       $self->order->delete;
 
1215       my $spool = $::lx_office_conf{paths}->{spool};
 
1216       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1219   }) || push(@{$errors}, $db->error);
 
1226 # And delete items that are deleted in the form.
 
1231   my $db     = $self->order->db;
 
1233   $db->with_transaction(sub {
 
1234     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
 
1235     $self->order->save(cascade => 1);
 
1238     if ($::form->{converted_from_oe_id}) {
 
1239       SL::DB::Order->new(id => $::form->{converted_from_oe_id})->load->link_to_record($self->order);
 
1241       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1243         foreach (@{ $self->order->items_sorted }) {
 
1244           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1246           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1247                                   from_id    => $from_id,
 
1248                                   to_table   => 'orderitems',
 
1256   }) || push(@{$errors}, $db->error);
 
1261 sub _workflow_sales_or_purchase_order {
 
1264   my $destination_type = $::form->{type} eq _sales_quotation_type()   ? _sales_order_type()
 
1265                        : $::form->{type} eq _request_quotation_type() ? _purchase_order_type()
 
1268   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
 
1269   $self->{converted_from_oe_id} = delete $::form->{id};
 
1272   $::form->{type} = $destination_type;
 
1277   $self->_get_unalterable_data();
 
1278   $self->_pre_render();
 
1280   # trigger rendering values for second row/longdescription as hidden,
 
1281   # because they are loaded only on demand. So we need to keep the values
 
1283   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
1284   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
1288     title => $self->_get_title_for('edit'),
 
1289     %{$self->{template_args}}
 
1297   $self->{all_taxzones}             = SL::DB::Manager::TaxZone->get_all_sorted();
 
1298   $self->{all_departments}          = SL::DB::Manager::Department->get_all_sorted();
 
1299   $self->{all_employees}            = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1302   $self->{all_salesmen}             = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1305   $self->{all_projects}             = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
 
1307                                                                         sort_by => 'projectnumber');
 
1308   $self->{all_payment_terms}        = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1310   $self->{all_delivery_terms}       = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1311   $self->{current_employee_id}      = SL::DB::Manager::Employee->current->id;
 
1312   $self->{periodic_invoices_status} = $self->_get_periodic_invoices_status($self->order->periodic_invoices_config);
 
1313   $self->{order_probabilities}      = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1315   my $print_form = Form->new('');
 
1316   $print_form->{type}      = $self->type;
 
1317   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
 
1318   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
 
1319   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
 
1320     form => $print_form,
 
1321     options => {dialog_name_prefix => 'print_options.',
 
1325                 no_opendocument    => 1,
 
1329   foreach my $item (@{$self->order->orderitems}) {
 
1330     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1331     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1332     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1335   if ($self->order->ordnumber && $::instance_conf->get_webdav) {
 
1336     my $webdav = SL::Webdav->new(
 
1337       type     => $self->type,
 
1338       number   => $self->order->ordnumber,
 
1340     my @all_objects = $webdav->get_all_objects;
 
1341     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1343                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1347   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config);
 
1348   $self->_setup_edit_action_bar;
 
1351 sub _setup_edit_action_bar {
 
1352   my ($self, %params) = @_;
 
1354   my $deletion_allowed = (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type()))
 
1355                       || (($self->type eq _sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
 
1356                       || (($self->type eq _purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
 
1358   for my $bar ($::request->layout->get('actionbar')) {
 
1363           call      => [ 'kivi.Order.save', $::instance_conf->get_order_warn_duplicate_parts ],
 
1364           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1367           t8('Save and Delivery Order'),
 
1368           call      => [ 'kivi.Order.save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
 
1369           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1370           only_if   => (any { $self->type eq $_ } (_sales_order_type(), _purchase_order_type()))
 
1373           t8('Save and Invoice'),
 
1374           call      => [ 'kivi.Order.save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1375           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
 
1377       ], # end of combobox "Save"
 
1385           submit  => [ '#order_form', { action => "Order/sales_order" } ],
 
1386           only_if => (any { $self->type eq $_ } (_sales_quotation_type())),
 
1389           t8('Purchase Order'),
 
1390           submit  => [ '#order_form', { action => "Order/purchase_order" } ],
 
1391           only_if   => (any { $self->type eq $_ } (_request_quotation_type())),
 
1393       ], # end of combobox "Workflow"
 
1401           call => [ 'kivi.Order.show_print_options' ],
 
1405           call => [ 'kivi.Order.email' ],
 
1408           t8('Download attachments of all parts'),
 
1409           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1410           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1411           only_if  => $::instance_conf->get_doc_storage,
 
1413       ], # end of combobox "Export"
 
1417         call     => [ 'kivi.Order.delete_order' ],
 
1418         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1419         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1420         only_if  => $deletion_allowed,
 
1427   my ($order, $pdf_ref, $params) = @_;
 
1431   my $print_form = Form->new('');
 
1432   $print_form->{type}        = $order->type;
 
1433   $print_form->{formname}    = $params->{formname} || $order->type;
 
1434   $print_form->{format}      = $params->{format}   || 'pdf';
 
1435   $print_form->{media}       = $params->{media}    || 'file';
 
1436   $print_form->{groupitems}  = $params->{groupitems};
 
1437   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
1439   $order->language($params->{language});
 
1440   $order->flatten_to_form($print_form, format_amounts => 1);
 
1442   # search for the template
 
1443   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
1444     name        => $print_form->{formname},
 
1445     email       => $print_form->{media} eq 'email',
 
1446     language    => $params->{language},
 
1447     printer_id  => $print_form->{printer_id},  # todo
 
1450   if (!defined $template_file) {
 
1451     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);
 
1454   return @errors if scalar @errors;
 
1456   $print_form->throw_on_error(sub {
 
1458       $print_form->prepare_for_printing;
 
1460       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
1461         template  => $template_file,
 
1462         variables => $print_form,
 
1463         variable_content_types => {
 
1464           longdescription => 'html',
 
1465           partnotes       => 'html',
 
1470     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
 
1476 sub _get_files_for_email_dialog {
 
1479   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
1481   return %files if !$::instance_conf->get_doc_storage;
 
1483   if ($self->order->id) {
 
1484     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
1485     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
1486     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
1490     uniq_by { $_->{id} }
 
1492       +{ id         => $_->part->id,
 
1493          partnumber => $_->part->partnumber }
 
1494     } @{$self->order->items_sorted};
 
1496   foreach my $part (@parts) {
 
1497     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
1498     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
1501   foreach my $key (keys %files) {
 
1502     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
1508 sub _make_periodic_invoices_config_from_yaml {
 
1509   my ($yaml_config) = @_;
 
1511   return if !$yaml_config;
 
1512   my $attr = YAML::Load($yaml_config);
 
1513   return if 'HASH' ne ref $attr;
 
1514   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
 
1518 sub _get_periodic_invoices_status {
 
1519   my ($self, $config) = @_;
 
1521   return                      if $self->type ne _sales_order_type();
 
1522   return t8('not configured') if !$config;
 
1524   my $active = ('HASH' eq ref $config)                           ? $config->{active}
 
1525              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
 
1526              :                                                     die "Cannot get status of periodic invoices config";
 
1528   return $active ? t8('active') : t8('inactive');
 
1531 sub _get_title_for {
 
1532   my ($self, $action) = @_;
 
1534   return '' if none { lc($action)} qw(add edit);
 
1537   # $::locale->text("Add Sales Order");
 
1538   # $::locale->text("Add Purchase Order");
 
1539   # $::locale->text("Add Quotation");
 
1540   # $::locale->text("Add Request for Quotation");
 
1541   # $::locale->text("Edit Sales Order");
 
1542   # $::locale->text("Edit Purchase Order");
 
1543   # $::locale->text("Edit Quotation");
 
1544   # $::locale->text("Edit Request for Quotation");
 
1546   $action = ucfirst(lc($action));
 
1547   return $self->type eq _sales_order_type()       ? $::locale->text("$action Sales Order")
 
1548        : $self->type eq _purchase_order_type()    ? $::locale->text("$action Purchase Order")
 
1549        : $self->type eq _sales_quotation_type()   ? $::locale->text("$action Quotation")
 
1550        : $self->type eq _request_quotation_type() ? $::locale->text("$action Request for Quotation")
 
1554 sub _sales_order_type {
 
1558 sub _purchase_order_type {
 
1562 sub _sales_quotation_type {
 
1566 sub _request_quotation_type {
 
1567   'request_quotation';
 
1578 SL::Controller::Order - controller for orders
 
1582 This is a new form to enter orders, completely rewritten with the use
 
1583 of controller and java script techniques.
 
1585 The aim is to provide the user a better expirience and a faster flow
 
1586 of work. Also the code should be more readable, more reliable and
 
1595 One input row, so that input happens every time at the same place.
 
1599 Use of pickers where possible.
 
1603 Possibility to enter more than one item at once.
 
1607 Save order only on "save" (and "save and delivery order"-workflow). No
 
1608 hidden save on "print" or "email".
 
1612 Item list in a scrollable area, so that the workflow buttons stay at
 
1617 Reordering item rows with drag and drop is possible. Sorting item rows is
 
1618 possible (by partnumber, description, qty, sellprice and discount for now).
 
1622 No C<update> is necessary. All entries and calculations are managed
 
1623 with ajax-calls and the page does only reload on C<save>.
 
1627 User can see changes immediately, because of the use of java script
 
1638 =item * C<SL/Controller/Order.pm>
 
1642 =item * C<template/webpages/order/form.html>
 
1646 =item * C<template/webpages/order/tabs/basic_data.html>
 
1648 Main tab for basic_data.
 
1650 This is the only tab here for now. "linked records" and "webdav" tabs are
 
1651 reused from generic code.
 
1655 =item * C<template/webpages/order/tabs/_item_input.html>
 
1657 The input line for items
 
1659 =item * C<template/webpages/order/tabs/_row.html>
 
1661 One row for already entered items
 
1663 =item * C<template/webpages/order/tabs/_tax_row.html>
 
1665 Displaying tax information
 
1667 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
 
1669 Dialog for entering more than one item at once
 
1671 =item * C<template/webpages/order/tabs/_multi_items_result.html>
 
1673 Results for the filter in the multi items dialog
 
1675 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
 
1677 Dialog for selecting price and discount sources
 
1681 =item * C<js/kivi.Order.js>
 
1683 java script functions
 
1695 =item * customer/vendor details ('D'-button)
 
1697 =item * credit limit
 
1699 =item * more workflows (save as new, quotation, purchase order)
 
1701 =item * price sources: little symbols showing better price / better discount
 
1703 =item * select units in input row?
 
1705 =item * custom shipto address
 
1707 =item * language / part translations
 
1709 =item * access rights
 
1711 =item * display weights
 
1717 =item * optional client/user behaviour
 
1719 (transactions has to be set - department has to be set -
 
1720  force project if enabled in client config - transport cost reminder)
 
1724 =head1 KNOWN BUGS AND CAVEATS
 
1730 Customer discount is not displayed as a valid discount in price source popup
 
1731 (this might be a bug in price sources)
 
1733 (I cannot reproduce this (Bernd))
 
1737 No indication that <shift>-up/down expands/collapses second row.
 
1741 Inline creation of parts is not currently supported
 
1745 Table header is not sticky in the scrolling area.
 
1749 Sorting does not include C<position>, neither does reordering.
 
1751 This behavior was implemented intentionally. But we can discuss, which behavior
 
1752 should be implemented.
 
1756 C<show_multi_items_dialog> does not use the currently inserted string for
 
1761 The language selected in print or email dialog is not saved when the order is saved.
 
1765 =head1 To discuss / Nice to have
 
1771 How to expand/collapse second row. Now it can be done clicking the icon or
 
1776 Possibility to change longdescription in input row?
 
1780 Possibility to select PriceSources in input row?
 
1784 This controller uses a (changed) copy of the template for the PriceSource
 
1785 dialog. Maybe there could be used one code source.
 
1789 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
1790 form. This is not only a problem here, but also in all parts using the PTC.
 
1791 There exists a ticket and a patch. This patch should be testet.
 
1795 An indicator, if the actual inputs are saved (like in an
 
1796 editor or on text processing application).
 
1800 A warning when leaving the page without saveing unchanged inputs.
 
1804 Workflows for delivery order and invoice are in the menu "Save", because the
 
1805 order is saved before opening the new document form. Nevertheless perhaps these
 
1806 workflow buttons should be put under "Workflows".
 
1813 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>