1 package SL::Controller::DeliveryOrder;
 
   4 use parent qw(SL::Controller::Base);
 
   6 use SL::Helper::Flash qw(flash_later);
 
   7 use SL::Helper::Number qw(_format_number _parse_number);
 
   8 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
 
   9 use SL::Presenter::DeliveryOrder qw(delivery_order_status_line);
 
  10 use SL::Locale::String qw(t8);
 
  11 use SL::SessionFile::Random;
 
  16 use SL::Util qw(trim);
 
  24 use SL::DB::PartClassification;
 
  25 use SL::DB::PartsGroup;
 
  28 use SL::DB::RecordLink;
 
  30 use SL::DB::Translation;
 
  31 use SL::DB::TransferType;
 
  33 use SL::Helper::CreatePDF qw(:all);
 
  34 use SL::Helper::PrintOptions;
 
  35 use SL::Helper::ShippedQty;
 
  36 use SL::Helper::UserPreferences::DisplayPreferences;
 
  37 use SL::Helper::UserPreferences::PositionsScrollbar;
 
  38 use SL::Helper::UserPreferences::UpdatePositions;
 
  40 use SL::Controller::Helper::GetModels;
 
  41 use SL::Controller::DeliveryOrder::TypeData qw(:types);
 
  43 use List::Util qw(first sum0);
 
  44 use List::UtilsBy qw(sort_by uniq_by);
 
  45 use List::MoreUtils qw(any none pairwise first_index);
 
  46 use English qw(-no_match_vars);
 
  51 use Rose::Object::MakeMethods::Generic
 
  53  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 
  54  'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids type_data) ],
 
  59 __PACKAGE__->run_before('check_auth',
 
  60                         except => [ qw(update_stock_information) ]);
 
  62 __PACKAGE__->run_before('check_auth_for_edit',
 
  63                         except => [ qw(update_stock_information edit show_customer_vendor_details_dialog price_popup stock_in_out_dialog load_second_rows) ]);
 
  65 __PACKAGE__->run_before('get_unalterable_data',
 
  66                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
 
  77   $self->order->transdate(DateTime->now_local());
 
  78   $self->type_data->set_reqdate_by_type;
 
  83     'delivery_order/form',
 
  84     title => $self->get_title_for('add'),
 
  85     %{$self->{template_args}}
 
  89 sub action_add_from_order {
 
  91   # this interfers with init_order
 
  92   $self->{converted_from_oe_id} = delete $::form->{id};
 
  94   $self->type_data->validate($::form->{type});
 
  96   my $order = SL::DB::Order->new(id => $self->{converted_from_oe_id})->load;
 
  98   $self->order(SL::DB::DeliveryOrder->new_from($order, type => $::form->{type}));
 
 103 # edit an existing order
 
 111     # this is to edit an order from an unsaved order object
 
 113     # set item ids to new fake id, to identify them as new items
 
 114     foreach my $item (@{$self->order->items_sorted}) {
 
 115       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 117     # trigger rendering values for second row as hidden, because they
 
 118     # are loaded only on demand. So we need to keep the values from
 
 120     $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
 125     'delivery_order/form',
 
 126     title => $self->get_title_for('edit'),
 
 127     %{$self->{template_args}}
 
 131 # edit a collective order (consisting of one or more existing orders)
 
 132 sub action_edit_collective {
 
 136   my @multi_ids = map {
 
 137     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 138   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 140   # fall back to add if no ids are given
 
 141   if (scalar @multi_ids == 0) {
 
 146   # fall back to save as new if only one id is given
 
 147   if (scalar @multi_ids == 1) {
 
 148     $self->order(SL::DB::DeliveryOrder->new(id => $multi_ids[0])->load);
 
 149     $self->action_save_as_new();
 
 153   # make new order from given orders
 
 154   my @multi_orders = map { SL::DB::DeliveryOrder->new(id => $_)->load } @multi_ids;
 
 155   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 156   $self->order(SL::DB::DeliveryOrder->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 158   $self->action_edit();
 
 165   my $errors = $self->delete();
 
 167   if (scalar @{ $errors }) {
 
 168     $self->js->flash('error', $_) foreach @{ $errors };
 
 169     return $self->js->render();
 
 172   flash_later('info', $self->type_data->text("delete"));
 
 174   my @redirect_params = (
 
 179   $self->redirect_to(@redirect_params);
 
 186   my $errors = $self->save();
 
 188   if (scalar @{ $errors }) {
 
 189     $self->js->flash('error', $_) foreach @{ $errors };
 
 190     return $self->js->render();
 
 193   flash_later('info', $self->type_data->text("saved"));
 
 195   my @redirect_params = (
 
 198     id     => $self->order->id,
 
 201   $self->redirect_to(@redirect_params);
 
 204 # save the order as new document an open it for edit
 
 205 sub action_save_as_new {
 
 208   my $order = $self->order;
 
 211     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 212     return $self->js->render();
 
 215   # load order from db to check if values changed
 
 216   my $saved_order = SL::DB::DeliveryOrder->new(id => $order->id)->load;
 
 219   # Lets assign a new number if the user hasn't changed the previous one.
 
 220   # If it has been changed manually then use it as-is.
 
 221   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 223                         : trim($order->number);
 
 225   # Clear transdate unless changed
 
 226   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 227                         ? DateTime->today_local
 
 230   # Set new reqdate unless changed if it is enabled in client config
 
 231   $new_attrs{reqdate} = $self->type_data->get_reqdate_by_type($order->reqdate, $saved_order->reqdate);
 
 234   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 236   # Create new record from current one
 
 237   $self->order(SL::DB::DeliveryOrder->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 239   # no linked records on save as new
 
 240   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 243   $self->action_save();
 
 248 # This is called if "print" is pressed in the print dialog.
 
 249 # If PDF creation was requested and succeeded, the pdf is offered for download
 
 250 # via send_file (which uses ajax in this case).
 
 254   my $errors = $self->save();
 
 256   if (scalar @{ $errors }) {
 
 257     $self->js->flash('error', $_) foreach @{ $errors };
 
 258     return $self->js->render();
 
 261   $self->js_reset_order_and_item_ids_after_save;
 
 263   my $format      = $::form->{print_options}->{format};
 
 264   my $media       = $::form->{print_options}->{media};
 
 265   my $formname    = $::form->{print_options}->{formname};
 
 266   my $copies      = $::form->{print_options}->{copies};
 
 267   my $groupitems  = $::form->{print_options}->{groupitems};
 
 268   my $printer_id  = $::form->{print_options}->{printer_id};
 
 270   # only pdf and opendocument by now
 
 271   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
 
 272     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 275   # only screen or printer by now
 
 276   if (none { $media eq $_ } qw(screen printer)) {
 
 277     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 280   # create a form for generate_attachment_filename
 
 281   my $form   = Form->new;
 
 282   $form->{$self->nr_key()}  = $self->order->number;
 
 283   $form->{type}             = $self->type;
 
 284   $form->{format}           = $format;
 
 285   $form->{formname}         = $formname;
 
 286   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 287   my $pdf_filename          = $form->generate_attachment_filename();
 
 290   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
 
 291                                                    formname   => $formname,
 
 292                                                    language   => $self->order->language,
 
 293                                                    printer_id => $printer_id,
 
 294                                                    groupitems => $groupitems });
 
 295   if (scalar @errors) {
 
 296     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 299   if ($media eq 'screen') {
 
 301     $self->js->flash('info', t8('The PDF has been created'));
 
 304       type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 305       name         => $pdf_filename,
 
 309   } elsif ($media eq 'printer') {
 
 311     my $printer_id = $::form->{print_options}->{printer_id};
 
 312     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 317     $self->js->flash('info', t8('The PDF has been printed'));
 
 320   my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $pdf_filename);
 
 321   if (scalar @warnings) {
 
 322     $self->js->flash('warning', $_) for @warnings;
 
 325   $self->save_history('PRINTED');
 
 328     ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
 
 331 sub action_preview_pdf {
 
 334   my $errors = $self->save();
 
 335   if (scalar @{ $errors }) {
 
 336     $self->js->flash('error', $_) foreach @{ $errors };
 
 337     return $self->js->render();
 
 340   $self->js_reset_order_and_item_ids_after_save;
 
 343   my $media       = 'screen';
 
 344   my $formname    = $self->type;
 
 347   # create a form for generate_attachment_filename
 
 348   my $form   = Form->new;
 
 349   $form->{$self->nr_key()}  = $self->order->number;
 
 350   $form->{type}             = $self->type;
 
 351   $form->{format}           = $format;
 
 352   $form->{formname}         = $formname;
 
 353   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 354   my $pdf_filename          = $form->generate_attachment_filename();
 
 357   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
 
 358                                                    formname   => $formname,
 
 359                                                    language   => $self->order->language,
 
 361   if (scalar @errors) {
 
 362     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 364   $self->save_history('PREVIEWED');
 
 365   $self->js->flash('info', t8('The PDF has been previewed'));
 
 369     type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 370     name         => $pdf_filename,
 
 375 # open the email dialog
 
 376 sub action_save_and_show_email_dialog {
 
 379   my $errors = $self->save();
 
 381   if (scalar @{ $errors }) {
 
 382     $self->js->flash('error', $_) foreach @{ $errors };
 
 383     return $self->js->render();
 
 386   my $cv_method = $self->cv;
 
 388   if (!$self->order->$cv_method) {
 
 389     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'))
 
 394   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 395   $email_form->{to} ||= $self->order->$cv_method->email;
 
 396   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 397   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 398   # Todo: get addresses from shipto, if any
 
 400   my $form = Form->new;
 
 401   $form->{$self->nr_key()}  = $self->order->number;
 
 402   $form->{cusordnumber}     = $self->order->cusordnumber;
 
 403   $form->{formname}         = $self->type;
 
 404   $form->{type}             = $self->type;
 
 405   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 406   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
 
 407   $form->{format}           = 'pdf';
 
 408   $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
 410   $email_form->{subject}             = $form->generate_email_subject();
 
 411   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 412   $email_form->{message}             = $form->generate_email_body();
 
 413   $email_form->{js_send_function}    = 'kivi.DeliveryOrder.send_email()';
 
 415   my %files = $self->get_files_for_email_dialog();
 
 416   $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
 
 417   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 418                                   email_form  => $email_form,
 
 419                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
 
 421                                   is_customer => $self->type_data->is_customer,
 
 422                                   ALL_EMPLOYEES => $self->{all_employees},
 
 426       ->run('kivi.DeliveryOrder.show_email_dialog', $dialog_html)
 
 433 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 434 sub action_send_email {
 
 437   my $errors = $self->save();
 
 439   if (scalar @{ $errors }) {
 
 440     $self->js->run('kivi.DeliveryOrder.close_email_dialog');
 
 441     $self->js->flash('error', $_) foreach @{ $errors };
 
 442     return $self->js->render();
 
 445   $self->js_reset_order_and_item_ids_after_save;
 
 447   my $email_form  = delete $::form->{email_form};
 
 448   my %field_names = (to => 'email');
 
 450   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 452   # for Form::cleanup which may be called in Form::send_email
 
 453   $::form->{cwd}    = getcwd();
 
 454   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 456   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 457   $::form->{media}  = 'email';
 
 459   if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
 
 461     my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
 
 462                                                     format     => $::form->{print_options}->{format},
 
 463                                                     formname   => $::form->{print_options}->{formname},
 
 464                                                     language   => $self->order->language,
 
 465                                                     printer_id => $::form->{print_options}->{printer_id},
 
 466                                                     groupitems => $::form->{print_options}->{groupitems}});
 
 467     if (scalar @errors) {
 
 468       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
 
 471     my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $::form->{attachment_filename});
 
 472     if (scalar @warnings) {
 
 473       flash_later('warning', $_) for @warnings;
 
 476     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 477     $sfile->fh->print($pdf);
 
 480     $::form->{tmpfile} = $sfile->file_name;
 
 481     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 484   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 485   $::form->send_email(\%::myconfig, 'pdf');
 
 487   # internal notes unless no email journal
 
 488   unless ($::instance_conf->get_email_journal) {
 
 490     my $intnotes = $self->order->intnotes;
 
 491     $intnotes   .= "\n\n" if $self->order->intnotes;
 
 492     $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 493     $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 494     $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 495     $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 496     $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 497     $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 498     $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
 500     $self->order->update_attributes(intnotes => $intnotes);
 
 503   $self->save_history('MAILED');
 
 505   flash_later('info', t8('The email has been sent.'));
 
 507   my @redirect_params = (
 
 510     id     => $self->order->id,
 
 513   $self->redirect_to(@redirect_params);
 
 516 # save the order and redirect to the frontend subroutine for a new
 
 518 sub action_save_and_delivery_order {
 
 521   $self->save_and_redirect_to(
 
 522     controller => 'oe.pl',
 
 523     action     => 'oe_delivery_order_from_order',
 
 527 # save the order and redirect to the frontend subroutine for a new
 
 529 sub action_save_and_invoice {
 
 532   $self->save_and_redirect_to(
 
 533     controller => 'oe.pl',
 
 534     action     => 'oe_invoice_from_order',
 
 538 # workflow from sales order to sales quotation
 
 539 sub action_sales_quotation {
 
 540   $_[0]->workflow_sales_or_request_for_quotation();
 
 543 # workflow from sales order to sales quotation
 
 544 sub action_request_for_quotation {
 
 545   $_[0]->workflow_sales_or_request_for_quotation();
 
 548 # workflow from sales quotation to sales order
 
 549 sub action_sales_order {
 
 550   $_[0]->workflow_sales_or_purchase_order();
 
 553 # workflow from rfq to purchase order
 
 554 sub action_purchase_order {
 
 555   $_[0]->workflow_sales_or_purchase_order();
 
 558 # workflow from purchase order to ap transaction
 
 559 sub action_save_and_ap_transaction {
 
 562   $self->save_and_redirect_to(
 
 563     controller => 'ap.pl',
 
 564     action     => 'add_from_purchase_order',
 
 568 # set form elements in respect to a changed customer or vendor
 
 570 # This action is called on an change of the customer/vendor picker.
 
 571 sub action_customer_vendor_changed {
 
 574   setup_order_from_cv($self->order);
 
 576   my $cv_method = $self->cv;
 
 578   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 579     $self->js->show('#cp_row');
 
 581     $self->js->hide('#cp_row');
 
 584   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 585     $self->js->show('#shipto_selection');
 
 587     $self->js->hide('#shipto_selection');
 
 590   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 593     ->replaceWith('#order_cp_id',            $self->build_contact_select)
 
 594     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
 
 595     ->replaceWith('#shipto_inputs  ',        $self->build_shipto_inputs)
 
 596     ->replaceWith('#business_info_row',      $self->build_business_info_row)
 
 597     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
 
 598     ->val(        '#order_taxincluded',      $self->order->taxincluded)
 
 599     ->val(        '#order_currency_id',      $self->order->currency_id)
 
 600     ->val(        '#order_payment_id',       $self->order->payment_id)
 
 601     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
 
 602     ->val(        '#order_intnotes',         $self->order->intnotes)
 
 603     ->val(        '#order_language_id',      $self->order->$cv_method->language_id)
 
 604     ->focus(      '#order_' . $self->cv . '_id')
 
 605     ->run('kivi.DeliveryOrder.update_exchangerate');
 
 607   $self->js_redisplay_cvpartnumbers;
 
 611 # open the dialog for customer/vendor details
 
 612 sub action_show_customer_vendor_details_dialog {
 
 615   my $is_customer = 'customer' eq $::form->{vc};
 
 618     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 620     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 623   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 624   $details{discount_as_percent} = $cv->discount_as_percent;
 
 625   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 626   $details{business}            = $cv->business->description      if $cv->business;
 
 627   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 628   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 629   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 630   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 632   foreach my $entry (@{ $cv->shipto }) {
 
 633     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 635   foreach my $entry (@{ $cv->contacts }) {
 
 636     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 639   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 640                 is_customer => $is_customer,
 
 645 # called if a unit in an existing item row is changed
 
 646 sub action_unit_changed {
 
 649   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 650   my $item = $self->order->items_sorted->[$idx];
 
 652   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 653   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 656     ->run('kivi.DeliveryOrder.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 657   $self->js_redisplay_line_values;
 
 661 # add an item row for a new item entered in the input row
 
 662 sub action_add_item {
 
 665   delete $::form->{add_item}->{create_part_type};
 
 667   my $form_attr = $::form->{add_item};
 
 669   return unless $form_attr->{parts_id};
 
 671   my $item = new_item($self->order, $form_attr);
 
 673   $self->order->add_items($item);
 
 675   $self->get_item_cvpartnumber($item);
 
 677   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 678   my $row_as_html = $self->p->render('delivery_order/tabs/_row',
 
 682                                      in_out => $self->type_data->transfer,
 
 685   if ($::form->{insert_before_item_id}) {
 
 687       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 690       ->append('#row_table_id', $row_as_html);
 
 693   if ( $item->part->is_assortment ) {
 
 694     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 695     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 696       my $attr = { parts_id => $assortment_item->parts_id,
 
 697                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # 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;
 
 706       $self->order->add_items( $item );
 
 707       $self->get_item_cvpartnumber($item);
 
 708       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 709       my $row_as_html = $self->p->render('delivery_order/tabs/_row',
 
 714       if ($::form->{insert_before_item_id}) {
 
 716           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 719           ->append('#row_table_id', $row_as_html);
 
 725     ->val('.add_item_input', '')
 
 726     ->run('kivi.DeliveryOrder.init_row_handlers')
 
 727     ->run('kivi.DeliveryOrder.renumber_positions')
 
 728     ->focus('#add_item_parts_id_name');
 
 730   $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 735 # add item rows for multiple items at once
 
 736 sub action_add_multi_items {
 
 739   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
 
 740   return $self->js->render() unless scalar @form_attr;
 
 743   foreach my $attr (@form_attr) {
 
 744     my $item = new_item($self->order, $attr);
 
 746     if ( $item->part->is_assortment ) {
 
 747       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 748         my $attr = { parts_id => $assortment_item->parts_id,
 
 749                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 750                      unit     => $assortment_item->unit,
 
 751                      description => $assortment_item->part->description,
 
 753         my $item = new_item($self->order, $attr);
 
 755         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 756         $item->discount(1) unless $assortment_item->charge;
 
 761   $self->order->add_items(@items);
 
 763   foreach my $item (@items) {
 
 764     $self->get_item_cvpartnumber($item);
 
 765     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 766     my $row_as_html = $self->p->render('delivery_order/tabs/_row',
 
 770                                        in_out => $self->type_data->transfer,
 
 773     if ($::form->{insert_before_item_id}) {
 
 775         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 778         ->append('#row_table_id', $row_as_html);
 
 783     ->run('kivi.Part.close_picker_dialogs')
 
 784     ->run('kivi.DeliveryOrder.init_row_handlers')
 
 785     ->run('kivi.DeliveryOrder.renumber_positions')
 
 786     ->focus('#add_item_parts_id_name');
 
 788   $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 793 sub action_update_exchangerate {
 
 797     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
 798     currency_name => $self->order->currency->name,
 
 801   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
 804 # redisplay item rows if they are sorted by an attribute
 
 805 sub action_reorder_items {
 
 809     partnumber   => sub { $_[0]->part->partnumber },
 
 810     description  => sub { $_[0]->description },
 
 811     qty          => sub { $_[0]->qty },
 
 812     sellprice    => sub { $_[0]->sellprice },
 
 813     discount     => sub { $_[0]->discount },
 
 814     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
 817   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
 819   my $method = $sort_keys{$::form->{order_by}};
 
 820   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
 821   if ($::form->{sort_dir}) {
 
 822     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 823       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
 825       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 828     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 829       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
 831       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 835     ->run('kivi.DeliveryOrder.redisplay_items', \@to_sort)
 
 839 # show the popup to choose a price/discount source
 
 840 sub action_price_popup {
 
 843   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 844   my $item = $self->order->items_sorted->[$idx];
 
 846   $self->render_price_dialog($item);
 
 849 # save the order in a session variable and redirect to the part controller
 
 850 sub action_create_part {
 
 853   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
 
 855   my $callback     = $self->url_for(
 
 856     action       => 'return_from_create_part',
 
 857     type         => $self->type, # type is needed for check_auth on return
 
 858     previousform => $previousform,
 
 861   flash_later('info', t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.'));
 
 863   my @redirect_params = (
 
 864     controller    => 'Part',
 
 866     part_type     => $::form->{add_item}->{create_part_type},
 
 867     callback      => $callback,
 
 871   $self->redirect_to(@redirect_params);
 
 874 sub action_return_from_create_part {
 
 877   $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
 
 879   $::auth->restore_form_from_session(delete $::form->{previousform});
 
 881   # set item ids to new fake id, to identify them as new items
 
 882   foreach my $item (@{$self->order->items_sorted}) {
 
 883     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 886   $self->get_unalterable_data();
 
 889   # trigger rendering values for second row/longdescription as hidden,
 
 890   # because they are loaded only on demand. So we need to keep the values
 
 892   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
 893   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
 896     'delivery_order/form',
 
 897     title => $self->get_title_for('edit'),
 
 898     %{$self->{template_args}}
 
 903 sub action_stock_in_out_dialog {
 
 906   my $part    = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
 
 907   my $unit    = SL::DB::Unit->load_cached($::form->{unit}) or die "need unit";
 
 908   my $stock   = $::form->{stock};
 
 909   my $row     = $::form->{row};
 
 910   my $item_id = $::form->{item_id};
 
 911   my $qty     = _parse_number($::form->{qty_as_number});
 
 913   my $inout = $self->type_data->transfer;
 
 915   my @contents   = DO->get_item_availability(parts_id => $part->id);
 
 916   my $stock_info = DO->unpack_stock_information(packed => $stock);
 
 918   $self->merge_stock_data($stock_info, \@contents, $part, $unit);
 
 920   $self->render("delivery_order/stock_dialog", { layout => 0 },
 
 921     WHCONTENTS => $self->order->delivered ? $stock_info : \@contents,
 
 924     do_unit    => $unit->unit,
 
 925     delivered  => $self->order->delivered,
 
 931 sub action_update_stock_information {
 
 934   my $stock_info = $::form->{stock_info};
 
 935   my $unit = $::form->{unit};
 
 936   my $yaml = SL::YAML::Dump($stock_info);
 
 937   my $stock_qty = $self->calculate_stock_in_out_from_stock_info($unit, $stock_info);
 
 941     stock_qty => $stock_qty,
 
 943   $self->render(\ SL::JSON::to_json($response), { layout => 0, type => 'json', process => 0 });
 
 946 sub merge_stock_data {
 
 947   my ($self, $stock_info, $contents, $part, $unit) = @_;
 
 948   # TODO rewrite to mapping
 
 950   if (!$self->order->delivered) {
 
 951     for my $row (@$contents) {
 
 952       # row here is in parts units. stock is in item units
 
 953       $row->{available_qty} = _format_number($part->unit_obj->convert_to($row->{qty}, $unit));
 
 955       for my $sinfo (@{ $stock_info }) {
 
 956         next if $row->{bin_id}       != $sinfo->{bin_id} ||
 
 957                 $row->{warehouse_id} != $sinfo->{warehouse_id} ||
 
 958                 $row->{chargenumber} ne $sinfo->{chargenumber} ||
 
 959                 $row->{bestbefore}   ne $sinfo->{bestbefore};
 
 961         $row->{"stock_$_"} = $sinfo->{$_}
 
 962           for qw(qty unit error delivery_order_items_stock_id);
 
 967     for my $sinfo (@{ $stock_info }) {
 
 968       my $bin = SL::DB::Bin->load_cached($sinfo->{bin_id});
 
 969       $sinfo->{warehousedescription} = $bin->warehouse->description;
 
 970       $sinfo->{bindescription}       = $bin->description;
 
 971       map { $sinfo->{"stock_$_"}      = $sinfo->{$_} } qw(qty unit);
 
 976 # load the second row for one or more items
 
 978 # This action gets the html code for all items second rows by rendering a template for
 
 979 # the second row and sets the html code via client js.
 
 980 sub action_load_second_rows {
 
 983   foreach my $item_id (@{ $::form->{item_ids} }) {
 
 984     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
 985     my $item = $self->order->items_sorted->[$idx];
 
 987     $self->js_load_second_row($item, $item_id, 0);
 
 990   $self->js->run('kivi.DeliveryOrder.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
 995 # update description, notes and sellprice from master data
 
 996 sub action_update_row_from_master_data {
 
 999   foreach my $item_id (@{ $::form->{item_ids} }) {
 
1000     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1001     my $item  = $self->order->items_sorted->[$idx];
 
1002     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1004     $item->description($texts->{description});
 
1005     $item->longdescription($texts->{longdescription});
 
1007     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1010     if ($item->part->is_assortment) {
 
1011     # add assortment items with price 0, as the components carry the price
 
1012       $price_src = $price_source->price_from_source("");
 
1013       $price_src->price(0);
 
1015       $price_src = $price_source->best_price
 
1016                  ? $price_source->best_price
 
1017                  : $price_source->price_from_source("");
 
1018       $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
 
1019       $price_src->price(0) if !$price_source->best_price;
 
1023     $item->sellprice($price_src->price);
 
1024     $item->active_price_source($price_src);
 
1027       ->run('kivi.DeliveryOrder.update_sellprice', $item_id, $item->sellprice_as_number)
 
1028       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1029       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1030       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1032     if ($self->search_cvpartnumber) {
 
1033       $self->get_item_cvpartnumber($item);
 
1034       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1038   $self->js_redisplay_line_values;
 
1040   $self->js->render();
 
1043 sub action_transfer_stock {
 
1046   if ($self->order->delivered) {
 
1047     return $self->js->flash("error", t8('The parts for this order have already been transferred'))->render;
 
1050   my $inout = $self->type_data->properties('transfer');
 
1052   my $errors = $self->save;
 
1055     $self->js->flash('error', $_) for @$errors;
 
1056     return $self->js->render;
 
1059   my $order = $self->order;
 
1061   # TODO move to type data
 
1062   my $trans_type = $inout eq 'in'
 
1063     ? SL::DB::Manager::TransferType->find_by(direction => "id", description => "stock")
 
1064     : SL::DB::Manager::TransferType->find_by(direction => "out", description => "shipped");
 
1066   my @transfer_requests;
 
1068   for my $item (@{ $order->items_sorted }) {
 
1069     for my $stock (@{ $item->delivery_order_stock_entries }) {
 
1070       my $transfer = SL::DB::Inventory->new_from($stock);
 
1071       $transfer->trans_type($trans_type);
 
1072       $transfer->qty($transfer->qty * -1) if $inout eq 'out';
 
1074       push @transfer_requests, $transfer if defined $transfer->qty && $transfer->qty != 0;
 
1078   if (!@transfer_requests) {
 
1079     return $self->js->flash("error", t8("No stock to transfer"))->render;
 
1082   SL::DB->client->with_transaction(sub {
 
1083     $_->save for @transfer_requests;
 
1084     $self->order->update_attributes(delivered => 1);
 
1088     ->flash("info", t8("Stock transfered"))
 
1089     ->run('kivi.ActionBar.setDisabled', '#transfer_out_action', t8('The parts for this order have already been transferred'))
 
1090     ->run('kivi.ActionBar.setDisabled', '#transfer_in_action', t8('The parts for this order have already been transferred'))
 
1091     ->run('kivi.ActionBar.setDisabled', '#delete_action', t8('The parts for this order have already been transferred'))
 
1092     ->replaceWith('#data-status-line', delivery_order_status_line($self->order))
 
1097 sub js_load_second_row {
 
1098   my ($self, $item, $item_id, $do_parse) = @_;
 
1101     # Parse values from form (they are formated while rendering (template)).
 
1102     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1103     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1104     foreach my $var (@{ $item->cvars_by_config }) {
 
1105       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1107     $item->parse_custom_variable_values;
 
1110   my $row_as_html = $self->p->render('delivery_order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1113     ->html('#second_row_' . $item_id, $row_as_html)
 
1114     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1117 sub js_redisplay_line_values {
 
1120   my $is_sales = $self->order->is_sales;
 
1122   # sales orders with margins
 
1127        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1128        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1129        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1130       ]} @{ $self->order->items_sorted };
 
1134        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1135       ]} @{ $self->order->items_sorted };
 
1139     ->run('kivi.DeliveryOrder.redisplay_line_values', $is_sales, \@data);
 
1142 sub js_redisplay_cvpartnumbers {
 
1145   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1147   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1150     ->run('kivi.DeliveryOrder.redisplay_cvpartnumbers', \@data);
 
1153 sub js_reset_order_and_item_ids_after_save {
 
1157     ->val('#id', $self->order->id)
 
1158     ->val('#converted_from_oe_id', '')
 
1159     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1162   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1163     next if !$self->order->items_sorted->[$idx]->id;
 
1164     next if $form_item_id !~ m{^new};
 
1166       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1167       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1168       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1172   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1182   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1183     die "Not a valid type for delivery order";
 
1186   $self->type($::form->{type});
 
1192   return $self->type_data->customervendor;
 
1195 sub init_search_cvpartnumber {
 
1198   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1199   my $search_cvpartnumber;
 
1200   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1201   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1203   return $search_cvpartnumber;
 
1206 sub init_show_update_button {
 
1209   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1220 sub init_all_price_factors {
 
1221   SL::DB::Manager::PriceFactor->get_all;
 
1224 sub init_part_picker_classification_ids {
 
1227   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => $self->type_data->part_classification_query) } ];
 
1233   $::auth->assert($self->type_data->access('view') || 'DOES_NOT_EXIST');
 
1236 sub check_auth_for_edit {
 
1239   $::auth->assert($self->type_data->access('edit') || 'DOES_NOT_EXIST');
 
1242 # build the selection box for contacts
 
1244 # Needed, if customer/vendor changed.
 
1245 sub build_contact_select {
 
1248   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1249     value_key  => 'cp_id',
 
1250     title_key  => 'full_name_dep',
 
1251     default    => $self->order->cp_id,
 
1253     style      => 'width: 300px',
 
1257 # build the selection box for shiptos
 
1259 # Needed, if customer/vendor changed.
 
1260 sub build_shipto_select {
 
1263   select_tag('order.shipto_id',
 
1264              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1265              value_key  => 'shipto_id',
 
1266              title_key  => 'displayable_id',
 
1267              default    => $self->order->shipto_id,
 
1269              style      => 'width: 300px',
 
1273 # build the inputs for the cusom shipto dialog
 
1275 # Needed, if customer/vendor changed.
 
1276 sub build_shipto_inputs {
 
1279   my $content = $self->p->render('common/_ship_to_dialog',
 
1280                                  vc_obj      => $self->order->customervendor,
 
1281                                  cs_obj      => $self->order->custom_shipto,
 
1282                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1283                                  id_selector => '#order_shipto_id');
 
1285   div_tag($content, id => 'shipto_inputs');
 
1288 # render the info line for business
 
1290 # Needed, if customer/vendor changed.
 
1291 sub build_business_info_row
 
1293   $_[0]->p->render('delivery_order/tabs/_business_info_row', SELF => $_[0]);
 
1300   return if !$::form->{id};
 
1302   $self->order(SL::DB::DeliveryOrder->new(id => $::form->{id})->load);
 
1304   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1305   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1306   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1308   $self->prepare_stock_info($_) for $self->order->items;
 
1310   return $self->order;
 
1313 # load or create a new order object
 
1315 # And assign changes from the form to this object.
 
1316 # If the order is loaded from db, check if items are deleted in the form,
 
1317 # remove them form the object and collect them for removing from db on saving.
 
1318 # Then create/update items from form (via make_item) and add them.
 
1322   # add_items adds items to an order with no items for saving, but they cannot
 
1323   # be retrieved via items until the order is saved. Adding empty items to new
 
1324   # order here solves this problem.
 
1326   $order   = SL::DB::DeliveryOrder->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1327   $order ||= SL::DB::DeliveryOrder->new(orderitems  => [], currency_id => $::instance_conf->get_currency_id(), order_type => $self->type_data->validate($::form->{type}));
 
1329   my $cv_id_method = $self->cv . '_id';
 
1330   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1331     $order->$cv_id_method($::form->{$cv_id_method});
 
1332     setup_order_from_cv($order);
 
1335   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1337   $order->assign_attributes(%{$::form->{order}});
 
1339   $self->setup_custom_shipto_from_form($order, $::form);
 
1341   # remove deleted items
 
1342   $self->item_ids_to_delete([]);
 
1343   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1344     my $item = $order->orderitems->[$idx];
 
1345     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1346       splice @{$order->orderitems}, $idx, 1;
 
1347       push @{$self->item_ids_to_delete}, $item->id;
 
1353   foreach my $form_attr (@{$form_orderitems}) {
 
1354     my $item = make_item($order, $form_attr);
 
1355     $item->position($pos);
 
1360   $self->prepare_stock_info($_) for $order->items, @items;
 
1362   $order->add_items(grep {!$_->id} @items);
 
1367 # create or update items from form
 
1369 # Make item objects from form values. For items already existing read from db.
 
1370 # Create a new item else. And assign attributes.
 
1372   my ($record, $attr) = @_;
 
1375   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1377   my $is_new = !$item;
 
1379   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1380   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1381   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1382   $item ||= SL::DB::DeliveryOrderItem->new(custom_variables => []);
 
1385   if (my $stock_info = delete $attr->{stock_info}) {
 
1386     my %existing = map { $_->id => $_ } $item->delivery_order_stock_entries;
 
1389     for my $line (@{ DO->unpack_stock_information(packed => $stock_info) }) {
 
1390       # lookup existing or make new
 
1391       my $obj = delete $existing{$line->{delivery_order_items_stock_id}}
 
1392              // SL::DB::DeliveryOrderItemsStock->new;
 
1395       $obj->$_($line->{$_}) for qw(bin_id warehouse_id chargenumber qty unit);
 
1396       $obj->bestbefore_as_date($line->{bestfbefore})
 
1397         if $line->{bestbefore} && $::instance_conf->get_show_bestbefore;
 
1398       push @save, $obj if $obj->qty;
 
1401     $item->delivery_order_stock_entries(@save);
 
1404   $item->assign_attributes(%$attr);
 
1407     my $texts = get_part_texts($item->part, $record->language_id);
 
1408     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1409     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1410     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1418 # This is used to add one item
 
1420   my ($record, $attr) = @_;
 
1422   my $item = SL::DB::DeliveryOrderItem->new;
 
1424   # Remove attributes where the user left or set the inputs empty.
 
1425   # So these attributes will be undefined and we can distinguish them
 
1426   # from zero later on.
 
1427   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1428     delete $attr->{$_} if $attr->{$_} eq '';
 
1431   $item->assign_attributes(%$attr);
 
1433   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1434   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1436   $item->unit($part->unit) if !$item->unit;
 
1439   if ( $part->is_assortment ) {
 
1440     # add assortment items with price 0, as the components carry the price
 
1441     $price_src = $price_source->price_from_source("");
 
1442     $price_src->price(0);
 
1443   } elsif (defined $item->sellprice) {
 
1444     $price_src = $price_source->price_from_source("");
 
1445     $price_src->price($item->sellprice);
 
1447     $price_src = $price_source->best_price
 
1448                ? $price_source->best_price
 
1449                : $price_source->price_from_source("");
 
1450     $price_src->price(0) if !$price_source->best_price;
 
1454   if (defined $item->discount) {
 
1455     $discount_src = $price_source->discount_from_source("");
 
1456     $discount_src->discount($item->discount);
 
1458     $discount_src = $price_source->best_discount
 
1459                   ? $price_source->best_discount
 
1460                   : $price_source->discount_from_source("");
 
1461     $discount_src->discount(0) if !$price_source->best_discount;
 
1465   $new_attr{part}                   = $part;
 
1466   $new_attr{description}            = $part->description     if ! $item->description;
 
1467   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1468   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1469   $new_attr{sellprice}              = $price_src->price;
 
1470   $new_attr{discount}               = $discount_src->discount;
 
1471   $new_attr{active_price_source}    = $price_src;
 
1472   $new_attr{active_discount_source} = $discount_src;
 
1473   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1474   $new_attr{project_id}             = $record->globalproject_id;
 
1475   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1477   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1478   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1479   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1480   $new_attr{custom_variables} = [];
 
1482   my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1484   $item->assign_attributes(%new_attr, %{ $texts });
 
1489 sub prepare_stock_info {
 
1490   my ($self, $item) = @_;
 
1492   $item->{stock_info} = SL::YAML::Dump([
 
1494       delivery_order_items_stock_id => $_->id,
 
1496       warehouse_id                  => $_->warehouse_id,
 
1497       bin_id                        => $_->bin_id,
 
1498       chargenumber                  => $_->chargenumber,
 
1500     }, $item->delivery_order_stock_entries
 
1504 sub setup_order_from_cv {
 
1507   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
 
1509   $order->intnotes($order->customervendor->notes);
 
1511   if ($order->is_sales) {
 
1512     $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1513     $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1514                         ? $order->customer->taxincluded_checked
 
1515                         : $::myconfig{taxincluded_checked});
 
1520 # setup custom shipto from form
 
1522 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1523 # with 'shiptocvar_'.
 
1524 # Mark it to be deleted if a shipto from master data is selected
 
1525 # (i.e. order has a shipto).
 
1526 # Else, update or create a new custom shipto. If the fields are empty, it
 
1527 # will not be saved on save.
 
1528 sub setup_custom_shipto_from_form {
 
1529   my ($self, $order, $form) = @_;
 
1531   if ($order->shipto) {
 
1532     $self->is_custom_shipto_to_delete(1);
 
1534     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1536     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1537     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1539     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1540     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1544 # get data for saving, printing, ..., that is not changed in the form
 
1546 # Only cvars for now.
 
1547 sub get_unalterable_data {
 
1550   foreach my $item (@{ $self->order->items }) {
 
1551     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1552     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1553     foreach my $var (@{ $item->cvars_by_config }) {
 
1554       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1556     $item->parse_custom_variable_values;
 
1562 # And remove related files in the spool directory
 
1567   my $db     = $self->order->db;
 
1569   $db->with_transaction(
 
1571       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1572       $self->order->delete;
 
1573       my $spool = $::lx_office_conf{paths}->{spool};
 
1574       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1576       $self->save_history('DELETED');
 
1579   }) || push(@{$errors}, $db->error);
 
1586 # And delete items that are deleted in the form.
 
1591   my $db     = $self->order->db;
 
1593   $db->with_transaction(sub {
 
1594     # delete custom shipto if it is to be deleted or if it is empty
 
1595     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1596       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1597       $self->order->custom_shipto(undef);
 
1600     SL::DB::DeliveryOrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1601     $self->order->save(cascade => 1);
 
1604     if ($::form->{converted_from_oe_id}) {
 
1605       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1606       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1607         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1608         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/ && $self->order->is_type(PURCHASE_DELIVERY_ORDER_TYPE);
 
1609         $src->link_to_record($self->order);
 
1611       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1613         foreach (@{ $self->order->items_sorted }) {
 
1614           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1616           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1617                                   from_id    => $from_id,
 
1618                                   to_table   => 'orderitems',
 
1626     $self->save_history('SAVED');
 
1629   }) || push(@{$errors}, $db->error);
 
1634 sub workflow_sales_or_request_for_quotation {
 
1638   my $errors = $self->save();
 
1640   if (scalar @{ $errors }) {
 
1641     $self->js->flash('error', $_) for @{ $errors };
 
1642     return $self->js->render();
 
1645   my $destination_type = $self->type_data->workflow("to_quotation_type");
 
1647   $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
 
1648   $self->{converted_from_oe_id} = delete $::form->{id};
 
1650   # set item ids to new fake id, to identify them as new items
 
1651   foreach my $item (@{$self->order->items_sorted}) {
 
1652     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1656   $::form->{type} = $destination_type;
 
1657   $self->type($self->init_type);
 
1658   $self->cv  ($self->init_cv);
 
1661   $self->get_unalterable_data();
 
1662   $self->pre_render();
 
1664   # trigger rendering values for second row as hidden, because they
 
1665   # are loaded only on demand. So we need to keep the values from the
 
1667   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1670     'delivery_order/form',
 
1671     title => $self->get_title_for('edit'),
 
1672     %{$self->{template_args}}
 
1676 sub workflow_sales_or_purchase_order {
 
1680   my $errors = $self->save();
 
1682   if (scalar @{ $errors }) {
 
1683     $self->js->flash('error', $_) foreach @{ $errors };
 
1684     return $self->js->render();
 
1687   my $destination_type = $self->type_data->workflow("to_order_type");
 
1689   # check for direct delivery
 
1690   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1692   if ($self->type_data->workflow("to_order_copy_shipto") && $::form->{use_shipto} && $self->order->shipto) {
 
1693     $custom_shipto = $self->order->shipto->clone('SL::DB::DeliveryOrder');
 
1696   $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
 
1697   $self->{converted_from_oe_id} = delete $::form->{id};
 
1699   # set item ids to new fake id, to identify them as new items
 
1700   foreach my $item (@{$self->order->items_sorted}) {
 
1701     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1704   if ($self->type_data->workflow("to_order_copy_shipto")) {
 
1705     if ($::form->{use_shipto}) {
 
1706       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
1708       # remove any custom shipto if not wanted
 
1709       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1714   $::form->{type} = $destination_type;
 
1715   $self->type($self->init_type);
 
1716   $self->cv  ($self->init_cv);
 
1719   $self->get_unalterable_data();
 
1720   $self->pre_render();
 
1722   # trigger rendering values for second row as hidden, because they
 
1723   # are loaded only on demand. So we need to keep the values from the
 
1725   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1728     'delivery_order/form',
 
1729     title => $self->get_title_for('edit'),
 
1730     %{$self->{template_args}}
 
1737   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1738   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
1739   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1740   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
 
1741   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1744   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1747   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1749   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1750   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1751   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1752   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1754   my $print_form = Form->new('');
 
1755   $print_form->{type}        = $self->type;
 
1756   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
1757   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
1758     form => $print_form,
 
1759     options => {dialog_name_prefix => 'print_options.',
 
1763                 no_opendocument    => 0,
 
1767   foreach my $item (@{$self->order->orderitems}) {
 
1768     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1769     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1770     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1773   if ($self->order->${\ $self->type_data->nr_key } && $::instance_conf->get_webdav) {
 
1774     my $webdav = SL::Webdav->new(
 
1775       type     => $self->type,
 
1776       number   => $self->order->number,
 
1778     my @all_objects = $webdav->get_all_objects;
 
1779     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1781                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1785   $self->{template_args}{in_out}                                 = $self->type_data->transfer;
 
1786   $self->{template_args}{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
 
1788   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1790   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.DeliveryOrder kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
1791                                                          calculate_qty kivi.Validator follow_up show_history);
 
1792   $self->setup_edit_action_bar;
 
1795 sub setup_edit_action_bar {
 
1796   my ($self, %params) = @_;
 
1798   my $deletion_allowed = $self->type_data->show_menu("delete");
 
1799   my $may_edit_create  = $::auth->assert($self->type_data->access('edit') || 'DOES_NOT_EXIST', 1);
 
1801   for my $bar ($::request->layout->get('actionbar')) {
 
1806           call     => [ 'kivi.DeliveryOrder.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1807                                                            $::instance_conf->get_order_warn_no_deliverydate,
 
1809           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1813           call     => [ 'kivi.DeliveryOrder.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1814           disabled => !$may_edit_create                        ? t8('You do not have the permissions to access this function.')
 
1815                     : $self->type eq 'supplier_delivery_order' ? t8('Need a workflow for Supplier Delivery Order')
 
1816                     : !$self->order->id                        ? t8('This object has not been saved yet.')
 
1819       ], # end of combobox "Save"
 
1826           t8('Save and Quotation'),
 
1827           submit   => [ '#order_form', { action => "DeliveryOrder/sales_quotation" } ],
 
1828           only_if  => $self->type_data->show_menu("save_and_quotation"),
 
1829           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1833           submit   => [ '#order_form', { action => "DeliveryOrder/request_for_quotation" } ],
 
1834           only_if  => $self->type_data->show_menu("save_and_rfq"),
 
1835           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1838           t8('Save and Sales Order'),
 
1839           submit   => [ '#order_form', { action => "DeliveryOrder/sales_order" } ],
 
1840           only_if  => $self->type_data->show_menu("save_and_sales_order"),
 
1841           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1844           t8('Save and Purchase Order'),
 
1845           call     => [ 'kivi.DeliveryOrder.purchase_order_check_for_direct_delivery' ],
 
1846           only_if  => $self->type_data->show_menu("save_and_purchase_order"),
 
1847           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1850           t8('Save and Delivery Order'),
 
1851           call     => [ 'kivi.DeliveryOrder.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
1852                                                                               $::instance_conf->get_order_warn_no_deliverydate,
 
1854           only_if  => $self->type_data->show_menu("save_and_delivery_order"),
 
1855           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1858           t8('Save and Invoice'),
 
1859           call     => [ 'kivi.DeliveryOrder.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1860           only_if  => $self->type_data->show_menu("save_and_invoice"),
 
1861           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1864           t8('Save and AP Transaction'),
 
1865           call     => [ 'kivi.DeliveryOrder.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
1866           only_if  => $self->type_data->show_menu("save_and_ap_transaction"),
 
1867           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1870       ], # end of combobox "Workflow"
 
1877           t8('Save and preview PDF'),
 
1878            call    => [ 'kivi.DeliveryOrder.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
1879                                                                   $::instance_conf->get_order_warn_no_deliverydate,
 
1881           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1884           t8('Save and print'),
 
1885           call     => [ 'kivi.DeliveryOrder.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
1886                                                                  $::instance_conf->get_order_warn_no_deliverydate,
 
1888           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1891           t8('Save and E-mail'),
 
1892           id       => 'save_and_email_action',
 
1893           call     => [ 'kivi.DeliveryOrder.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
1894                                                                                  $::instance_conf->get_order_warn_no_deliverydate,
 
1896           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
 
1897                     : !$self->order->id ? t8('This object has not been saved yet.')
 
1901           t8('Download attachments of all parts'),
 
1902           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1903           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
 
1904                     : !$self->order->id ? t8('This object has not been saved yet.')
 
1906           only_if  => $::instance_conf->get_doc_storage,
 
1908       ], # end of combobox "Export"
 
1912         id       => 'delete_action',
 
1913         call     => [ 'kivi.DeliveryOrder.delete_order' ],
 
1914         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1915         disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
 
1916                   : !$self->order->id       ? t8('This object has not been saved yet.')
 
1917                   : $self->order->delivered ? t8('The parts for this order have already been transferred')
 
1919         only_if  => $self->type_data->show_menu("delete"),
 
1925           id       => 'transfer_out_action',
 
1926           call     => [ 'kivi.DeliveryOrder.save', 'transfer_stock' ],
 
1927           disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
 
1928                     : !$self->order->id       ? t8('This object has not been saved yet.')
 
1929                     : $self->order->delivered ? t8('The parts for this order have already been transferred')
 
1931           only_if  => $self->type_data->properties('transfer') eq 'out',
 
1932           confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
 
1936           id       => 'transfer_in_action',
 
1937           call     => [ 'kivi.DeliveryOrder.save', 'transfer_stock' ],
 
1938           disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
 
1939                     : !$self->order->id       ? t8('This object has not been saved yet.')
 
1940                     : $self->order->delivered ? t8('The parts for this order have already been transferred')
 
1942           only_if  => $self->type_data->properties('transfer') eq 'in',
 
1943           confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
 
1953           call     => [ 'kivi.DeliveryOrder.follow_up_window' ],
 
1954           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1955           only_if  => $::auth->assert('productivity', 1),
 
1959           call     => [ 'set_history_window', $self->order->id, 'id' ],
 
1960           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
 
1962       ], # end of combobox "more"
 
1968   my ($order, $pdf_ref, $params) = @_;
 
1972   my $print_form = Form->new('');
 
1973   $print_form->{type}        = $order->type;
 
1974   $print_form->{formname}    = $params->{formname} || $order->type;
 
1975   $print_form->{format}      = $params->{format}   || 'pdf';
 
1976   $print_form->{media}       = $params->{media}    || 'file';
 
1977   $print_form->{groupitems}  = $params->{groupitems};
 
1978   $print_form->{printer_id}  = $params->{printer_id};
 
1979   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
1981   $order->language($params->{language});
 
1982   $order->flatten_to_form($print_form, format_amounts => 1);
 
1986   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
1987     $template_ext  = 'odt';
 
1988     $template_type = 'OpenDocument';
 
1991   # search for the template
 
1992   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
1993     name        => $print_form->{formname},
 
1994     extension   => $template_ext,
 
1995     email       => $print_form->{media} eq 'email',
 
1996     language    => $params->{language},
 
1997     printer_id  => $print_form->{printer_id},
 
2000   if (!defined $template_file) {
 
2001     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);
 
2004   return @errors if scalar @errors;
 
2006   $print_form->throw_on_error(sub {
 
2008       $print_form->prepare_for_printing;
 
2010       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
2011         format        => $print_form->{format},
 
2012         template_type => $template_type,
 
2013         template      => $template_file,
 
2014         variables     => $print_form,
 
2015         variable_content_types => {
 
2016           longdescription => 'html',
 
2017           partnotes       => 'html',
 
2022     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2028 sub get_files_for_email_dialog {
 
2031   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2033   return %files if !$::instance_conf->get_doc_storage;
 
2035   if ($self->order->id) {
 
2036     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2037     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2038     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2039     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2043     uniq_by { $_->{id} }
 
2045       +{ id         => $_->part->id,
 
2046          partnumber => $_->part->partnumber }
 
2047     } @{$self->order->items_sorted};
 
2049   foreach my $part (@parts) {
 
2050     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2051     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2054   foreach my $key (keys %files) {
 
2055     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2062   my ($self, $action) = @_;
 
2064   return '' if none { lc($action)} qw(add edit);
 
2065   return $self->type_data->text($action);
 
2068 sub get_item_cvpartnumber {
 
2069   my ($self, $item) = @_;
 
2071   return if !$self->search_cvpartnumber;
 
2072   return if !$self->order->customervendor;
 
2074   if ($self->cv eq 'vendor') {
 
2075     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2076     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2077   } elsif ($self->cv eq 'customer') {
 
2078     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2079     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2083 sub get_part_texts {
 
2084   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2086   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2087   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2089     description     => $defaults{description}     // $part->description,
 
2090     longdescription => $defaults{longdescription} // $part->notes,
 
2093   return $texts unless $language_id;
 
2095   my $translation = SL::DB::Manager::Translation->get_first(
 
2097       parts_id    => $part->id,
 
2098       language_id => $language_id,
 
2101   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2102   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2108   return $_[0]->type_data->nr_key;
 
2111 sub save_and_redirect_to {
 
2112   my ($self, %params) = @_;
 
2114   my $errors = $self->save();
 
2116   if (scalar @{ $errors }) {
 
2117     $self->js->flash('error', $_) foreach @{ $errors };
 
2118     return $self->js->render();
 
2121   flash_later('info', $self->type_data->text("saved"));
 
2123   $self->redirect_to(%params, id => $self->order->id);
 
2127   my ($self, $addition) = @_;
 
2129   my $number_type = $self->nr_key;
 
2130   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2132   SL::DB::History->new(
 
2133     trans_id    => $self->order->id,
 
2134     employee_id => SL::DB::Manager::Employee->current->id,
 
2135     what_done   => $self->order->type,
 
2136     snumbers    => $snumbers,
 
2137     addition    => $addition,
 
2141 sub store_pdf_to_webdav_and_filemanagement {
 
2142   my($order, $content, $filename) = @_;
 
2146   # copy file to webdav folder
 
2147   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2148     my $webdav = SL::Webdav->new(
 
2149       type     => $order->type,
 
2150       number   => $order->number,
 
2152     my $webdav_file = SL::Webdav::File->new(
 
2154       filename => $filename,
 
2157       $webdav_file->store(data => \$content);
 
2160       push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
 
2163   if ($order->id && $::instance_conf->get_doc_storage) {
 
2165       SL::File->save(object_id     => $order->id,
 
2166                      object_type   => $order->type,
 
2167                      mime_type     => 'application/pdf',
 
2168                      source        => 'created',
 
2169                      file_type     => 'document',
 
2170                      file_name     => $filename,
 
2171                      file_contents => $content);
 
2174       push @errors, t8('Storing PDF in storage backend failed: #1', $@);
 
2181 sub calculate_stock_in_out_from_stock_info {
 
2182   my ($self, $unit, $stock_info) = @_;
 
2184   return "" if !$unit;
 
2186   my %units_by_name = map { $_->name => $_ } @{ SL::DB::Manager::Unit->get_all };
 
2188   my $sum      = sum0 map {
 
2189     $units_by_name{$_->{unit}}->convert_to($_->{qty}, $units_by_name{$unit})
 
2192   my $content  = _format_number($sum, 2) . ' ' . $unit;
 
2197 sub calculate_stock_in_out {
 
2198   my ($self, $item, $stock_info) = @_;
 
2200   return "" if !$item->part || !$item->part->unit || !$item->unit;
 
2202   my $sum      = sum0 map {
 
2203     $_->unit_obj->convert_to($_->qty, $item->unit_obj)
 
2204   } $item->delivery_order_stock_entries;
 
2206   my $content  = _format_number($sum, 2);
 
2211 sub init_type_data {
 
2212   SL::Controller::DeliveryOrder::TypeData->new($_[0]);
 
2215 sub init_valid_types {
 
2216   $_[0]->type_data->valid_types;
 
2227 SL::Controller::Order - controller for orders
 
2231 This is a new form to enter orders, completely rewritten with the use
 
2232 of controller and java script techniques.
 
2234 The aim is to provide the user a better experience and a faster workflow. Also
 
2235 the code should be more readable, more reliable and better to maintain.
 
2243 One input row, so that input happens every time at the same place.
 
2247 Use of pickers where possible.
 
2251 Possibility to enter more than one item at once.
 
2255 Item list in a scrollable area, so that the workflow buttons stay at
 
2260 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2261 possible (by partnumber, description, qty, sellprice and discount for now).
 
2265 No C<update> is necessary. All entries and calculations are managed
 
2266 with ajax-calls and the page only reloads on C<save>.
 
2270 User can see changes immediately, because of the use of java script
 
2281 =item * C<SL/Controller/Order.pm>
 
2285 =item * C<template/webpages/delivery_order/form.html>
 
2289 =item * C<template/webpages/delivery_order/tabs/basic_data.html>
 
2291 Main tab for basic_data.
 
2293 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2294 reused from generic code.
 
2298 =item * C<template/webpages/delivery_order/tabs/_business_info_row.html>
 
2300 For displaying information on business type
 
2302 =item * C<template/webpages/delivery_order/tabs/_item_input.html>
 
2304 The input line for items
 
2306 =item * C<template/webpages/delivery_order/tabs/_row.html>
 
2308 One row for already entered items
 
2310 =item * C<template/webpages/delivery_order/tabs/_tax_row.html>
 
2312 Displaying tax information
 
2316 =item * C<js/kivi.DeliveryOrder.js>
 
2318 java script functions
 
2328 =item * price sources: little symbols showing better price / better discount
 
2330 =item * select units in input row?
 
2332 =item * check for direct delivery (workflow sales order -> purchase order)
 
2334 =item * access rights
 
2336 =item * display weights
 
2340 =item * optional client/user behaviour
 
2342 (transactions has to be set - department has to be set -
 
2343  force project if enabled in client config - transport cost reminder)
 
2347 =head1 KNOWN BUGS AND CAVEATS
 
2353 Customer discount is not displayed as a valid discount in price source popup
 
2354 (this might be a bug in price sources)
 
2356 (I cannot reproduce this (Bernd))
 
2360 No indication that <shift>-up/down expands/collapses second row.
 
2364 Inline creation of parts is not currently supported
 
2368 Table header is not sticky in the scrolling area.
 
2372 Sorting does not include C<position>, neither does reordering.
 
2374 This behavior was implemented intentionally. But we can discuss, which behavior
 
2375 should be implemented.
 
2379 =head1 To discuss / Nice to have
 
2385 How to expand/collapse second row. Now it can be done clicking the icon or
 
2390 Possibility to select PriceSources in input row?
 
2394 This controller uses a (changed) copy of the template for the PriceSource
 
2395 dialog. Maybe there could be used one code source.
 
2399 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2400 form. This is not only a problem here, but also in all parts using the PTC.
 
2401 There exists a ticket and a patch. This patch should be testet.
 
2405 An indicator, if the actual inputs are saved (like in an
 
2406 editor or on text processing application).
 
2410 A warning when leaving the page without saveing unchanged inputs.
 
2417 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>