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 8eb0f83..350edec 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 2519c1a..42d155f 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 a2c3a73..21240a6 100644 (file)
@@ -176,7 +176,6 @@ sub orphaned {
     SL::DB::OrderItem
     SL::DB::DeliveryOrderItem
     SL::DB::Inventory
-    SL::DB::Assembly
     SL::DB::AssortmentItem
   );
 
index fa970f2..e1d08e8 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 cec7aec..cd97a3c 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 72f6098..9a0c174 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 ad2a505..b07cd97 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 92a0d0d..2428a51 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 d9dbefc..866b147 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 185248d..02bf33a 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);
 }