Kundenstatistik: POD
[kivitendo-erp.git] / SL / Controller / Part.pm
index 1c8db0e..4fbd0c8 100644 (file)
@@ -6,6 +6,7 @@ use parent qw(SL::Controller::Base);
 use Clone qw(clone);
 use SL::DB::Part;
 use SL::DB::PartsGroup;
+use SL::DB::Shop;
 use SL::Controller::Helper::GetModels;
 use SL::Locale::String qw(t8);
 use SL::JSON;
@@ -16,15 +17,20 @@ use DateTime;
 use SL::DB::History;
 use SL::DB::Helper::ValidateAssembly qw(validate_assembly);
 use SL::CVar;
+use SL::MoreCommon qw(save_form);
 use Carp;
+use SL::Presenter::EscapedText qw(escape is_escaped);
+use SL::Presenter::Tag qw(select_tag);
 
 use Rose::Object::MakeMethods::Generic (
   'scalar --get_set_init' => [ qw(parts models part p warehouses multi_items_models
-                                  makemodels
+                                  makemodels shops_not_assigned
+                                  customerprices
                                   orphaned
                                   assortment assortment_items assembly assembly_items
                                   all_pricegroups all_translations all_partsgroups all_units
                                   all_buchungsgruppen all_payment_terms all_warehouses
+                                  parts_classification_filter
                                   all_languages all_units all_price_factors) ],
   'scalar'                => [ qw(warehouse bin) ],
 );
@@ -40,7 +46,6 @@ __PACKAGE__->run_before('check_part_id', only   => [ qw(edit delete) ]);
 sub action_add_part {
   my ($self, %params) = @_;
 
-  $::form->{callback} = $self->url_for(action => 'add_part') unless $::form->{callback};
   $self->part( SL::DB::Part->new_part );
   $self->add;
 };
@@ -48,7 +53,6 @@ sub action_add_part {
 sub action_add_service {
   my ($self, %params) = @_;
 
-  $::form->{callback} = $self->url_for(action => 'add_service') unless $::form->{callback};
   $self->part( SL::DB::Part->new_service );
   $self->add;
 };
@@ -56,7 +60,6 @@ sub action_add_service {
 sub action_add_assembly {
   my ($self, %params) = @_;
 
-  $::form->{callback} = $self->url_for(action => 'add_assembly') unless $::form->{callback};
   $self->part( SL::DB::Part->new_assembly );
   $self->add;
 };
@@ -64,11 +67,22 @@ sub action_add_assembly {
 sub action_add_assortment {
   my ($self, %params) = @_;
 
-  $::form->{callback} = $self->url_for(action => 'add_assortment') unless $::form->{callback};
   $self->part( SL::DB::Part->new_assortment );
   $self->add;
 };
 
+sub action_add_from_record {
+  my ($self) = @_;
+
+  check_has_valid_part_type($::form->{part}{part_type});
+
+  die 'parts_classification_type must be "sales" or "purchases"'
+    unless $::form->{parts_classification_type} =~ m/^(sales|purchases)$/;
+
+  $self->parse_form;
+  $self->add;
+}
+
 sub action_add {
   my ($self) = @_;
 
@@ -94,8 +108,11 @@ sub action_save {
       return $self->js->error(t8('The document has been changed by another user. Please reopen it in another window and copy the changes to the new window'))->render;
   }
 
-  if ( $is_new and !$::form->{part}{partnumber} ) {
-    $self->check_next_transnumber_is_free or return $self->js->error(t8('The next partnumber in the number range already exists!'))->render;
+  if (    $is_new
+       && $::form->{part}{partnumber}
+       && SL::DB::Manager::Part->find_by(partnumber => $::form->{part}{partnumber})
+     ) {
+    return $self->js->error(t8('The partnumber is already being used'))->render;
   }
 
   $self->parse_form;
@@ -132,10 +149,12 @@ sub action_save {
     1;
   }) or return $self->js->error(t8('The item couldn\'t be saved!') . " " . $self->part->db->error )->render;
 
-  flash_later('info', $is_new ? t8('The item has been created.') : t8('The item has been saved.'));
+  ;
+  flash_later('info', $is_new ? t8('The item has been created.') . " " . $self->part->displayable_name : t8('The item has been saved.'));
 
   if ( $::form->{callback} ) {
-    $self->redirect_to($::form->unescape($::form->{callback}));
+    $self->redirect_to($::form->unescape($::form->{callback}) . '&new_parts_id=' . $self->part->id);
+
   } else {
     # default behaviour after save: reload item, this also resets last_modification!
     $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->part->id);
@@ -175,11 +194,7 @@ sub action_delete {
   if ( $::form->{callback} ) {
     $self->redirect_to($::form->unescape($::form->{callback}));
   } else {
-    my @redirect_params = (
-        controller => 'controller.pl',
-        action     => 'LoginScreen/user_login'
-    );
-    $self->redirect_to(@redirect_params);
+    $self->redirect_to(controller => 'ic.pl', action => 'search', searchitems => 'article');
   }
 }
 
@@ -213,6 +228,7 @@ sub render_form {
   %assembly_vars   = %{ $self->prepare_assembly_render_vars   } if $self->part->is_assembly;
 
   $params{CUSTOM_VARIABLES}  = CVar->get_custom_variables(module => 'IC', trans_id => $self->part->id);
+  $_->{valid}                = 1 for @{ $params{CUSTOM_VARIABLES} };
 
   CVar->render_inputs('variables' => $params{CUSTOM_VARIABLES}, show_disabled_message => 1, partsgroup_id => $self->part->partsgroup_id)
     if (scalar @{ $params{CUSTOM_VARIABLES} });
@@ -396,10 +412,10 @@ sub action_multi_items_update_result {
   my $count = $_[0]->multi_items_models->count;
 
   if ($count == 0) {
-    my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
+    my $text = escape($::locale->text('No results.'));
     $_[0]->render($text, { layout => 0 });
   } elsif ($count > $max_count) {
-    my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
+    my $text = escpae($::locale->text('Too many results (#1 from #2).', $count, $max_count));
     $_[0]->render($text, { layout => 0 });
   } else {
     my $multi_items = $_[0]->multi_items_models->get;
@@ -442,6 +458,39 @@ sub action_add_makemodel_row {
     ->render;
 }
 
+sub action_add_customerprice_row {
+  my ($self) = @_;
+
+  my $customer_id = $::form->{add_customerprice};
+
+  my $customer = SL::DB::Manager::Customer->find_by(id => $customer_id)
+    or return $self->js->error(t8("No customer selected or found!"))->render;
+
+  if (grep { $customer_id == $_->customer_id } @{ $self->customerprices }) {
+    $self->js->flash('info', t8("This customer has already been added."));
+  }
+
+  my $position = scalar @{ $self->customerprices } + 1;
+
+  my $cu = SL::DB::PartCustomerPrice->new(
+                      customer_id         => $customer->id,
+                      customer_partnumber => '',
+                      price               => 0,
+                      sortorder           => $position,
+  ) or die "Can't create Customerprice object";
+
+  my $row_as_html = $self->p->render(
+                                     'part/_customerprice_row',
+                                      customerprice => $cu,
+                                      listrow       => $position % 2 ? 0
+                                                                     : 1,
+  );
+
+  $self->js->append('#customerprice_rows', $row_as_html)    # append in tbody
+           ->val('.add_customerprice_input', '')
+           ->run('kivi.Part.focus_last_customerprice_input')->render;
+}
+
 sub action_reorder_items {
   my ($self) = @_;
 
@@ -536,6 +585,7 @@ sub action_ajax_autocomplete {
      id          => $_->id,
      partnumber  => $_->partnumber,
      description => $_->description,
+     ean         => $_->ean,
      part_type   => $_->part_type,
      unit        => $_->unit,
      cvars       => { map { ($_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }) } @{ $_->cvars_by_config } },
@@ -625,7 +675,7 @@ sub add {
 
 sub _set_javascript {
   my ($self) = @_;
-  $::request->layout->use_javascript("${_}.js")  for qw(kivi.Part kivi.File kivi.PriceRule ckeditor/ckeditor ckeditor/adapters/jquery);
+  $::request->layout->use_javascript("${_}.js")  for qw(kivi.Part kivi.File kivi.PriceRule ckeditor/ckeditor ckeditor/adapters/jquery kivi.ShopPart);
   $::request->layout->add_javascripts_inline("\$(function(){kivi.PriceRule.load_price_rules_for_part(@{[ $self->part->id ]})});") if $self->part->id;
 }
 
@@ -677,8 +727,6 @@ sub parse_form {
   my $params = delete($::form->{part}) || { };
 
   delete $params->{id};
-  # never overwrite existing partnumber for parts in use, should be a read-only field in that case anyway
-  delete $params->{partnumber} if $self->part->partnumber and not $self->orphaned;
   $self->part->assign_attributes(%{ $params});
   $self->part->bin_id(undef) unless $self->part->warehouse_id;
 
@@ -701,6 +749,7 @@ sub parse_form {
   $self->part->prices([]);
   $self->parse_form_prices;
 
+  $self->parse_form_customerprices;
   $self->parse_form_makemodels;
 }
 
@@ -770,13 +819,53 @@ sub parse_form_makemodels {
   };
 }
 
+sub parse_form_customerprices {
+  my ($self) = @_;
+
+  my $customerprices_map;
+  if ( $self->part->customerprices ) { # check for new parts or parts without customerprices
+    $customerprices_map = { map { $_->id => Rose::DB::Object::Helpers::clone($_) } @{$self->part->customerprices} };
+  };
+
+  $self->part->customerprices([]);
+
+  my $position = 0;
+  my $customerprices = delete($::form->{customerprices}) || [];
+  foreach my $customerprice ( @{$customerprices} ) {
+    next unless $customerprice->{customer_id};
+    $position++;
+    my $customer = SL::DB::Manager::Customer->find_by(id => $customerprice->{customer_id}) || die "Can't find customer from id";
+
+    my $cu = SL::DB::PartCustomerPrice->new( # parts_id   => $self->part->id, # will be assigned by row add_customerprices
+                                     id                   => $customerprice->{id},
+                                     customer_id          => $customerprice->{customer_id},
+                                     customer_partnumber  => $customerprice->{customer_partnumber} || '',
+                                     price                => $::form->parse_amount(\%::myconfig, $customerprice->{price_as_number}),
+                                     sortorder            => $position,
+                                   );
+    if ($customerprices_map->{$cu->id} && !$customerprices_map->{$cu->id}->lastupdate && $customerprices_map->{$cu->id}->price == 0 && $cu->price == 0) {
+      # lastupdate isn't set, original price is 0 and new lastcost is 0
+      # don't change lastupdate
+    } elsif ( !$customerprices_map->{$cu->id} && $cu->price == 0 ) {
+      # new customerprice, no lastcost entered, leave lastupdate empty
+    } elsif ($customerprices_map->{$cu->id} && $customerprices_map->{$cu->id}->price == $cu->price) {
+      # price hasn't changed, use original lastupdate
+      $cu->lastupdate($customerprices_map->{$cu->id}->lastupdate);
+    } else {
+      $cu->lastupdate(DateTime->now);
+    };
+    $self->part->add_customerprices($cu);
+  };
+}
+
 sub build_bin_select {
-  $_[0]->p->select_tag('part.bin_id', [ $_[0]->warehouse->bins ],
+  select_tag('part.bin_id', [ $_[0]->warehouse->bins ],
     title_key => 'description',
     default   => $_[0]->bin->id,
   );
 }
 
+
 # get_set_inits for partpicker
 
 sub init_parts {
@@ -794,7 +883,7 @@ sub init_part {
   # used by edit, save, delete and add
 
   if ( $::form->{part}{id} ) {
-    return SL::DB::Part->new(id => $::form->{part}{id})->load(with => [ qw(makemodels prices translations partsgroup) ]);
+    return SL::DB::Part->new(id => $::form->{part}{id})->load(with => [ qw(makemodels customerprices prices translations partsgroup shop_parts shop_parts.shop) ]);
   } else {
     die "part_type missing" unless $::form->{part}{part_type};
     return SL::DB::Part->new(part_type => $::form->{part}{part_type});
@@ -873,6 +962,29 @@ sub init_makemodels {
   return \@makemodel_array;
 }
 
+sub init_customerprices {
+  my ($self) = @_;
+
+  my $position = 0;
+  my @customerprice_array = ();
+  my $customerprices = delete($::form->{customerprices}) || [];
+
+  foreach my $customerprice ( @{$customerprices} ) {
+    next unless $customerprice->{customer_id};
+    $position++;
+    my $cu = SL::DB::PartCustomerPrice->new( # parts_id   => $self->part->id, # will be assigned by row add_customerprices
+                                    id                  => $customerprice->{id},
+                                    customer_partnumber => $customerprice->{customer_partnumber},
+                                    customer_id         => $customerprice->{customer_id} || '',
+                                    price               => $::form->parse_amount(\%::myconfig, $customerprice->{price_as_number} || 0),
+                                    sortorder           => $position,
+                                  ) or die "Can't create cu";
+    # $cu->id($customerprice->{id}) if $customerprice->{id};
+    push(@customerprice_array, $cu);
+  };
+  return \@customerprice_array;
+}
+
 sub init_assembly_items {
   my ($self) = @_;
   my $position = 0;
@@ -911,7 +1023,19 @@ sub init_all_buchungsgruppen {
   if ( $self->part->orphaned ) {
     return SL::DB::Manager::Buchungsgruppe->get_all_sorted;
   } else {
-    return SL::DB::Manager::Buchungsgruppe->get_all(where => [ id => $self->part->buchungsgruppen_id ]);
+    return SL::DB::Manager::Buchungsgruppe->get_all_sorted(where => [ id => $self->part->buchungsgruppen_id ]);
+  }
+}
+
+sub init_shops_not_assigned {
+  my ($self) = @_;
+
+  my @used_shop_ids = map { $_->shop->id } @{ $self->part->shop_parts };
+  if ( @used_shop_ids ) {
+    return SL::DB::Manager::Shop->get_all( query => [ obsolete => 0, '!id' => \@used_shop_ids ], sort_by => 'sortkey' );
+  }
+  else {
+    return SL::DB::Manager::Shop->get_all( query => [ obsolete => 0 ], sort_by => 'sortkey' );
   }
 }
 
@@ -955,6 +1079,15 @@ sub init_multi_items_models {
   );
 }
 
+sub init_parts_classification_filter {
+  return [] unless $::form->{parts_classification_type};
+
+  return [ used_for_sale     => 't' ] if $::form->{parts_classification_type} eq 'sales';
+  return [ used_for_purchase => 't' ] if $::form->{parts_classification_type} eq 'purchases';
+
+  die "no query rules for parts_classification_type " . $::form->{parts_classification_type};
+}
+
 # simple checks to run on $::form before saving
 
 sub form_check_part_description_exists {
@@ -1036,17 +1169,6 @@ sub form_check_partnumber_is_unique {
 }
 
 # general checking functions
-sub check_next_transnumber_is_free {
-  my ($self) = @_;
-
-  my ($next_transnumber, $count);
-  $self->part->db->with_transaction(sub {
-    $next_transnumber = $self->part->get_next_trans_number;
-    $count = SL::DB::Manager::Part->get_all_count(where => [ partnumber => $next_transnumber ]);
-    return 1;
-  }) or die $@;
-  $count ? return 0 : return 1;
-}
 
 sub check_part_id {
   die t8("Can't load item without a valid part.id") . "\n" unless $::form->{part}{id};
@@ -1151,7 +1273,6 @@ sub _setup_form_action_bar {
           t8('Save'),
           call      => [ 'kivi.Part.save' ],
           disabled  => !$may_edit ? t8('You do not have the permissions to access this function.') : undef,
-          accesskey => 'enter',
         ],
         action => [
           t8('Use as new'),
@@ -1284,6 +1405,17 @@ parameter part_type as an action. Example:
 
   controller.pl?action=Part/add&part_type=service
 
+=item C<action_add_from_record>
+
+When adding new items to records they can be created on the fly if the entered
+partnumber or description doesn't exist yet. After being asked what part type
+the new item should have the user is redirected to the correct edit page.
+
+Depending on whether the item was added from a sales or a purchase record, only
+the relevant part classifications should be selectable for new item, so this
+parameter is passed on via a hidden parts_classification_type in the new_item
+template.
+
 =item C<action_save>
 
 Saves the current part and then reloads the edit page for the part.