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::PositionsScrollbar;
 
  37 use SL::Helper::UserPreferences::UpdatePositions;
 
  39 use SL::Controller::Helper::GetModels;
 
  40 use SL::Controller::DeliveryOrder::TypeData qw(:types);
 
  42 use List::Util qw(first sum0);
 
  43 use List::UtilsBy qw(sort_by uniq_by);
 
  44 use List::MoreUtils qw(any none pairwise first_index);
 
  45 use English qw(-no_match_vars);
 
  50 use Rose::Object::MakeMethods::Generic
 
  52  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
 
  53  '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) ],
 
  58 __PACKAGE__->run_before('check_auth',
 
  59                         except => [ qw(update_stock_information) ]);
 
  61 __PACKAGE__->run_before('check_auth_for_edit',
 
  62                         except => [ qw(update_stock_information edit show_customer_vendor_details_dialog price_popup stock_in_out_dialog load_second_rows) ]);
 
  64 __PACKAGE__->run_before('get_unalterable_data',
 
  65                         only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
 
  76   $self->order->transdate(DateTime->now_local());
 
  77   $self->type_data->set_reqdate_by_type;
 
  82     'delivery_order/form',
 
  83     title => $self->get_title_for('add'),
 
  84     %{$self->{template_args}}
 
  88 sub action_add_from_order {
 
  90   # this interfers with init_order
 
  91   $self->{converted_from_oe_id} = delete $::form->{id};
 
  93   $self->type_data->validate($::form->{type});
 
  95   my $order = SL::DB::Order->new(id => $self->{converted_from_oe_id})->load;
 
  97   $self->order(SL::DB::DeliveryOrder->new_from($order, type => $::form->{type}));
 
 102 # edit an existing order
 
 110     # this is to edit an order from an unsaved order object
 
 112     # set item ids to new fake id, to identify them as new items
 
 113     foreach my $item (@{$self->order->items_sorted}) {
 
 114       $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 116     # trigger rendering values for second row as hidden, because they
 
 117     # are loaded only on demand. So we need to keep the values from
 
 119     $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
 124     'delivery_order/form',
 
 125     title => $self->get_title_for('edit'),
 
 126     %{$self->{template_args}}
 
 130 # edit a collective order (consisting of one or more existing orders)
 
 131 sub action_edit_collective {
 
 135   my @multi_ids = map {
 
 136     $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
 
 137   } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
 
 139   # fall back to add if no ids are given
 
 140   if (scalar @multi_ids == 0) {
 
 145   # fall back to save as new if only one id is given
 
 146   if (scalar @multi_ids == 1) {
 
 147     $self->order(SL::DB::DeliveryOrder->new(id => $multi_ids[0])->load);
 
 148     $self->action_save_as_new();
 
 152   # make new order from given orders
 
 153   my @multi_orders = map { SL::DB::DeliveryOrder->new(id => $_)->load } @multi_ids;
 
 154   $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
 
 155   $self->order(SL::DB::DeliveryOrder->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
 
 157   $self->action_edit();
 
 164   my $errors = $self->delete();
 
 166   if (scalar @{ $errors }) {
 
 167     $self->js->flash('error', $_) foreach @{ $errors };
 
 168     return $self->js->render();
 
 171   flash_later('info', $self->type_data->text("delete"));
 
 173   my @redirect_params = (
 
 178   $self->redirect_to(@redirect_params);
 
 185   my $errors = $self->save();
 
 187   if (scalar @{ $errors }) {
 
 188     $self->js->flash('error', $_) foreach @{ $errors };
 
 189     return $self->js->render();
 
 192   flash_later('info', $self->type_data->text("saved"));
 
 194   my @redirect_params = (
 
 197     id     => $self->order->id,
 
 200   $self->redirect_to(@redirect_params);
 
 203 # save the order as new document an open it for edit
 
 204 sub action_save_as_new {
 
 207   my $order = $self->order;
 
 210     $self->js->flash('error', t8('This object has not been saved yet.'));
 
 211     return $self->js->render();
 
 214   # load order from db to check if values changed
 
 215   my $saved_order = SL::DB::DeliveryOrder->new(id => $order->id)->load;
 
 218   # Lets assign a new number if the user hasn't changed the previous one.
 
 219   # If it has been changed manually then use it as-is.
 
 220   $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
 
 222                         : trim($order->number);
 
 224   # Clear transdate unless changed
 
 225   $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
 
 226                         ? DateTime->today_local
 
 229   # Set new reqdate unless changed if it is enabled in client config
 
 230   $new_attrs{reqdate} = $self->type_data->get_reqdate_by_type($order->reqdate, $saved_order->reqdate);
 
 233   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
 235   # Create new record from current one
 
 236   $self->order(SL::DB::DeliveryOrder->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
 238   # no linked records on save as new
 
 239   delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
 
 242   $self->action_save();
 
 247 # This is called if "print" is pressed in the print dialog.
 
 248 # If PDF creation was requested and succeeded, the pdf is offered for download
 
 249 # via send_file (which uses ajax in this case).
 
 253   my $errors = $self->save();
 
 255   if (scalar @{ $errors }) {
 
 256     $self->js->flash('error', $_) foreach @{ $errors };
 
 257     return $self->js->render();
 
 260   $self->js_reset_order_and_item_ids_after_save;
 
 262   my $format      = $::form->{print_options}->{format};
 
 263   my $media       = $::form->{print_options}->{media};
 
 264   my $formname    = $::form->{print_options}->{formname};
 
 265   my $copies      = $::form->{print_options}->{copies};
 
 266   my $groupitems  = $::form->{print_options}->{groupitems};
 
 267   my $printer_id  = $::form->{print_options}->{printer_id};
 
 269   # only pdf and opendocument by now
 
 270   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
 
 271     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
 
 274   # only screen or printer by now
 
 275   if (none { $media eq $_ } qw(screen printer)) {
 
 276     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
 
 279   # create a form for generate_attachment_filename
 
 280   my $form   = Form->new;
 
 281   $form->{$self->nr_key()}  = $self->order->number;
 
 282   $form->{type}             = $self->type;
 
 283   $form->{format}           = $format;
 
 284   $form->{formname}         = $formname;
 
 285   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 286   my $pdf_filename          = $form->generate_attachment_filename();
 
 289   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
 
 290                                                    formname   => $formname,
 
 291                                                    language   => $self->order->language,
 
 292                                                    printer_id => $printer_id,
 
 293                                                    groupitems => $groupitems });
 
 294   if (scalar @errors) {
 
 295     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 298   if ($media eq 'screen') {
 
 300     $self->js->flash('info', t8('The PDF has been created'));
 
 303       type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 304       name         => $pdf_filename,
 
 308   } elsif ($media eq 'printer') {
 
 310     my $printer_id = $::form->{print_options}->{printer_id};
 
 311     SL::DB::Printer->new(id => $printer_id)->load->print_document(
 
 316     $self->js->flash('info', t8('The PDF has been printed'));
 
 319   my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $pdf_filename);
 
 320   if (scalar @warnings) {
 
 321     $self->js->flash('warning', $_) for @warnings;
 
 324   $self->save_history('PRINTED');
 
 327     ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
 
 330 sub action_preview_pdf {
 
 333   my $errors = $self->save();
 
 334   if (scalar @{ $errors }) {
 
 335     $self->js->flash('error', $_) foreach @{ $errors };
 
 336     return $self->js->render();
 
 339   $self->js_reset_order_and_item_ids_after_save;
 
 342   my $media       = 'screen';
 
 343   my $formname    = $self->type;
 
 346   # create a form for generate_attachment_filename
 
 347   my $form   = Form->new;
 
 348   $form->{$self->nr_key()}  = $self->order->number;
 
 349   $form->{type}             = $self->type;
 
 350   $form->{format}           = $format;
 
 351   $form->{formname}         = $formname;
 
 352   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 353   my $pdf_filename          = $form->generate_attachment_filename();
 
 356   my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
 
 357                                                    formname   => $formname,
 
 358                                                    language   => $self->order->language,
 
 360   if (scalar @errors) {
 
 361     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
 
 363   $self->save_history('PREVIEWED');
 
 364   $self->js->flash('info', t8('The PDF has been previewed'));
 
 368     type         => SL::MIME->mime_type_from_ext($pdf_filename),
 
 369     name         => $pdf_filename,
 
 374 # open the email dialog
 
 375 sub action_save_and_show_email_dialog {
 
 378   my $errors = $self->save();
 
 380   if (scalar @{ $errors }) {
 
 381     $self->js->flash('error', $_) foreach @{ $errors };
 
 382     return $self->js->render();
 
 385   my $cv_method = $self->cv;
 
 387   if (!$self->order->$cv_method) {
 
 388     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'))
 
 393   $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
 
 394   $email_form->{to} ||= $self->order->$cv_method->email;
 
 395   $email_form->{cc}   = $self->order->$cv_method->cc;
 
 396   $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
 
 397   # Todo: get addresses from shipto, if any
 
 399   my $form = Form->new;
 
 400   $form->{$self->nr_key()}  = $self->order->number;
 
 401   $form->{cusordnumber}     = $self->order->cusordnumber;
 
 402   $form->{formname}         = $self->type;
 
 403   $form->{type}             = $self->type;
 
 404   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
 
 405   $form->{language_id}      = $self->order->language->id                  if $self->order->language;
 
 406   $form->{format}           = 'pdf';
 
 407   $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
 
 409   $email_form->{subject}             = $form->generate_email_subject();
 
 410   $email_form->{attachment_filename} = $form->generate_attachment_filename();
 
 411   $email_form->{message}             = $form->generate_email_body();
 
 412   $email_form->{js_send_function}    = 'kivi.DeliveryOrder.send_email()';
 
 414   my %files = $self->get_files_for_email_dialog();
 
 415   $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
 
 416   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
 
 417                                   email_form  => $email_form,
 
 418                                   show_bcc    => $::auth->assert('email_bcc', 'may fail'),
 
 420                                   is_customer => $self->type_data->is_customer,
 
 421                                   ALL_EMPLOYEES => $self->{all_employees},
 
 425       ->run('kivi.DeliveryOrder.show_email_dialog', $dialog_html)
 
 432 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 
 433 sub action_send_email {
 
 436   my $errors = $self->save();
 
 438   if (scalar @{ $errors }) {
 
 439     $self->js->run('kivi.DeliveryOrder.close_email_dialog');
 
 440     $self->js->flash('error', $_) foreach @{ $errors };
 
 441     return $self->js->render();
 
 444   $self->js_reset_order_and_item_ids_after_save;
 
 446   my $email_form  = delete $::form->{email_form};
 
 447   my %field_names = (to => 'email');
 
 449   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
 
 451   # for Form::cleanup which may be called in Form::send_email
 
 452   $::form->{cwd}    = getcwd();
 
 453   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
 
 455   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
 
 456   $::form->{media}  = 'email';
 
 458   if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
 
 460     my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
 
 461                                                     format     => $::form->{print_options}->{format},
 
 462                                                     formname   => $::form->{print_options}->{formname},
 
 463                                                     language   => $self->order->language,
 
 464                                                     printer_id => $::form->{print_options}->{printer_id},
 
 465                                                     groupitems => $::form->{print_options}->{groupitems}});
 
 466     if (scalar @errors) {
 
 467       return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
 
 470     my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $::form->{attachment_filename});
 
 471     if (scalar @warnings) {
 
 472       flash_later('warning', $_) for @warnings;
 
 475     my $sfile = SL::SessionFile::Random->new(mode => "w");
 
 476     $sfile->fh->print($pdf);
 
 479     $::form->{tmpfile} = $sfile->file_name;
 
 480     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
 
 483   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
 
 484   $::form->send_email(\%::myconfig, 'pdf');
 
 486   # internal notes unless no email journal
 
 487   unless ($::instance_conf->get_email_journal) {
 
 489     my $intnotes = $self->order->intnotes;
 
 490     $intnotes   .= "\n\n" if $self->order->intnotes;
 
 491     $intnotes   .= t8('[email]')                                                                                        . "\n";
 
 492     $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
 
 493     $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
 
 494     $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
 
 495     $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
 
 496     $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
 
 497     $intnotes   .= t8('Message')    . ": " . $::form->{message};
 
 499     $self->order->update_attributes(intnotes => $intnotes);
 
 502   $self->save_history('MAILED');
 
 504   flash_later('info', t8('The email has been sent.'));
 
 506   my @redirect_params = (
 
 509     id     => $self->order->id,
 
 512   $self->redirect_to(@redirect_params);
 
 515 # save the order and redirect to the frontend subroutine for a new
 
 517 sub action_save_and_delivery_order {
 
 520   $self->save_and_redirect_to(
 
 521     controller => 'oe.pl',
 
 522     action     => 'oe_delivery_order_from_order',
 
 526 # save the order and redirect to the frontend subroutine for a new
 
 528 sub action_save_and_invoice {
 
 531   $self->save_and_redirect_to(
 
 532     controller => 'oe.pl',
 
 533     action     => 'oe_invoice_from_order',
 
 537 # workflow from sales order to sales quotation
 
 538 sub action_sales_quotation {
 
 539   $_[0]->workflow_sales_or_request_for_quotation();
 
 542 # workflow from sales order to sales quotation
 
 543 sub action_request_for_quotation {
 
 544   $_[0]->workflow_sales_or_request_for_quotation();
 
 547 # workflow from sales quotation to sales order
 
 548 sub action_sales_order {
 
 549   $_[0]->workflow_sales_or_purchase_order();
 
 552 # workflow from rfq to purchase order
 
 553 sub action_purchase_order {
 
 554   $_[0]->workflow_sales_or_purchase_order();
 
 557 # workflow from purchase order to ap transaction
 
 558 sub action_save_and_ap_transaction {
 
 561   $self->save_and_redirect_to(
 
 562     controller => 'ap.pl',
 
 563     action     => 'add_from_purchase_order',
 
 567 # set form elements in respect to a changed customer or vendor
 
 569 # This action is called on an change of the customer/vendor picker.
 
 570 sub action_customer_vendor_changed {
 
 573   setup_order_from_cv($self->order);
 
 575   my $cv_method = $self->cv;
 
 577   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
 
 578     $self->js->show('#cp_row');
 
 580     $self->js->hide('#cp_row');
 
 583   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
 
 584     $self->js->show('#shipto_selection');
 
 586     $self->js->hide('#shipto_selection');
 
 589   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
 
 592     ->replaceWith('#order_cp_id',            $self->build_contact_select)
 
 593     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
 
 594     ->replaceWith('#shipto_inputs  ',        $self->build_shipto_inputs)
 
 595     ->replaceWith('#business_info_row',      $self->build_business_info_row)
 
 596     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
 
 597     ->val(        '#order_taxincluded',      $self->order->taxincluded)
 
 598     ->val(        '#order_currency_id',      $self->order->currency_id)
 
 599     ->val(        '#order_payment_id',       $self->order->payment_id)
 
 600     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
 
 601     ->val(        '#order_intnotes',         $self->order->intnotes)
 
 602     ->val(        '#order_language_id',      $self->order->$cv_method->language_id)
 
 603     ->focus(      '#order_' . $self->cv . '_id')
 
 604     ->run('kivi.DeliveryOrder.update_exchangerate');
 
 606   $self->js_redisplay_cvpartnumbers;
 
 610 # open the dialog for customer/vendor details
 
 611 sub action_show_customer_vendor_details_dialog {
 
 614   my $is_customer = 'customer' eq $::form->{vc};
 
 617     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
 
 619     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
 
 622   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
 
 623   $details{discount_as_percent} = $cv->discount_as_percent;
 
 624   $details{creditlimt}          = $cv->creditlimit_as_number;
 
 625   $details{business}            = $cv->business->description      if $cv->business;
 
 626   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
 
 627   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
 
 628   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
 
 629   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
 
 631   foreach my $entry (@{ $cv->shipto }) {
 
 632     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 634   foreach my $entry (@{ $cv->contacts }) {
 
 635     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
 
 638   $_[0]->render('common/show_vc_details', { layout => 0 },
 
 639                 is_customer => $is_customer,
 
 644 # called if a unit in an existing item row is changed
 
 645 sub action_unit_changed {
 
 648   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 649   my $item = $self->order->items_sorted->[$idx];
 
 651   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
 
 652   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
 
 655     ->run('kivi.DeliveryOrder.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
 
 656   $self->js_redisplay_line_values;
 
 660 # add an item row for a new item entered in the input row
 
 661 sub action_add_item {
 
 664   delete $::form->{add_item}->{create_part_type};
 
 666   my $form_attr = $::form->{add_item};
 
 668   return unless $form_attr->{parts_id};
 
 670   my $item = new_item($self->order, $form_attr);
 
 672   $self->order->add_items($item);
 
 674   $self->get_item_cvpartnumber($item);
 
 676   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 677   my $row_as_html = $self->p->render('delivery_order/tabs/_row',
 
 681                                      in_out => $self->type_data->transfer,
 
 684   if ($::form->{insert_before_item_id}) {
 
 686       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 689       ->append('#row_table_id', $row_as_html);
 
 692   if ( $item->part->is_assortment ) {
 
 693     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
 
 694     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 695       my $attr = { parts_id => $assortment_item->parts_id,
 
 696                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
 
 697                    unit     => $assortment_item->unit,
 
 698                    description => $assortment_item->part->description,
 
 700       my $item = new_item($self->order, $attr);
 
 702       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 703       $item->discount(1) unless $assortment_item->charge;
 
 705       $self->order->add_items( $item );
 
 706       $self->get_item_cvpartnumber($item);
 
 707       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 708       my $row_as_html = $self->p->render('delivery_order/tabs/_row',
 
 713       if ($::form->{insert_before_item_id}) {
 
 715           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 718           ->append('#row_table_id', $row_as_html);
 
 724     ->val('.add_item_input', '')
 
 725     ->run('kivi.DeliveryOrder.init_row_handlers')
 
 726     ->run('kivi.DeliveryOrder.renumber_positions')
 
 727     ->focus('#add_item_parts_id_name');
 
 729   $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 734 # add item rows for multiple items at once
 
 735 sub action_add_multi_items {
 
 738   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
 
 739   return $self->js->render() unless scalar @form_attr;
 
 742   foreach my $attr (@form_attr) {
 
 743     my $item = new_item($self->order, $attr);
 
 745     if ( $item->part->is_assortment ) {
 
 746       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
 
 747         my $attr = { parts_id => $assortment_item->parts_id,
 
 748                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
 
 749                      unit     => $assortment_item->unit,
 
 750                      description => $assortment_item->part->description,
 
 752         my $item = new_item($self->order, $attr);
 
 754         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
 
 755         $item->discount(1) unless $assortment_item->charge;
 
 760   $self->order->add_items(@items);
 
 762   foreach my $item (@items) {
 
 763     $self->get_item_cvpartnumber($item);
 
 764     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 765     my $row_as_html = $self->p->render('delivery_order/tabs/_row',
 
 769                                        in_out => $self->type_data->transfer,
 
 772     if ($::form->{insert_before_item_id}) {
 
 774         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
 
 777         ->append('#row_table_id', $row_as_html);
 
 782     ->run('kivi.Part.close_picker_dialogs')
 
 783     ->run('kivi.DeliveryOrder.init_row_handlers')
 
 784     ->run('kivi.DeliveryOrder.renumber_positions')
 
 785     ->focus('#add_item_parts_id_name');
 
 787   $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
 
 792 sub action_update_exchangerate {
 
 796     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
 
 797     currency_name => $self->order->currency->name,
 
 800   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 
 803 # redisplay item rows if they are sorted by an attribute
 
 804 sub action_reorder_items {
 
 808     partnumber   => sub { $_[0]->part->partnumber },
 
 809     description  => sub { $_[0]->description },
 
 810     qty          => sub { $_[0]->qty },
 
 811     sellprice    => sub { $_[0]->sellprice },
 
 812     discount     => sub { $_[0]->discount },
 
 813     cvpartnumber => sub { $_[0]->{cvpartnumber} },
 
 816   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
 818   my $method = $sort_keys{$::form->{order_by}};
 
 819   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
 
 820   if ($::form->{sort_dir}) {
 
 821     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 822       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
 
 824       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
 
 827     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
 
 828       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
 
 830       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
 
 834     ->run('kivi.DeliveryOrder.redisplay_items', \@to_sort)
 
 838 # show the popup to choose a price/discount source
 
 839 sub action_price_popup {
 
 842   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
 
 843   my $item = $self->order->items_sorted->[$idx];
 
 845   $self->render_price_dialog($item);
 
 848 # save the order in a session variable and redirect to the part controller
 
 849 sub action_create_part {
 
 852   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
 
 854   my $callback     = $self->url_for(
 
 855     action       => 'return_from_create_part',
 
 856     type         => $self->type, # type is needed for check_auth on return
 
 857     previousform => $previousform,
 
 860   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.'));
 
 862   my @redirect_params = (
 
 863     controller => 'Part',
 
 865     part_type  => $::form->{add_item}->{create_part_type},
 
 866     callback   => $callback,
 
 870   $self->redirect_to(@redirect_params);
 
 873 sub action_return_from_create_part {
 
 876   $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
 
 878   $::auth->restore_form_from_session(delete $::form->{previousform});
 
 880   # set item ids to new fake id, to identify them as new items
 
 881   foreach my $item (@{$self->order->items_sorted}) {
 
 882     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
 885   $self->get_unalterable_data();
 
 888   # trigger rendering values for second row/longdescription as hidden,
 
 889   # because they are loaded only on demand. So we need to keep the values
 
 891   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
 
 892   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
 
 895     'delivery_order/form',
 
 896     title => $self->get_title_for('edit'),
 
 897     %{$self->{template_args}}
 
 902 sub action_stock_in_out_dialog {
 
 905   my $part    = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
 
 906   my $unit    = SL::DB::Unit->load_cached($::form->{unit}) or die "need unit";
 
 907   my $stock   = $::form->{stock};
 
 908   my $row     = $::form->{row};
 
 909   my $item_id = $::form->{item_id};
 
 910   my $qty     = _parse_number($::form->{qty_as_number});
 
 912   my $inout = $self->type_data->transfer;
 
 914   my @contents   = DO->get_item_availability(parts_id => $part->id);
 
 915   my $stock_info = DO->unpack_stock_information(packed => $stock);
 
 917   $self->merge_stock_data($stock_info, \@contents, $part, $unit);
 
 919   $self->render("delivery_order/stock_dialog", { layout => 0 },
 
 920     WHCONTENTS => $self->order->delivered ? $stock_info : \@contents,
 
 923     do_unit    => $unit->unit,
 
 924     delivered  => $self->order->delivered,
 
 930 sub action_update_stock_information {
 
 933   my $stock_info = $::form->{stock_info};
 
 934   my $unit = $::form->{unit};
 
 935   my $yaml = SL::YAML::Dump($stock_info);
 
 936   my $stock_qty = $self->calculate_stock_in_out_from_stock_info($unit, $stock_info);
 
 940     stock_qty => $stock_qty,
 
 942   $self->render(\ SL::JSON::to_json($response), { layout => 0, type => 'json', process => 0 });
 
 945 sub merge_stock_data {
 
 946   my ($self, $stock_info, $contents, $part, $unit) = @_;
 
 947   # TODO rewrite to mapping
 
 949   if (!$self->order->delivered) {
 
 950     for my $row (@$contents) {
 
 951       # row here is in parts units. stock is in item units
 
 952       $row->{available_qty} = _format_number($part->unit_obj->convert_to($row->{qty}, $unit));
 
 954       for my $sinfo (@{ $stock_info }) {
 
 955         next if $row->{bin_id}       != $sinfo->{bin_id} ||
 
 956                 $row->{warehouse_id} != $sinfo->{warehouse_id} ||
 
 957                 $row->{chargenumber} ne $sinfo->{chargenumber} ||
 
 958                 $row->{bestbefore}   ne $sinfo->{bestbefore};
 
 960         $row->{"stock_$_"} = $sinfo->{$_}
 
 961           for qw(qty unit error delivery_order_items_stock_id);
 
 966     for my $sinfo (@{ $stock_info }) {
 
 967       my $bin = SL::DB::Bin->load_cached($sinfo->{bin_id});
 
 968       $sinfo->{warehousedescription} = $bin->warehouse->description;
 
 969       $sinfo->{bindescription}       = $bin->description;
 
 970       map { $sinfo->{"stock_$_"}      = $sinfo->{$_} } qw(qty unit);
 
 975 # load the second row for one or more items
 
 977 # This action gets the html code for all items second rows by rendering a template for
 
 978 # the second row and sets the html code via client js.
 
 979 sub action_load_second_rows {
 
 982   foreach my $item_id (@{ $::form->{item_ids} }) {
 
 983     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
 984     my $item = $self->order->items_sorted->[$idx];
 
 986     $self->js_load_second_row($item, $item_id, 0);
 
 989   $self->js->run('kivi.DeliveryOrder.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
 
 994 # update description, notes and sellprice from master data
 
 995 sub action_update_row_from_master_data {
 
 998   foreach my $item_id (@{ $::form->{item_ids} }) {
 
 999     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
 
1000     my $item  = $self->order->items_sorted->[$idx];
 
1001     my $texts = get_part_texts($item->part, $self->order->language_id);
 
1003     $item->description($texts->{description});
 
1004     $item->longdescription($texts->{longdescription});
 
1006     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1009     if ($item->part->is_assortment) {
 
1010     # add assortment items with price 0, as the components carry the price
 
1011       $price_src = $price_source->price_from_source("");
 
1012       $price_src->price(0);
 
1014       $price_src = $price_source->best_price
 
1015                  ? $price_source->best_price
 
1016                  : $price_source->price_from_source("");
 
1017       $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
 
1018       $price_src->price(0) if !$price_source->best_price;
 
1022     $item->sellprice($price_src->price);
 
1023     $item->active_price_source($price_src);
 
1026       ->run('kivi.DeliveryOrder.update_sellprice', $item_id, $item->sellprice_as_number)
 
1027       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
 
1028       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
 
1029       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
 
1031     if ($self->search_cvpartnumber) {
 
1032       $self->get_item_cvpartnumber($item);
 
1033       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
 
1037   $self->js_redisplay_line_values;
 
1039   $self->js->render();
 
1042 sub action_transfer_stock {
 
1045   if ($self->order->delivered) {
 
1046     return $self->js->flash("error", t8('The parts for this order have already been transferred'))->render;
 
1049   my $inout = $self->type_data->properties('transfer');
 
1051   my $errors = $self->save;
 
1054     $self->js->flash('error', $_) for @$errors;
 
1055     return $self->js->render;
 
1058   my $order = $self->order;
 
1060   # TODO move to type data
 
1061   my $trans_type = $inout eq 'in'
 
1062     ? SL::DB::Manager::TransferType->find_by(direction => "id", description => "stock")
 
1063     : SL::DB::Manager::TransferType->find_by(direction => "out", description => "shipped");
 
1065   my @transfer_requests;
 
1067   for my $item (@{ $order->items_sorted }) {
 
1068     for my $stock (@{ $item->delivery_order_stock_entries }) {
 
1069       my $transfer = SL::DB::Inventory->new_from($stock);
 
1070       $transfer->trans_type($trans_type);
 
1071       $transfer->qty($transfer->qty * -1) if $inout eq 'out';
 
1073       push @transfer_requests, $transfer if defined $transfer->qty && $transfer->qty != 0;
 
1077   if (!@transfer_requests) {
 
1078     return $self->js->flash("error", t8("No stock to transfer"))->render;
 
1081   SL::DB->client->with_transaction(sub {
 
1082     $_->save for @transfer_requests;
 
1083     $self->order->update_attributes(delivered => 1);
 
1087     ->flash("info", t8("Stock transfered"))
 
1088     ->run('kivi.ActionBar.setDisabled', '#transfer_out_action', t8('The parts for this order have already been transferred'))
 
1089     ->run('kivi.ActionBar.setDisabled', '#transfer_in_action', t8('The parts for this order have already been transferred'))
 
1090     ->run('kivi.ActionBar.setDisabled', '#delete_action', t8('The parts for this order have already been transferred'))
 
1091     ->replaceWith('#data-status-line', delivery_order_status_line($self->order))
 
1096 sub js_load_second_row {
 
1097   my ($self, $item, $item_id, $do_parse) = @_;
 
1100     # Parse values from form (they are formated while rendering (template)).
 
1101     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1102     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
 
1103     foreach my $var (@{ $item->cvars_by_config }) {
 
1104       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1106     $item->parse_custom_variable_values;
 
1109   my $row_as_html = $self->p->render('delivery_order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
 
1112     ->html('#second_row_' . $item_id, $row_as_html)
 
1113     ->data('#second_row_' . $item_id, 'loaded', 1);
 
1116 sub js_redisplay_line_values {
 
1119   my $is_sales = $self->order->is_sales;
 
1121   # sales orders with margins
 
1126        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1127        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
 
1128        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
 
1129       ]} @{ $self->order->items_sorted };
 
1133        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
 
1134       ]} @{ $self->order->items_sorted };
 
1138     ->run('kivi.DeliveryOrder.redisplay_line_values', $is_sales, \@data);
 
1141 sub js_redisplay_cvpartnumbers {
 
1144   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1146   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
 
1149     ->run('kivi.DeliveryOrder.redisplay_cvpartnumbers', \@data);
 
1152 sub js_reset_order_and_item_ids_after_save {
 
1156     ->val('#id', $self->order->id)
 
1157     ->val('#converted_from_oe_id', '')
 
1158     ->val('#order_' . $self->nr_key(), $self->order->number);
 
1161   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
 
1162     next if !$self->order->items_sorted->[$idx]->id;
 
1163     next if $form_item_id !~ m{^new};
 
1165       ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
 
1166       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
 
1167       ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
 
1171   $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
 
1181   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
 
1182     die "Not a valid type for delivery order";
 
1185   $self->type($::form->{type});
 
1191   return $self->type_data->customervendor;
 
1194 sub init_search_cvpartnumber {
 
1197   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
 
1198   my $search_cvpartnumber;
 
1199   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
 
1200   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
 
1202   return $search_cvpartnumber;
 
1205 sub init_show_update_button {
 
1208   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 
1219 sub init_all_price_factors {
 
1220   SL::DB::Manager::PriceFactor->get_all;
 
1223 sub init_part_picker_classification_ids {
 
1226   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => $self->type_data->part_classification_query) } ];
 
1232   $::auth->assert($self->type_data->access('view') || 'DOES_NOT_EXIST');
 
1235 sub check_auth_for_edit {
 
1238   $::auth->assert($self->type_data->access('edit') || 'DOES_NOT_EXIST');
 
1241 # build the selection box for contacts
 
1243 # Needed, if customer/vendor changed.
 
1244 sub build_contact_select {
 
1247   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
 
1248     value_key  => 'cp_id',
 
1249     title_key  => 'full_name_dep',
 
1250     default    => $self->order->cp_id,
 
1252     style      => 'width: 300px',
 
1256 # build the selection box for shiptos
 
1258 # Needed, if customer/vendor changed.
 
1259 sub build_shipto_select {
 
1262   select_tag('order.shipto_id',
 
1263              [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
 
1264              value_key  => 'shipto_id',
 
1265              title_key  => 'displayable_id',
 
1266              default    => $self->order->shipto_id,
 
1268              style      => 'width: 300px',
 
1272 # build the inputs for the cusom shipto dialog
 
1274 # Needed, if customer/vendor changed.
 
1275 sub build_shipto_inputs {
 
1278   my $content = $self->p->render('common/_ship_to_dialog',
 
1279                                  vc_obj      => $self->order->customervendor,
 
1280                                  cs_obj      => $self->order->custom_shipto,
 
1281                                  cvars       => $self->order->custom_shipto->cvars_by_config,
 
1282                                  id_selector => '#order_shipto_id');
 
1284   div_tag($content, id => 'shipto_inputs');
 
1287 # render the info line for business
 
1289 # Needed, if customer/vendor changed.
 
1290 sub build_business_info_row
 
1292   $_[0]->p->render('delivery_order/tabs/_business_info_row', SELF => $_[0]);
 
1299   return if !$::form->{id};
 
1301   $self->order(SL::DB::DeliveryOrder->new(id => $::form->{id})->load);
 
1303   # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
 
1304   # You need a custom shipto object to call cvars_by_config to get the cvars.
 
1305   $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
 
1307   $self->prepare_stock_info($_) for $self->order->items;
 
1309   return $self->order;
 
1312 # load or create a new order object
 
1314 # And assign changes from the form to this object.
 
1315 # If the order is loaded from db, check if items are deleted in the form,
 
1316 # remove them form the object and collect them for removing from db on saving.
 
1317 # Then create/update items from form (via make_item) and add them.
 
1321   # add_items adds items to an order with no items for saving, but they cannot
 
1322   # be retrieved via items until the order is saved. Adding empty items to new
 
1323   # order here solves this problem.
 
1325   $order   = SL::DB::DeliveryOrder->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
 
1326   $order ||= SL::DB::DeliveryOrder->new(orderitems  => [], currency_id => $::instance_conf->get_currency_id(), order_type => $self->type_data->validate($::form->{type}));
 
1328   my $cv_id_method = $self->cv . '_id';
 
1329   if (!$::form->{id} && $::form->{$cv_id_method}) {
 
1330     $order->$cv_id_method($::form->{$cv_id_method});
 
1331     setup_order_from_cv($order);
 
1334   my $form_orderitems                  = delete $::form->{order}->{orderitems};
 
1336   $order->assign_attributes(%{$::form->{order}});
 
1338   $self->setup_custom_shipto_from_form($order, $::form);
 
1340   # remove deleted items
 
1341   $self->item_ids_to_delete([]);
 
1342   foreach my $idx (reverse 0..$#{$order->orderitems}) {
 
1343     my $item = $order->orderitems->[$idx];
 
1344     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
 
1345       splice @{$order->orderitems}, $idx, 1;
 
1346       push @{$self->item_ids_to_delete}, $item->id;
 
1352   foreach my $form_attr (@{$form_orderitems}) {
 
1353     my $item = make_item($order, $form_attr);
 
1354     $item->position($pos);
 
1359   $self->prepare_stock_info($_) for $order->items, @items;
 
1361   $order->add_items(grep {!$_->id} @items);
 
1366 # create or update items from form
 
1368 # Make item objects from form values. For items already existing read from db.
 
1369 # Create a new item else. And assign attributes.
 
1371   my ($record, $attr) = @_;
 
1374   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
 
1376   my $is_new = !$item;
 
1378   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1379   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1380   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1381   $item ||= SL::DB::DeliveryOrderItem->new(custom_variables => []);
 
1384   if (my $stock_info = delete $attr->{stock_info}) {
 
1385     my %existing = map { $_->id => $_ } $item->delivery_order_stock_entries;
 
1388     for my $line (@{ DO->unpack_stock_information(packed => $stock_info) }) {
 
1389       # lookup existing or make new
 
1390       my $obj = delete $existing{$line->{delivery_order_items_stock_id}}
 
1391              // SL::DB::DeliveryOrderItemsStock->new;
 
1394       $obj->$_($line->{$_}) for qw(bin_id warehouse_id chargenumber qty unit);
 
1395       $obj->bestbefore_as_date($line->{bestfbefore})
 
1396         if $line->{bestbefore} && $::instance_conf->get_show_bestbefore;
 
1397       push @save, $obj if $obj->qty;
 
1400     $item->delivery_order_stock_entries(@save);
 
1403   $item->assign_attributes(%$attr);
 
1406     my $texts = get_part_texts($item->part, $record->language_id);
 
1407     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
 
1408     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
 
1409     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
 
1417 # This is used to add one item
 
1419   my ($record, $attr) = @_;
 
1421   my $item = SL::DB::DeliveryOrderItem->new;
 
1423   # Remove attributes where the user left or set the inputs empty.
 
1424   # So these attributes will be undefined and we can distinguish them
 
1425   # from zero later on.
 
1426   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
 
1427     delete $attr->{$_} if $attr->{$_} eq '';
 
1430   $item->assign_attributes(%$attr);
 
1432   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
 
1433   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
 
1435   $item->unit($part->unit) if !$item->unit;
 
1438   if ( $part->is_assortment ) {
 
1439     # add assortment items with price 0, as the components carry the price
 
1440     $price_src = $price_source->price_from_source("");
 
1441     $price_src->price(0);
 
1442   } elsif (defined $item->sellprice) {
 
1443     $price_src = $price_source->price_from_source("");
 
1444     $price_src->price($item->sellprice);
 
1446     $price_src = $price_source->best_price
 
1447                ? $price_source->best_price
 
1448                : $price_source->price_from_source("");
 
1449     $price_src->price(0) if !$price_source->best_price;
 
1453   if (defined $item->discount) {
 
1454     $discount_src = $price_source->discount_from_source("");
 
1455     $discount_src->discount($item->discount);
 
1457     $discount_src = $price_source->best_discount
 
1458                   ? $price_source->best_discount
 
1459                   : $price_source->discount_from_source("");
 
1460     $discount_src->discount(0) if !$price_source->best_discount;
 
1464   $new_attr{part}                   = $part;
 
1465   $new_attr{description}            = $part->description     if ! $item->description;
 
1466   $new_attr{qty}                    = 1.0                    if ! $item->qty;
 
1467   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
 
1468   $new_attr{sellprice}              = $price_src->price;
 
1469   $new_attr{discount}               = $discount_src->discount;
 
1470   $new_attr{active_price_source}    = $price_src;
 
1471   $new_attr{active_discount_source} = $discount_src;
 
1472   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
 
1473   $new_attr{project_id}             = $record->globalproject_id;
 
1474   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
 
1476   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
 
1477   # they cannot be retrieved via custom_variables until the order/orderitem is
 
1478   # saved. Adding empty custom_variables to new orderitem here solves this problem.
 
1479   $new_attr{custom_variables} = [];
 
1481   my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
1483   $item->assign_attributes(%new_attr, %{ $texts });
 
1488 sub prepare_stock_info {
 
1489   my ($self, $item) = @_;
 
1491   $item->{stock_info} = SL::YAML::Dump([
 
1493       delivery_order_items_stock_id => $_->id,
 
1495       warehouse_id                  => $_->warehouse_id,
 
1496       bin_id                        => $_->bin_id,
 
1497       chargenumber                  => $_->chargenumber,
 
1499     }, $item->delivery_order_stock_entries
 
1503 sub setup_order_from_cv {
 
1506   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
 
1508   $order->intnotes($order->customervendor->notes);
 
1510   if ($order->is_sales) {
 
1511     $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
 
1512     $order->taxincluded(defined($order->customer->taxincluded_checked)
 
1513                         ? $order->customer->taxincluded_checked
 
1514                         : $::myconfig{taxincluded_checked});
 
1519 # setup custom shipto from form
 
1521 # The dialog returns form variables starting with 'shipto' and cvars starting
 
1522 # with 'shiptocvar_'.
 
1523 # Mark it to be deleted if a shipto from master data is selected
 
1524 # (i.e. order has a shipto).
 
1525 # Else, update or create a new custom shipto. If the fields are empty, it
 
1526 # will not be saved on save.
 
1527 sub setup_custom_shipto_from_form {
 
1528   my ($self, $order, $form) = @_;
 
1530   if ($order->shipto) {
 
1531     $self->is_custom_shipto_to_delete(1);
 
1533     my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1535     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
 
1536     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
 
1538     $custom_shipto->assign_attributes(%$shipto_attrs);
 
1539     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
 
1543 # get data for saving, printing, ..., that is not changed in the form
 
1545 # Only cvars for now.
 
1546 sub get_unalterable_data {
 
1549   foreach my $item (@{ $self->order->items }) {
 
1550     # autovivify all cvars that are not in the form (cvars_by_config can do it).
 
1551     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
 
1552     foreach my $var (@{ $item->cvars_by_config }) {
 
1553       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
 
1555     $item->parse_custom_variable_values;
 
1561 # And remove related files in the spool directory
 
1566   my $db     = $self->order->db;
 
1568   $db->with_transaction(
 
1570       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
 
1571       $self->order->delete;
 
1572       my $spool = $::lx_office_conf{paths}->{spool};
 
1573       unlink map { "$spool/$_" } @spoolfiles if $spool;
 
1575       $self->save_history('DELETED');
 
1578   }) || push(@{$errors}, $db->error);
 
1585 # And delete items that are deleted in the form.
 
1590   my $db     = $self->order->db;
 
1592   $db->with_transaction(sub {
 
1593     # delete custom shipto if it is to be deleted or if it is empty
 
1594     if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
 
1595       $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
 
1596       $self->order->custom_shipto(undef);
 
1599     SL::DB::DeliveryOrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
 
1600     $self->order->save(cascade => 1);
 
1603     if ($::form->{converted_from_oe_id}) {
 
1604       my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
 
1605       foreach my $converted_from_oe_id (@converted_from_oe_ids) {
 
1606         my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
 
1607         $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/ && $self->order->is_type(PURCHASE_DELIVERY_ORDER_TYPE);
 
1608         $src->link_to_record($self->order);
 
1610       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
 
1612         foreach (@{ $self->order->items_sorted }) {
 
1613           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
 
1615           SL::DB::RecordLink->new(from_table => 'orderitems',
 
1616                                   from_id    => $from_id,
 
1617                                   to_table   => 'orderitems',
 
1625     $self->save_history('SAVED');
 
1628   }) || push(@{$errors}, $db->error);
 
1633 sub workflow_sales_or_request_for_quotation {
 
1637   my $errors = $self->save();
 
1639   if (scalar @{ $errors }) {
 
1640     $self->js->flash('error', $_) for @{ $errors };
 
1641     return $self->js->render();
 
1644   my $destination_type = $self->type_data->workflow("to_quotation_type");
 
1646   $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
 
1647   $self->{converted_from_oe_id} = delete $::form->{id};
 
1649   # set item ids to new fake id, to identify them as new items
 
1650   foreach my $item (@{$self->order->items_sorted}) {
 
1651     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1655   $::form->{type} = $destination_type;
 
1656   $self->type($self->init_type);
 
1657   $self->cv  ($self->init_cv);
 
1660   $self->get_unalterable_data();
 
1661   $self->pre_render();
 
1663   # trigger rendering values for second row as hidden, because they
 
1664   # are loaded only on demand. So we need to keep the values from the
 
1666   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1669     'delivery_order/form',
 
1670     title => $self->get_title_for('edit'),
 
1671     %{$self->{template_args}}
 
1675 sub workflow_sales_or_purchase_order {
 
1679   my $errors = $self->save();
 
1681   if (scalar @{ $errors }) {
 
1682     $self->js->flash('error', $_) foreach @{ $errors };
 
1683     return $self->js->render();
 
1686   my $destination_type = $self->type_data->workflow("to_order_type");
 
1688   # check for direct delivery
 
1689   # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
 
1691   if ($self->type_data->workflow("to_order_copy_shipto") && $::form->{use_shipto} && $self->order->shipto) {
 
1692     $custom_shipto = $self->order->shipto->clone('SL::DB::DeliveryOrder');
 
1695   $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
 
1696   $self->{converted_from_oe_id} = delete $::form->{id};
 
1698   # set item ids to new fake id, to identify them as new items
 
1699   foreach my $item (@{$self->order->items_sorted}) {
 
1700     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
 
1703   if ($self->type_data->workflow("to_order_copy_shipto")) {
 
1704     if ($::form->{use_shipto}) {
 
1705       $self->order->custom_shipto($custom_shipto) if $custom_shipto;
 
1707       # remove any custom shipto if not wanted
 
1708       $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
 
1713   $::form->{type} = $destination_type;
 
1714   $self->type($self->init_type);
 
1715   $self->cv  ($self->init_cv);
 
1718   $self->get_unalterable_data();
 
1719   $self->pre_render();
 
1721   # trigger rendering values for second row as hidden, because they
 
1722   # are loaded only on demand. So we need to keep the values from the
 
1724   $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
 
1727     'delivery_order/form',
 
1728     title => $self->get_title_for('edit'),
 
1729     %{$self->{template_args}}
 
1736   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
 
1737   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
 
1738   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
 
1739   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
 
1740   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
 
1743   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
 
1746   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
 
1748   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
1749   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
 
1750   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
1751   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
 
1753   my $print_form = Form->new('');
 
1754   $print_form->{type}        = $self->type;
 
1755   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
 
1756   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
 
1757     form => $print_form,
 
1758     options => {dialog_name_prefix => 'print_options.',
 
1762                 no_opendocument    => 0,
 
1766   foreach my $item (@{$self->order->orderitems}) {
 
1767     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
 
1768     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
 
1769     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
 
1772   if ($self->order->${\ $self->type_data->nr_key } && $::instance_conf->get_webdav) {
 
1773     my $webdav = SL::Webdav->new(
 
1774       type     => $self->type,
 
1775       number   => $self->order->number,
 
1777     my @all_objects = $webdav->get_all_objects;
 
1778     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
 
1780                                                     link => File::Spec->catfile($_->full_filedescriptor),
 
1784   $self->{template_args}{in_out} = $self->type_data->transfer;
 
1786   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
1788   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.DeliveryOrder kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
 
1789                                                          calculate_qty kivi.Validator follow_up show_history);
 
1790   $self->setup_edit_action_bar;
 
1793 sub setup_edit_action_bar {
 
1794   my ($self, %params) = @_;
 
1796   my $deletion_allowed = $self->type_data->show_menu("delete");
 
1797   my $may_edit_create  = $::auth->assert($self->type_data->access('edit') || 'DOES_NOT_EXIST', 1);
 
1799   for my $bar ($::request->layout->get('actionbar')) {
 
1804           call     => [ 'kivi.DeliveryOrder.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
 
1805                                                            $::instance_conf->get_order_warn_no_deliverydate,
 
1807           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1811           call     => [ 'kivi.DeliveryOrder.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
 
1812           disabled => !$may_edit_create                        ? t8('You do not have the permissions to access this function.')
 
1813                     : $self->type eq 'supplier_delivery_order' ? t8('Need a workflow for Supplier Delivery Order')
 
1814                     : !$self->order->id                        ? t8('This object has not been saved yet.')
 
1817       ], # end of combobox "Save"
 
1824           t8('Save and Quotation'),
 
1825           submit   => [ '#order_form', { action => "DeliveryOrder/sales_quotation" } ],
 
1826           only_if  => $self->type_data->show_menu("save_and_quotation"),
 
1827           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1831           submit   => [ '#order_form', { action => "DeliveryOrder/request_for_quotation" } ],
 
1832           only_if  => $self->type_data->show_menu("save_and_rfq"),
 
1833           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1836           t8('Save and Sales Order'),
 
1837           submit   => [ '#order_form', { action => "DeliveryOrder/sales_order" } ],
 
1838           only_if  => $self->type_data->show_menu("save_and_sales_order"),
 
1839           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1842           t8('Save and Purchase Order'),
 
1843           call     => [ 'kivi.DeliveryOrder.purchase_order_check_for_direct_delivery' ],
 
1844           only_if  => $self->type_data->show_menu("save_and_purchase_order"),
 
1845           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1848           t8('Save and Delivery Order'),
 
1849           call     => [ 'kivi.DeliveryOrder.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
 
1850                                                                               $::instance_conf->get_order_warn_no_deliverydate,
 
1852           only_if  => $self->type_data->show_menu("save_and_delivery_order"),
 
1853           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1856           t8('Save and Invoice'),
 
1857           call     => [ 'kivi.DeliveryOrder.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
 
1858           only_if  => $self->type_data->show_menu("save_and_invoice"),
 
1859           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1862           t8('Save and AP Transaction'),
 
1863           call     => [ 'kivi.DeliveryOrder.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
 
1864           only_if  => $self->type_data->show_menu("save_and_ap_transaction"),
 
1865           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1868       ], # end of combobox "Workflow"
 
1875           t8('Save and preview PDF'),
 
1876            call    => [ 'kivi.DeliveryOrder.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
 
1877                                                                   $::instance_conf->get_order_warn_no_deliverydate,
 
1879           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1882           t8('Save and print'),
 
1883           call     => [ 'kivi.DeliveryOrder.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
 
1884                                                                  $::instance_conf->get_order_warn_no_deliverydate,
 
1886           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
 
1889           t8('Save and E-mail'),
 
1890           id       => 'save_and_email_action',
 
1891           call     => [ 'kivi.DeliveryOrder.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
 
1892                                                                                  $::instance_conf->get_order_warn_no_deliverydate,
 
1894           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
 
1895                     : !$self->order->id ? t8('This object has not been saved yet.')
 
1899           t8('Download attachments of all parts'),
 
1900           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
 
1901           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
 
1902                     : !$self->order->id ? t8('This object has not been saved yet.')
 
1904           only_if  => $::instance_conf->get_doc_storage,
 
1906       ], # end of combobox "Export"
 
1910         id       => 'delete_action',
 
1911         call     => [ 'kivi.DeliveryOrder.delete_order' ],
 
1912         confirm  => $::locale->text('Do you really want to delete this object?'),
 
1913         disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
 
1914                   : !$self->order->id       ? t8('This object has not been saved yet.')
 
1915                   : $self->order->delivered ? t8('The parts for this order have already been transferred')
 
1917         only_if  => $self->type_data->show_menu("delete"),
 
1923           id       => 'transfer_out_action',
 
1924           call     => [ 'kivi.DeliveryOrder.save', 'transfer_stock' ],
 
1925           disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
 
1926                     : !$self->order->id       ? t8('This object has not been saved yet.')
 
1927                     : $self->order->delivered ? t8('The parts for this order have already been transferred')
 
1929           only_if  => $self->type_data->properties('transfer') eq 'out',
 
1930           confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
 
1934           id       => 'transfer_in_action',
 
1935           call     => [ 'kivi.DeliveryOrder.save', 'transfer_stock' ],
 
1936           disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
 
1937                     : !$self->order->id       ? t8('This object has not been saved yet.')
 
1938                     : $self->order->delivered ? t8('The parts for this order have already been transferred')
 
1940           only_if  => $self->type_data->properties('transfer') eq 'in',
 
1941           confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
 
1951           call     => [ 'kivi.DeliveryOrder.follow_up_window' ],
 
1952           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
 
1953           only_if  => $::auth->assert('productivity', 1),
 
1957           call     => [ 'set_history_window', $self->order->id, 'id' ],
 
1958           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
 
1960       ], # end of combobox "more"
 
1966   my ($order, $pdf_ref, $params) = @_;
 
1970   my $print_form = Form->new('');
 
1971   $print_form->{type}        = $order->type;
 
1972   $print_form->{formname}    = $params->{formname} || $order->type;
 
1973   $print_form->{format}      = $params->{format}   || 'pdf';
 
1974   $print_form->{media}       = $params->{media}    || 'file';
 
1975   $print_form->{groupitems}  = $params->{groupitems};
 
1976   $print_form->{printer_id}  = $params->{printer_id};
 
1977   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
 
1979   $order->language($params->{language});
 
1980   $order->flatten_to_form($print_form, format_amounts => 1);
 
1984   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
 
1985     $template_ext  = 'odt';
 
1986     $template_type = 'OpenDocument';
 
1989   # search for the template
 
1990   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
 
1991     name        => $print_form->{formname},
 
1992     extension   => $template_ext,
 
1993     email       => $print_form->{media} eq 'email',
 
1994     language    => $params->{language},
 
1995     printer_id  => $print_form->{printer_id},
 
1998   if (!defined $template_file) {
 
1999     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);
 
2002   return @errors if scalar @errors;
 
2004   $print_form->throw_on_error(sub {
 
2006       $print_form->prepare_for_printing;
 
2008       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
 
2009         format        => $print_form->{format},
 
2010         template_type => $template_type,
 
2011         template      => $template_file,
 
2012         variables     => $print_form,
 
2013         variable_content_types => {
 
2014           longdescription => 'html',
 
2015           partnotes       => 'html',
 
2020     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
 
2026 sub get_files_for_email_dialog {
 
2029   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
 
2031   return %files if !$::instance_conf->get_doc_storage;
 
2033   if ($self->order->id) {
 
2034     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
 
2035     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
 
2036     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
 
2037     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
 
2041     uniq_by { $_->{id} }
 
2043       +{ id         => $_->part->id,
 
2044          partnumber => $_->part->partnumber }
 
2045     } @{$self->order->items_sorted};
 
2047   foreach my $part (@parts) {
 
2048     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
 
2049     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
 
2052   foreach my $key (keys %files) {
 
2053     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
 
2060   my ($self, $action) = @_;
 
2062   return '' if none { lc($action)} qw(add edit);
 
2063   return $self->type_data->text($action);
 
2066 sub get_item_cvpartnumber {
 
2067   my ($self, $item) = @_;
 
2069   return if !$self->search_cvpartnumber;
 
2070   return if !$self->order->customervendor;
 
2072   if ($self->cv eq 'vendor') {
 
2073     my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
 
2074     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
 
2075   } elsif ($self->cv eq 'customer') {
 
2076     my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
 
2077     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
 
2081 sub get_part_texts {
 
2082   my ($part_or_id, $language_or_id, %defaults) = @_;
 
2084   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
 
2085   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
 
2087     description     => $defaults{description}     // $part->description,
 
2088     longdescription => $defaults{longdescription} // $part->notes,
 
2091   return $texts unless $language_id;
 
2093   my $translation = SL::DB::Manager::Translation->get_first(
 
2095       parts_id    => $part->id,
 
2096       language_id => $language_id,
 
2099   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
 
2100   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
 
2106   return $_[0]->type_data->nr_key;
 
2109 sub save_and_redirect_to {
 
2110   my ($self, %params) = @_;
 
2112   my $errors = $self->save();
 
2114   if (scalar @{ $errors }) {
 
2115     $self->js->flash('error', $_) foreach @{ $errors };
 
2116     return $self->js->render();
 
2119   flash_later('info', $self->type_data->text("saved"));
 
2121   $self->redirect_to(%params, id => $self->order->id);
 
2125   my ($self, $addition) = @_;
 
2127   my $number_type = $self->nr_key;
 
2128   my $snumbers    = $number_type . '_' . $self->order->$number_type;
 
2130   SL::DB::History->new(
 
2131     trans_id    => $self->order->id,
 
2132     employee_id => SL::DB::Manager::Employee->current->id,
 
2133     what_done   => $self->order->type,
 
2134     snumbers    => $snumbers,
 
2135     addition    => $addition,
 
2139 sub store_pdf_to_webdav_and_filemanagement {
 
2140   my($order, $content, $filename) = @_;
 
2144   # copy file to webdav folder
 
2145   if ($order->number && $::instance_conf->get_webdav_documents) {
 
2146     my $webdav = SL::Webdav->new(
 
2147       type     => $order->type,
 
2148       number   => $order->number,
 
2150     my $webdav_file = SL::Webdav::File->new(
 
2152       filename => $filename,
 
2155       $webdav_file->store(data => \$content);
 
2158       push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
 
2161   if ($order->id && $::instance_conf->get_doc_storage) {
 
2163       SL::File->save(object_id     => $order->id,
 
2164                      object_type   => $order->type,
 
2165                      mime_type     => 'application/pdf',
 
2166                      source        => 'created',
 
2167                      file_type     => 'document',
 
2168                      file_name     => $filename,
 
2169                      file_contents => $content);
 
2172       push @errors, t8('Storing PDF in storage backend failed: #1', $@);
 
2179 sub calculate_stock_in_out_from_stock_info {
 
2180   my ($self, $unit, $stock_info) = @_;
 
2182   return "" if !$unit;
 
2184   my %units_by_name = map { $_->name => $_ } @{ SL::DB::Manager::Unit->get_all };
 
2186   my $sum      = sum0 map {
 
2187     $units_by_name{$_->{unit}}->convert_to($_->{qty}, $units_by_name{$unit})
 
2190   my $content  = _format_number($sum, 2) . ' ' . $unit;
 
2195 sub calculate_stock_in_out {
 
2196   my ($self, $item, $stock_info) = @_;
 
2198   return "" if !$item->part || !$item->part->unit || !$item->unit;
 
2200   my $sum      = sum0 map {
 
2201     $_->unit_obj->convert_to($_->qty, $item->unit_obj)
 
2202   } $item->delivery_order_stock_entries;
 
2204   my $content  = _format_number($sum, 2);
 
2209 sub init_type_data {
 
2210   SL::Controller::DeliveryOrder::TypeData->new($_[0]);
 
2213 sub init_valid_types {
 
2214   $_[0]->type_data->valid_types;
 
2225 SL::Controller::Order - controller for orders
 
2229 This is a new form to enter orders, completely rewritten with the use
 
2230 of controller and java script techniques.
 
2232 The aim is to provide the user a better experience and a faster workflow. Also
 
2233 the code should be more readable, more reliable and better to maintain.
 
2241 One input row, so that input happens every time at the same place.
 
2245 Use of pickers where possible.
 
2249 Possibility to enter more than one item at once.
 
2253 Item list in a scrollable area, so that the workflow buttons stay at
 
2258 Reordering item rows with drag and drop is possible. Sorting item rows is
 
2259 possible (by partnumber, description, qty, sellprice and discount for now).
 
2263 No C<update> is necessary. All entries and calculations are managed
 
2264 with ajax-calls and the page only reloads on C<save>.
 
2268 User can see changes immediately, because of the use of java script
 
2279 =item * C<SL/Controller/Order.pm>
 
2283 =item * C<template/webpages/delivery_order/form.html>
 
2287 =item * C<template/webpages/delivery_order/tabs/basic_data.html>
 
2289 Main tab for basic_data.
 
2291 This is the only tab here for now. "linked records" and "webdav" tabs are
 
2292 reused from generic code.
 
2296 =item * C<template/webpages/delivery_order/tabs/_business_info_row.html>
 
2298 For displaying information on business type
 
2300 =item * C<template/webpages/delivery_order/tabs/_item_input.html>
 
2302 The input line for items
 
2304 =item * C<template/webpages/delivery_order/tabs/_row.html>
 
2306 One row for already entered items
 
2308 =item * C<template/webpages/delivery_order/tabs/_tax_row.html>
 
2310 Displaying tax information
 
2314 =item * C<js/kivi.DeliveryOrder.js>
 
2316 java script functions
 
2326 =item * price sources: little symbols showing better price / better discount
 
2328 =item * select units in input row?
 
2330 =item * check for direct delivery (workflow sales order -> purchase order)
 
2332 =item * access rights
 
2334 =item * display weights
 
2338 =item * optional client/user behaviour
 
2340 (transactions has to be set - department has to be set -
 
2341  force project if enabled in client config - transport cost reminder)
 
2345 =head1 KNOWN BUGS AND CAVEATS
 
2351 Customer discount is not displayed as a valid discount in price source popup
 
2352 (this might be a bug in price sources)
 
2354 (I cannot reproduce this (Bernd))
 
2358 No indication that <shift>-up/down expands/collapses second row.
 
2362 Inline creation of parts is not currently supported
 
2366 Table header is not sticky in the scrolling area.
 
2370 Sorting does not include C<position>, neither does reordering.
 
2372 This behavior was implemented intentionally. But we can discuss, which behavior
 
2373 should be implemented.
 
2377 =head1 To discuss / Nice to have
 
2383 How to expand/collapse second row. Now it can be done clicking the icon or
 
2388 Possibility to select PriceSources in input row?
 
2392 This controller uses a (changed) copy of the template for the PriceSource
 
2393 dialog. Maybe there could be used one code source.
 
2397 Rounding-differences between this controller (PriceTaxCalculator) and the old
 
2398 form. This is not only a problem here, but also in all parts using the PTC.
 
2399 There exists a ticket and a patch. This patch should be testet.
 
2403 An indicator, if the actual inputs are saved (like in an
 
2404 editor or on text processing application).
 
2408 A warning when leaving the page without saveing unchanged inputs.
 
2415 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>