Auftrags-Controller: Preisquellenermittlung in eigene Funktion ausgelagert
[kivitendo-erp.git] / SL / Controller / Order.pm
index b7793b1..89d2af0 100644 (file)
@@ -33,6 +33,7 @@ use SL::DB::Translation;
 use SL::Helper::CreatePDF qw(:all);
 use SL::Helper::PrintOptions;
 use SL::Helper::ShippedQty;
+use SL::Helper::UserPreferences::DisplayPreferences;
 use SL::Helper::UserPreferences::PositionsScrollbar;
 use SL::Helper::UserPreferences::UpdatePositions;
 
@@ -197,11 +198,19 @@ sub action_save {
            : '';
   flash_later('info', $text);
 
-  my @redirect_params = (
-    action => 'edit',
-    type   => $self->type,
-    id     => $self->order->id,
-  );
+  my @redirect_params;
+  if ($::form->{back_to_caller}) {
+    @redirect_params = $::form->{callback} ? ($::form->{callback})
+                                           : (controller => 'LoginScreen', action => 'user_login');
+
+  } else {
+    @redirect_params = (
+      action   => 'edit',
+      type     => $self->type,
+      id       => $self->order->id,
+      callback => $::form->{callback},
+    );
+  }
 
   $self->redirect_to(@redirect_params);
 }
@@ -250,6 +259,10 @@ sub action_save_as_new {
   # Update employee
   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
 
+  # Warn on obsolete items
+  my @obsolete_positions = map { $_->position } grep { $_->part->obsolete } @{ $order->items_sorted };
+  flash_later('warning', t8('This record containts obsolete items at position #1', join ', ', @obsolete_positions)) if @obsolete_positions;
+
   # Create new record from current one
   $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
 
@@ -438,12 +451,16 @@ sub action_save_and_show_email_dialog {
     $user && !!trim($user->get_config_value('email'));
   } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
 
+
+  my $all_partner_email_addresses = $self->order->customervendor->get_all_email_addresses();
+
   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
                                   email_form    => $email_form,
                                   show_bcc      => $::auth->assert('email_bcc', 'may fail'),
                                   FILES         => \%files,
                                   is_customer   => $self->cv eq 'customer',
                                   ALL_EMPLOYEES => \@employees_with_email,
+                                  ALL_PARTNER_EMAIL_ADDRESSES => $all_partner_email_addresses,
   );
 
   $self->js
@@ -469,6 +486,12 @@ sub action_send_email {
   $self->js_reset_order_and_item_ids_after_save;
 
   my $email_form  = delete $::form->{email_form};
+
+  if ($email_form->{additional_to}) {
+    $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
+    delete $email_form->{additional_to};
+  }
+
   my %field_names = (to => 'email');
 
   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
@@ -864,6 +887,34 @@ sub action_unit_changed {
   $self->js->render();
 }
 
+# update item input row when a part ist picked
+sub action_update_item_input_row {
+  my ($self) = @_;
+
+  delete $::form->{add_item}->{$_} for qw(create_part_type sellprice_as_number discount_as_percent);
+
+  my $form_attr = $::form->{add_item};
+
+  return unless $form_attr->{parts_id};
+
+  my $record       = $self->order;
+  my $item         = SL::DB::OrderItem->new(%$form_attr);
+  $item->unit($item->part->unit);
+
+  my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
+
+  $self->js
+    ->val     ('#add_item_unit',                $item->unit)
+    ->val     ('#add_item_description',         $item->part->description)
+    ->val     ('#add_item_sellprice_as_number', '')
+    ->attr    ('#add_item_sellprice_as_number', 'placeholder', $price_src->price_as_number)
+    ->attr    ('#add_item_sellprice_as_number', 'title',       $price_src->source_description)
+    ->val     ('#add_item_discount_as_percent', '')
+    ->attr    ('#add_item_discount_as_percent', 'placeholder', $discount_src->discount_as_percent)
+    ->attr    ('#add_item_discount_as_percent', 'title',       $discount_src->source_description)
+    ->render;
+}
+
 # add an item row for a new item entered in the input row
 sub action_add_item {
   my ($self) = @_;
@@ -931,6 +982,8 @@ sub action_add_item {
 
   $self->js
     ->val('.add_item_input', '')
+    ->attr('.add_item_input', 'placeholder', '')
+    ->attr('.add_item_input', 'title', '')
     ->run('kivi.Order.init_row_handlers')
     ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
@@ -1084,11 +1137,11 @@ sub action_create_part {
   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.'));
 
   my @redirect_params = (
-    controller => 'Part',
-    action     => 'add',
-    part_type  => $::form->{add_item}->{create_part_type},
-    callback   => $callback,
-    show_abort => 1,
+    controller    => 'Part',
+    action        => 'add',
+    part_type     => $::form->{add_item}->{create_part_type},
+    callback      => $callback,
+    inline_create => 1,
   );
 
   $self->redirect_to(@redirect_params);
@@ -1157,27 +1210,18 @@ sub action_update_row_from_master_data {
     $item->description($texts->{description});
     $item->longdescription($texts->{longdescription});
 
-    my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
-
-    my $price_src;
-    if ($item->part->is_assortment) {
-    # add assortment items with price 0, as the components carry the price
-      $price_src = $price_source->price_from_source("");
-      $price_src->price(0);
-    } else {
-      $price_src = $price_source->best_price
-                 ? $price_source->best_price
-                 : $price_source->price_from_source("");
-      $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
-      $price_src->price(0) if !$price_source->best_price;
-    }
-
+    my ($price_src, $discount_src) = get_best_price_and_discount_source($self->order, $item, 1);
 
     $item->sellprice($price_src->price);
     $item->active_price_source($price_src);
+    $item->discount($discount_src->discount);
+    $item->active_discount_source($discount_src);
+
+    my $price_editable = $self->order->is_sales ? $::auth->assert('sales_edit_prices', 1) : $::auth->assert('purchase_edit_prices', 1);
 
     $self->js
-      ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
+      ->run('kivi.Order.set_price_and_source_text',    $item_id, $price_src   ->source, $price_src   ->source_description, $item->sellprice_as_number, $price_editable)
+      ->run('kivi.Order.set_discount_and_source_text', $item_id, $discount_src->source, $discount_src->source_description, $item->discount_as_percent, $price_editable)
       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
@@ -1619,58 +1663,29 @@ sub new_item {
   }
 
   $item->assign_attributes(%$attr);
+  $item->qty(1.0)                   if !$item->qty;
+  $item->unit($item->part->unit)    if !$item->unit;
 
-  my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
-  my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
-
-  $item->unit($part->unit) if !$item->unit;
-
-  my $price_src;
-  if ( $part->is_assortment ) {
-    # add assortment items with price 0, as the components carry the price
-    $price_src = $price_source->price_from_source("");
-    $price_src->price(0);
-  } elsif (defined $item->sellprice) {
-    $price_src = $price_source->price_from_source("");
-    $price_src->price($item->sellprice);
-  } else {
-    $price_src = $price_source->best_price
-               ? $price_source->best_price
-               : $price_source->price_from_source("");
-    $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
-    $price_src->price(0) if !$price_source->best_price;
-  }
-
-  my $discount_src;
-  if (defined $item->discount) {
-    $discount_src = $price_source->discount_from_source("");
-    $discount_src->discount($item->discount);
-  } else {
-    $discount_src = $price_source->best_discount
-                  ? $price_source->best_discount
-                  : $price_source->discount_from_source("");
-    $discount_src->discount(0) if !$price_source->best_discount;
-  }
+  my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
 
   my %new_attr;
-  $new_attr{part}                   = $part;
-  $new_attr{description}            = $part->description     if ! $item->description;
-  $new_attr{qty}                    = 1.0                    if ! $item->qty;
-  $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
+  $new_attr{description}            = $item->part->description     if ! $item->description;
+  $new_attr{qty}                    = 1.0                          if ! $item->qty;
+  $new_attr{price_factor_id}        = $item->part->price_factor_id if ! $item->price_factor_id;
   $new_attr{sellprice}              = $price_src->price;
   $new_attr{discount}               = $discount_src->discount;
   $new_attr{active_price_source}    = $price_src;
   $new_attr{active_discount_source} = $discount_src;
-  $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
+  $new_attr{longdescription}        = $item->part->notes           if ! defined $attr->{longdescription};
   $new_attr{project_id}             = $record->globalproject_id;
-  $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
+  $new_attr{lastcost}               = $record->is_sales ? $item->part->lastcost : 0;
 
   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
   # they cannot be retrieved via custom_variables until the order/orderitem is
   # saved. Adding empty custom_variables to new orderitem here solves this problem.
   $new_attr{custom_variables} = [];
 
-  my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
+  my $texts = get_part_texts($item->part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
 
   $item->assign_attributes(%new_attr, %{ $texts });
 
@@ -1950,7 +1965,7 @@ sub pre_render {
   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
-  $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted();
+  $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
                                                                                               deleted => 0 ] ],
                                                                            sort_by => 'name');
@@ -2007,6 +2022,7 @@ sub pre_render {
       && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
     $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
   }
+  $self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
 
   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
@@ -2055,6 +2071,17 @@ sub setup_edit_action_bar {
           ],
           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
         ],
+        action => [
+          t8('Save and Close'),
+          call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
+                                                    $::instance_conf->get_order_warn_no_deliverydate,
+                                                    1
+          ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
         action => [
           t8('Save as new'),
           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
@@ -2207,6 +2234,22 @@ sub setup_edit_action_bar {
         only_if  => $deletion_allowed,
       ],
 
+      combobox => [
+        action => [
+          t8('more')
+        ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $self->order->id, 'id' ],
+          disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
+        ],
+        action => [
+          t8('Follow-Up'),
+          call     => [ 'kivi.Order.follow_up_window' ],
+          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          only_if  => $::auth->assert('productivity', 1),
+        ],
+      ], # end of combobox "more"
     );
   }
 }
@@ -2395,6 +2438,41 @@ sub get_part_texts {
   return $texts;
 }
 
+sub get_best_price_and_discount_source {
+  my ($record, $item, $ignore_given) = @_;
+
+  my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+
+  my $price_src;
+  if ( $item->part->is_assortment ) {
+    # add assortment items with price 0, as the components carry the price
+    $price_src = $price_source->price_from_source("");
+    $price_src->price(0);
+  } elsif (!$ignore_given && defined $item->sellprice) {
+    $price_src = $price_source->price_from_source("");
+    $price_src->price($item->sellprice);
+  } else {
+    $price_src = $price_source->best_price
+               ? $price_source->best_price
+               : $price_source->price_from_source("");
+    $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
+    $price_src->price(0) if !$price_source->best_price;
+  }
+
+  my $discount_src;
+  if (!$ignore_given && defined $item->discount) {
+    $discount_src = $price_source->discount_from_source("");
+    $discount_src->discount($item->discount);
+  } else {
+    $discount_src = $price_source->best_discount
+                  ? $price_source->best_discount
+                  : $price_source->discount_from_source("");
+    $discount_src->discount(0) if !$price_source->best_discount;
+  }
+
+  return ($price_src, $discount_src);
+}
+
 sub sales_order_type {
   'sales_order';
 }
@@ -2660,21 +2738,10 @@ java script functions
 
 =item *
 
-Customer discount is not displayed as a valid discount in price source popup
-(this might be a bug in price sources)
-
-(I cannot reproduce this (Bernd))
-
-=item *
-
 No indication that <shift>-up/down expands/collapses second row.
 
 =item *
 
-Inline creation of parts is not currently supported
-
-=item *
-
 Table header is not sticky in the scrolling area.
 
 =item *
@@ -2697,10 +2764,6 @@ How to expand/collapse second row. Now it can be done clicking the icon or
 
 =item *
 
-Possibility to select PriceSources in input row?
-
-=item *
-
 This controller uses a (changed) copy of the template for the PriceSource
 dialog. Maybe there could be used one code source.