]> wagnertech.de Git - kivitendo-erp.git/commitdiff
Prüfen der Bestandteile eines Erzeugnisses beim Hinzufügen
authorMartin Helmling martin.helmling@octosoft.eu <martin.helmling@octosoft.eu>
Wed, 4 Jan 2017 16:46:35 +0000 (17:46 +0100)
committerMartin Helmling martin.helmling@octosoft.eu <martin.helmling@octosoft.eu>
Wed, 11 Jan 2017 07:42:21 +0000 (08:42 +0100)
Erst Prüfung innerhalb des Erzeugnisses,
dann recursive Prüfung der das Erzeugnis enthaltenen Erzeugnisse,
Abbruch nach 100 Rekursionen.

Die Abfrage ist so, dass nur vom Erzeugnis abwärts der Baum in die Tiefe geprüft wird.
Dabei darf auf einem Graph kein Erzeugnis doppelt vorkommen.

Erzeugnisse sind nun editierbar, wenn sie von einem anderen Erzeugnis verwendet werden
solange sie in keinem ERP-Dokument verwendet werden.

Implementiert in einem Helper für SL::Controller::Part.
Er wird auch im Test t/part/assembly.t verwendet

SL/Controller/Part.pm
SL/DB/Assembly.pm
SL/DB/Helper/ValidateAssembly.pm [new file with mode: 0644]
SL/DB/Part.pm
SL/Dev/Part.pm
js/kivi.Part.js
locale/de/all
t/part/assembly.t
templates/webpages/part/_assembly.html
templates/webpages/part/_assortment.html
templates/webpages/part/_multi_items_dialog.html

index 8eb0f834c050757509f686249a3c1c78c0ebade4..350edec138b5725eba052f6bda0cce90ee1a6eda 100644 (file)
@@ -13,6 +13,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;
 
@@ -250,7 +251,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 {
@@ -270,7 +271,14 @@ 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;
+  }
+
+  my $html = $self->render_assembly_items_to_html(\@checked_objects);
 
   $self->js->run('kivi.Part.close_multi_items_dialog')
            ->append('#assembly_rows', $html)
@@ -313,6 +321,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 +330,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 +338,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;
index 2519c1a459f128ffdb60c591f6577354c1ce2f69..42d155f4b8ddd4c1db04ce1110fccf343beb244d 100644 (file)
@@ -1,6 +1,3 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::Assembly;
 
 use strict;
diff --git a/SL/DB/Helper/ValidateAssembly.pm b/SL/DB/Helper/ValidateAssembly.pm
new file mode 100644 (file)
index 0000000..430a662
--- /dev/null
@@ -0,0 +1,80 @@
+package SL::DB::Helper::ValidateAssembly;
+
+use strict;
+use parent qw(Exporter);
+our @EXPORT = qw(validate_assembly);
+
+use SL::Locale::String;
+use SL::DB::Part;
+use SL::DB::Assembly;
+
+sub validate_assembly {
+  my ($new_part, $part) = @_;
+
+  return t8("The assembly '#1' cannot be a part from itself.", $part->partnumber) if $new_part->id == $part->id;
+
+  my @seen = ($part->id);
+
+  return assembly_loop_exists(0, $new_part, @seen);
+}
+
+sub assembly_loop_exists {
+  my ($depth, $new_part, @seen) = @_;
+
+  return t8("Too much recursions in assembly tree (>100)") if $depth > 100;
+
+  # 1. check part is an assembly
+  return unless $new_part->is_assembly;
+
+  # 2. check assembly is still in list
+  return t8("The assembly '#1' would make a loop in assembly tree.", $new_part->partnumber) if grep { $_ == $new_part->id } @seen;
+
+  # 3. add to new list
+
+  push @seen, $new_part->id;
+
+  # 4. go into depth for each child
+
+  foreach my $assembly ($new_part->assemblies) {
+    my $retval = assembly_loop_exists($depth + 1, $assembly->part, @seen);
+    return $retval if $retval;
+  }
+  return undef;
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DB::Helper::ValidateAssembly - Mixin to check loops in assemblies
+
+=head1 SYNOPSIS
+
+SL::DB::Helper::ValidateAssembly->validate_assembly($newpart,$assembly_part);
+
+
+=head1 HELPER FUNCTION
+
+=over 4
+
+=item C<validate_assembly new_part_object  part_object>
+
+A new part is added to an assembly. C<new_part_object> is the part which is want to added.
+
+First it was checked if the new part is equal the actual part.
+Then recursively all assemblies in the assemby are checked for a loop.
+
+The function returns an error string if a loop exists or the maximum of 100 iterations is reached
+else on success ''.
+
+=back
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.de>E<gt>
+
+=cut
index a2c3a73394e0e18ee484cf0e7e434c87cd517d50..21240a64a86fe35a75614c75b1b9747f961b3fbb 100644 (file)
@@ -176,7 +176,6 @@ sub orphaned {
     SL::DB::OrderItem
     SL::DB::DeliveryOrderItem
     SL::DB::Inventory
-    SL::DB::Assembly
     SL::DB::AssortmentItem
   );
 
index fa970f232bd9071a04a81161c48be73347918a89..e1d08e8b5913c69b7ab6541ea42b88e81fa5c7c2 100644 (file)
@@ -40,7 +40,8 @@ sub create_assembly {
   my (%params) = @_;
 
   my @parts;
-  my $part1 = SL::Dev::Part::create_part(partnumber   => 'ap1',
+  my $partnumber = delete $params{part1number} || 'ap1';
+  my $part1 = SL::Dev::Part::create_part(partnumber   => $partnumber,
                                          description  => 'Testpart',
                                         )->save;
   push(@parts, $part1);
@@ -49,14 +50,15 @@ sub create_assembly {
 
   for my $i ( 2 .. $number_of_parts ) {
     my $part = $parts[0]->clone_and_reset;
-    $part->partnumber(  ($part->partnumber  // '') . " " . $i );
+    $part->partnumber(  $partnumber . " " . $i );
     $part->description( ($part->description // '') . " " . $i );
     $part->save;
     push(@parts, $part);
   }
 
+  my $assnumber = delete $params{assnumber} || 'as1';
   my $assembly = SL::DB::Part->new_assembly(
-    partnumber         => 'as1',
+    partnumber         => $assnumber,
     description        => 'Test Assembly',
     sellprice          => '10',
     lastcost           => '5',
index cec7aecf05627f262d3d3b8e199cd4f94b6daf18..cd97a3c9853c0343d82939555b4f8952de9de544 100644 (file)
@@ -204,7 +204,7 @@ namespace('kivi.Part', function(ns) {
     $("#assembly_rows tr:last").find('input[type=text]').filter(':visible:first').focus();
   };
 
-  ns.show_multi_items_dialog = function(part_type) {
+  ns.show_multi_items_dialog = function(part_type,part_id) {
 
     $('#row_table_id thead a img').remove();
 
@@ -213,6 +213,7 @@ namespace('kivi.Part', function(ns) {
       data: { callback:         'Part/add_multi_' + part_type + '_items',
               callback_data_id: 'ic',
               'part.part_type': part_type,
+              'part.id'       : part_id,
             },
       id: 'jq_multi_items_dialog',
       dialog: {
index 72f60985bc8c8c6d6f203fd1e18925ac4223e0ff..9a0c17443da913f3b81e74bd8824532cf933f25e 100755 (executable)
@@ -930,7 +930,6 @@ $self->{texts} = {
   'Department (description)'    => 'Abteilung (Beschreibung)',
   'Department 1'                => 'Abteilung (1)',
   'Department 2'                => 'Abteilung (2)',
-  'Department Id'               => 'Reservierung',
   'Departments'                 => 'Abteilungen',
   'Dependencies'                => 'Abhängigkeiten',
   'Dependency loop detected:'   => 'Schleife in den Abh&auml;ngigkeiten entdeckt:',
@@ -2845,6 +2844,8 @@ $self->{texts} = {
   'The action you\'ve chosen has not been executed because the document does not contain any item yet.' => 'Die von Ihnen ausgewählte Aktion wurde nicht ausgeführt, weil der Beleg noch keine Positionen enthält.',
   'The administration area is always accessible.' => 'Der Administrationsbereich ist immer zugänglich.',
   'The application "#1" was not found on the system.' => 'Die Anwendung "#1" wurde auf dem System nicht gefunden.',
+  'The assembly \'#1\' cannot be a part from itself.' => 'Das Erzeugnis \'#1\' kann kein Teil von sich selbst sein.',
+  'The assembly \'#1\' would make a loop in assembly tree.' => 'Das Erzeugnis \'#1\' würde eine Schleife im Erzeugnisbaum machen.',
   'The assembly doesn\'t have any items.' => 'Das Erzeugnis enthält keine Artikel.',
   'The assembly has been created.' => 'Das Erzeugnis wurde hergestellt.',
   'The assistant could not find anything wrong with #1. Maybe the problem has been solved in the meantime.' => 'Der Korrekturassistent konnte kein Problem bei #1 feststellen. Eventuell wurde das Problem in der Zwischenzeit bereits behoben.',
@@ -3260,6 +3261,7 @@ $self->{texts} = {
   'To user login'               => 'Zum Benutzerlogin',
   'Toggle marker'               => 'Markierung umschalten',
   'Too many results (#1 from #2).' => 'Zu viele Artikel (#1 von #2)',
+  'Too much recursions in assembly tree (>100)' => 'Zu tiefe Verschachtelung (>100) des Erzeugnisbaum',
   'Top'                         => 'Oben',
   'Top (CSS)'                   => 'Oben (mit CSS)',
   'Top (Javascript)'            => 'Oben (mit Javascript)',
index ad2a5050d21b960d0867f581e23d0f73d277c722..b07cd978b23940cd870abeadf4b23b49f90068bf 100644 (file)
@@ -8,8 +8,10 @@ use SL::DB::Unit;
 use SL::DB::Part;
 use SL::DB::Assembly;
 use SL::Dev::Part;
+use SL::DB::Helper::ValidateAssembly;
 
 Support::TestSetup::login();
+$::locale        = Locale->new("en");
 
 clear_up();
 reset_state();
@@ -20,7 +22,7 @@ my $assembly_part      = SL::DB::Manager::Part->find_by( partnumber => '19000' )
 my $assembly_item_part = SL::DB::Manager::Part->find_by( partnumber => 'ap1' );
 
 is($assembly_part->part_type, 'assembly', 'assembly has correct type');
-is( scalar @{$assembly_part->assemblies}, 3, 'assembly consists of two parts' );
+is( scalar @{$assembly_part->assemblies}, 3, 'assembly consists of three parts' );
 
 # fetch assembly item corresponding to partnumber 19000
 my $assembly_items = $assembly_part->find_assemblies( { parts_id => $assembly_item_part->id } ) || die "can't find assembly_item";
@@ -28,6 +30,57 @@ my $assembly_item = $assembly_items->[0];
 is($assembly_item->part->partnumber, 'ap1', 'assembly part part relation works');
 is($assembly_item->assembly_part->partnumber, '19000', 'assembly part assembly part relation works');
 
+
+
+my $assembly2_part = SL::Dev::Part::create_assembly( partnumber => '20000', part1number => 'ap2', assnumber => 'as2' )->save;
+my $retval = validate_assembly($assembly_part,$assembly2_part);
+ok( $retval eq undef , 'assembly 19000 can be child of assembly 20000' );
+$assembly2_part->add_assemblies(SL::DB::Assembly->new(parts_id => $assembly_part->id, qty => 3, bom => 1));
+$assembly2_part->save;
+
+my $assembly3_part = SL::Dev::Part::create_assembly( partnumber => '30000', part1number => 'ap3', assnumber => 'as3' )->save;
+$retval = validate_assembly($assembly3_part,$assembly_part);
+ok( $retval eq undef , 'assembly 30000 can be child of assembly 19000' );
+
+$retval = validate_assembly($assembly3_part,$assembly2_part);
+ok( $retval eq undef , 'assembly 30000 can be child of assembly 20000' );
+
+$assembly_part->add_assemblies(SL::DB::Assembly->new(parts_id => $assembly3_part->id, qty => 4, bom => 1));
+$assembly_part->save;
+
+$retval = validate_assembly($assembly3_part,$assembly2_part);
+ok( $retval eq undef , 'assembly 30000 can be child of assembly 20000' );
+
+$assembly2_part->add_assemblies(SL::DB::Assembly->new(parts_id => $assembly3_part->id, qty => 5, bom => 1));
+$assembly2_part->save;
+
+# fetch assembly item corresponding to partnumber 20000
+my $assembly2_items = $assembly2_part->find_assemblies() || die "can't find assembly_item";
+is( scalar @{$assembly2_items}, 5, 'assembly2 consists of four parts' );
+my $assembly2_item = $assembly2_items->[3];
+is($assembly2_item->qty, 3, 'count of 3.th assembly is 3' );
+is($assembly2_item->part->part_type, 'assembly', '3.th assembly \''.$assembly2_item->part->partnumber. '\' is also an assembly');
+my $assembly3_items = $assembly2_item->part->find_assemblies() || die "can't find assembly_item";
+is( scalar @{$assembly3_items}, 4, 'assembly3 consists of three parts' );
+
+
+
+# check loop to itself
+$retval = validate_assembly($assembly_part,$assembly_part);
+is( $retval,"The assembly '19000' cannot be a part from itself.", 'assembly loops to itself' );
+if (!$retval && $assembly_part->add_assemblies( SL::DB::Assembly->new(parts_id => $assembly_part->id, qty => 8, bom => 1))) {
+  $assembly_part->save;
+}
+is( scalar @{$assembly_part->assemblies}, 4, 'assembly consists of three parts' );
+
+# check indirekt loop
+$retval = validate_assembly($assembly2_part,$assembly_part);
+ok( $retval, 'assembly indirect loop' );
+if (!$retval && $assembly_part->add_assemblies( SL::DB::Assembly->new(parts_id => $assembly2_part->id, qty => 9, bom => 1))) {
+  $assembly_part->save;
+}
+is( scalar @{$assembly_part->assemblies}, 4, 'assembly consists of three parts' );
+
 clear_up();
 done_testing;
 
index 92a0d0db35f7e3dd13dabb7209c39a337a396b61..2428a51350c51b67fcf8544c7b9a0faa215e97b1 100644 (file)
@@ -43,7 +43,7 @@
  <td align="right">[% 'Part' | $T8 %]:</td>
  <td>[% L.part_picker('add_items[+].parts_id'   , ''  , style='width: 300px' , class="add_assembly_item_input") %][% L.hidden_tag('add_items[].qty_as_number', 1) %]</td>
  <td>[%- L.button_tag("kivi.Part.add_assembly_item()", LxERP.t8("Add")) %]</td>
- <td>[% L.button_tag('kivi.Part.show_multi_items_dialog("assembly")', LxERP.t8('Add multiple items')) %]</td>
+ <td>[% L.button_tag('kivi.Part.show_multi_items_dialog("assembly",' _ SELF.part.id _ ')', LxERP.t8('Add multiple items')) %]</td>
  [% ELSE %]
  <td></td>
  <td></td>
index d9dbefc442e832bf48354a167f605467ebabfde7..866b147c2ebbcbc3db6ac117c6d278a8e0fca4fc 100644 (file)
@@ -42,7 +42,7 @@
  <td align="right">[% 'Part' | $T8 %]:</td>
  <td>[% L.part_picker('add_items[+].parts_id'   , ''  , style='width: 300px' , class="add_assortment_item_input") %][% L.hidden_tag('add_items[].qty_as_number', 1) %]</td>
  <td>[%- L.button_tag("kivi.Part.add_assortment_item()", LxERP.t8("Add")) %]</td>
- <td>[% L.button_tag('kivi.Part.show_multi_items_dialog("assortment")', LxERP.t8('Add multiple items')) %]</td>
+ <td>[% L.button_tag('kivi.Part.show_multi_items_dialog("assortment",' _ SELF.part.id _ ')', LxERP.t8('Add multiple items')) %]</td>
  <td></td>
  [% ELSE %]
  <td></td>
index 185248dce6eace7a50a2d3766d2ec1e853d7a315..02bf33ae9f8ebb6e54dc9706506df68cab223748 100644 (file)
@@ -73,7 +73,8 @@ function add_multi_items() {
   // var data = data.concat($('#multi_items_form').serializeArray());
   var data = $('#multi_items_form').serializeArray();
   data.push({ name: 'action', value: '[%- FORM.callback %]' });
-  data.push({ name: 'part_type', value: '[%- part_type %]' });
+  data.push({ name: 'part_type', value: '[%- FORM.part.part_type %]' });
+  data.push({ name: 'part.id'  , value: '[%- FORM.part.id %]' });
   $.post("controller.pl", data, kivi.eval_json_result);
 }