Part Controller - mit nächster freier Nummer speichern
[kivitendo-erp.git] / SL / Controller / Part.pm
index 23b4eff..6477a90 100644 (file)
@@ -5,6 +5,7 @@ use parent qw(SL::Controller::Base);
 
 use Clone qw(clone);
 use SL::DB::Part;
+use SL::DB::PartsGroup;
 use SL::Controller::Helper::GetModels;
 use SL::Locale::String qw(t8);
 use SL::JSON;
@@ -13,6 +14,7 @@ use SL::Helper::Flash;
 use Data::Dumper;
 use DateTime;
 use SL::DB::History;
+use SL::DB::Helper::ValidateAssembly qw(validate_assembly);
 use SL::CVar;
 use Carp;
 
@@ -38,6 +40,7 @@ __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;
 };
@@ -45,6 +48,7 @@ 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;
 };
@@ -52,6 +56,7 @@ 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;
 };
@@ -59,6 +64,7 @@ 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;
 };
@@ -88,8 +94,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;
@@ -126,10 +135,15 @@ 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.'));
 
-  # reload item, this also resets last_modification!
-  $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->part->id);
+  if ( $::form->{callback} ) {
+    $self->redirect_to($::form->unescape($::form->{callback}));
+  } else {
+    # default behaviour after save: reload item, this also resets last_modification!
+    $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->part->id);
+  }
 }
 
 sub action_save_as_new {
@@ -162,11 +176,15 @@ sub action_delete {
   }) or return $self->js->error(t8('The item couldn\'t be deleted!') . " " . $self->part->db->error)->render;
 
   flash_later('info', t8('The item has been deleted.'));
-  my @redirect_params = (
-    controller => 'controller.pl',
-    action => 'LoginScreen/user_login'
-  );
-  $self->redirect_to(@redirect_params);
+  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);
+  }
 }
 
 sub action_use_as_new {
@@ -192,6 +210,7 @@ sub render_form {
   my ($self, %params) = @_;
 
   $self->_set_javascript;
+  $self->_setup_form_action_bar;
 
   my (%assortment_vars, %assembly_vars);
   %assortment_vars = %{ $self->prepare_assortment_render_vars } if $self->part->is_assortment;
@@ -214,7 +233,6 @@ sub render_form {
   $self->render(
     'part/form',
     title             => $title_hash{$self->part->part_type},
-    show_edit_buttons => $::auth->assert('part_service_assembly_edit'),
     %assortment_vars,
     %assembly_vars,
     translations_map  => { map { ($_->language_id   => $_) } @{$self->part->translations} },
@@ -250,7 +268,7 @@ sub action_update_item_totals {
     ->html('#items_sum_diff',            $::form->format_amount(\%::myconfig, $sum_diff,      2, 0))
     ->html('#items_sellprice_sum_basic', $::form->format_amount(\%::myconfig, $sellprice_sum, 2, 0))
     ->html('#items_lastcost_sum_basic',  $::form->format_amount(\%::myconfig, $lastcost_sum,  2, 0))
-    ->render();
+    ->no_flash_clear->render();
 }
 
 sub action_add_multi_assortment_items {
@@ -259,7 +277,7 @@ sub action_add_multi_assortment_items {
   my $item_objects = $self->parse_add_items_to_objects(part_type => 'assortment');
   my $html         = $self->render_assortment_items_to_html($item_objects);
 
-  $self->js->run('kivi.Part.close_multi_items_dialog')
+  $self->js->run('kivi.Part.close_picker_dialogs')
            ->append('#assortment_rows', $html)
            ->run('kivi.Part.renumber_positions')
            ->run('kivi.Part.assortment_recalc')
@@ -270,9 +288,16 @@ sub action_add_multi_assembly_items {
   my ($self) = @_;
 
   my $item_objects = $self->parse_add_items_to_objects(part_type => 'assembly');
-  my $html         = $self->render_assembly_items_to_html($item_objects);
+  my @checked_objects;
+  foreach my $item (@{$item_objects}) {
+    my $errstr = validate_assembly($item->part,$self->part);
+    $self->js->flash('error',$errstr) if     $errstr;
+    push (@checked_objects,$item)     unless $errstr;
+  }
 
-  $self->js->run('kivi.Part.close_multi_items_dialog')
+  my $html = $self->render_assembly_items_to_html(\@checked_objects);
+
+  $self->js->run('kivi.Part.close_picker_dialogs')
            ->append('#assembly_rows', $html)
            ->run('kivi.Part.renumber_positions')
            ->run('kivi.Part.assembly_recalc')
@@ -313,6 +338,7 @@ sub action_add_assortment_item {
     ->html('#items_lastcost_sum_basic',  $::form->format_amount(\%::myconfig, $items_lastcost_sum,  2, 0))
     ->render;
 }
+
 sub action_add_assembly_item {
   my ($self) = @_;
 
@@ -321,6 +347,7 @@ sub action_add_assembly_item {
   carp('Too many objects passed to add_assembly_item') if @{$::form->{add_items}} > 1;
 
   my $add_item_id = $::form->{add_items}->[0]->{parts_id};
+
   my $duplicate_warning = 0; # duplicates are allowed, just warn
   if ( $add_item_id && grep { $add_item_id == $_->parts_id } @{ $self->assembly_items } ) {
     $duplicate_warning++;
@@ -328,6 +355,14 @@ sub action_add_assembly_item {
 
   my $number_of_items = scalar @{$self->assembly_items};
   my $item_objects    = $self->parse_add_items_to_objects(part_type => 'assembly');
+  if ($add_item_id ) {
+    foreach my $item (@{$item_objects}) {
+      my $errstr = validate_assembly($item->part,$self->part);
+      return $self->js->flash('error',$errstr)->render if $errstr;
+    }
+  }
+
+
   my $html            = $self->render_assembly_items_to_html($item_objects, $number_of_items);
 
   $self->js->flash('info', t8("This part has already been added.")) if $duplicate_warning;
@@ -352,11 +387,9 @@ sub action_add_assembly_item {
 }
 
 sub action_show_multi_items_dialog {
-  require SL::DB::PartsGroup;
   $_[0]->render('part/_multi_items_dialog', { layout => 0 },
-                part_type => 'assortment',
-                partfilter => '', # can I get at the current input of the partpicker here?
-                all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
+    all_partsgroups => SL::DB::Manager::PartsGroup->get_all
+  );
 }
 
 sub action_multi_items_update_result {
@@ -483,21 +516,19 @@ sub action_ajax_autocomplete {
 
   # if someone types something, and hits enter, assume he entered the full name.
   # if something matches, treat that as sole match
-  # unfortunately get_models can't do more than one per package atm, so we d it
-  # the oldfashioned way.
+  # since we need a second get models instance with different filters for that,
+  # we only modify the original filter temporarily in place
   if ($::form->{prefer_exact}) {
+    local $::form->{filter}{'all::ilike'} = delete local $::form->{filter}{'all:substr:multi::ilike'};
+
+    my $exact_models = SL::Controller::Helper::GetModels->new(
+      controller   => $self,
+      sorted       => 0,
+      paginated    => { per_page => 2 },
+      with_objects => [ qw(unit_obj classification) ],
+    );
     my $exact_matches;
-    if (1 == scalar @{ $exact_matches = SL::DB::Manager::Part->get_all(
-      query => [
-        obsolete => 0,
-        SL::DB::Manager::Part->type_filter($::form->{filter}{part_type}),
-        or => [
-          description => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
-          partnumber  => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
-        ]
-      ],
-      limit => 2,
-    ) }) {
+    if (1 == scalar @{ $exact_matches = $exact_models->get }) {
       $self->parts($exact_matches);
     }
   }
@@ -523,11 +554,11 @@ sub action_test_page {
 }
 
 sub action_part_picker_search {
-  $_[0]->render('part/part_picker_search', { layout => 0 }, parts => $_[0]->parts);
+  $_[0]->render('part/part_picker_search', { layout => 0 });
 }
 
 sub action_part_picker_result {
-  $_[0]->render('part/_part_picker_result', { layout => 0 });
+  $_[0]->render('part/_part_picker_result', { layout => 0 }, parts => $_[0]->parts);
 }
 
 sub action_show {
@@ -581,6 +612,7 @@ sub add {
   check_has_valid_part_type($self->part->part_type);
 
   $self->_set_javascript;
+  $self->_setup_form_action_bar;
 
   my %title_hash = ( part       => t8('Add Part'),
                      assembly   => t8('Add Assembly'),
@@ -590,15 +622,14 @@ sub add {
 
   $self->render(
     'part/form',
-    title             => $title_hash{$self->part->part_type},
-    show_edit_buttons => $::auth->assert('part_service_assembly_edit'),
+    title => $title_hash{$self->part->part_type},
   );
 }
 
 
 sub _set_javascript {
   my ($self) = @_;
-  $::request->layout->use_javascript("${_}.js")  for qw(kivi.Part 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);
   $::request->layout->add_javascripts_inline("\$(function(){kivi.PriceRule.load_price_rules_for_part(@{[ $self->part->id ]})});") if $self->part->id;
 }
 
@@ -650,8 +681,8 @@ sub parse_form {
   my $params = delete($::form->{part}) || { };
 
   delete $params->{id};
-  # never overwrite existing partnumber, should be a read-only field anyway
-  delete $params->{partnumber} if $self->part->partnumber;
+  # 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;
 
@@ -792,7 +823,7 @@ sub init_models {
       partnumber  => t8('Partnumber'),
       description  => t8('Description'),
     },
-    with_objects => [ qw(unit_obj) ],
+    with_objects => [ qw(unit_obj classification) ],
   );
 }
 
@@ -915,7 +946,7 @@ sub init_multi_items_models {
   SL::Controller::Helper::GetModels->new(
     controller     => $_[0],
     model          => 'Part',
-    with_objects   => [ qw(unit_obj partsgroup) ],
+    with_objects   => [ qw(unit_obj partsgroup classification) ],
     disable_plugin => 'paginated',
     source         => $::form->{multi_items},
     sorted         => {
@@ -945,7 +976,7 @@ sub form_check_assortment_items_exist {
   my ($self) = @_;
 
   return 1 unless $::form->{part}{part_type} eq 'assortment';
-  # skip check for existing parts that have been used
+  # skip item check for existing assortments that have been used
   return 1 if ($self->part->id and !$self->part->orphaned);
 
   # new or orphaned parts must have items in $::form->{assortment_items}
@@ -982,6 +1013,9 @@ sub form_check_assembly_items_exist {
 
   return 1 unless $::form->{part}->{part_type} eq 'assembly';
 
+  # skip item check for existing assembly that have been used
+  return 1 if ($self->part->id and !$self->part->orphaned);
+
   unless ( $::form->{assembly_items} and scalar @{$::form->{assembly_items}} ) {
     $self->js->run('kivi.Part.set_tab_active_by_name', 'assembly_tab')
              ->focus('#add_assembly_item_name')
@@ -1006,17 +1040,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};
@@ -1109,6 +1132,52 @@ sub parse_add_items_to_objects {
   return \@item_objects;
 }
 
+sub _setup_form_action_bar {
+  my ($self) = @_;
+
+  my $may_edit = $::auth->assert('part_service_assembly_edit', 'may fail');
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          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'),
+          call     => [ 'kivi.Part.use_as_new' ],
+          disabled => !$self->part->id ? t8('The object has not been saved yet.')
+                    : !$may_edit       ? t8('You do not have the permissions to access this function.')
+                    :                    undef,
+        ],
+      ], # end of combobox "Save"
+
+      action => [
+        t8('Delete'),
+        call     => [ 'kivi.Part.delete' ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => !$self->part->id       ? t8('This object has not been saved yet.')
+                  : !$may_edit             ? t8('You do not have the permissions to access this function.')
+                  : !$self->part->orphaned ? t8('This object has already been used.')
+                  :                          undef,
+      ],
+
+      'separator',
+
+      action => [
+        t8('History'),
+        call     => [ 'kivi.Part.open_history_popup' ],
+        disabled => !$self->part->id ? t8('This object has not been saved yet.')
+                  : !$may_edit       ? t8('You do not have the permissions to access this function.')
+                  :                    undef,
+      ],
+    );
+  }
+}
+
 1;
 
 __END__