From: Martin Helmling martin.helmling@octosoft.eu Date: Wed, 4 Jan 2017 16:46:35 +0000 (+0100) Subject: Prüfen der Bestandteile eines Erzeugnisses beim Hinzufügen X-Git-Tag: release-3.5.4~1741 X-Git-Url: http://wagnertech.de/git?a=commitdiff_plain;h=5d711a25d9257690164f396b25f57095776790d6;p=kivitendo-erp.git Prüfen der Bestandteile eines Erzeugnisses beim Hinzufügen 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 --- diff --git a/SL/Controller/Part.pm b/SL/Controller/Part.pm index 8eb0f834c..350edec13 100644 --- a/SL/Controller/Part.pm +++ b/SL/Controller/Part.pm @@ -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; diff --git a/SL/DB/Assembly.pm b/SL/DB/Assembly.pm index 2519c1a45..42d155f4b 100644 --- a/SL/DB/Assembly.pm +++ b/SL/DB/Assembly.pm @@ -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 index 000000000..430a66297 --- /dev/null +++ b/SL/DB/Helper/ValidateAssembly.pm @@ -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 + +A new part is added to an assembly. C 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 Emartin.helmling@opendynamic.de>E + +=cut diff --git a/SL/DB/Part.pm b/SL/DB/Part.pm index a2c3a7339..21240a64a 100644 --- a/SL/DB/Part.pm +++ b/SL/DB/Part.pm @@ -176,7 +176,6 @@ sub orphaned { SL::DB::OrderItem SL::DB::DeliveryOrderItem SL::DB::Inventory - SL::DB::Assembly SL::DB::AssortmentItem ); diff --git a/SL/Dev/Part.pm b/SL/Dev/Part.pm index fa970f232..e1d08e8b5 100644 --- a/SL/Dev/Part.pm +++ b/SL/Dev/Part.pm @@ -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', diff --git a/js/kivi.Part.js b/js/kivi.Part.js index cec7aecf0..cd97a3c98 100644 --- a/js/kivi.Part.js +++ b/js/kivi.Part.js @@ -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: { diff --git a/locale/de/all b/locale/de/all index 72f60985b..9a0c17443 100755 --- a/locale/de/all +++ b/locale/de/all @@ -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ä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)', diff --git a/t/part/assembly.t b/t/part/assembly.t index ad2a5050d..b07cd978b 100644 --- a/t/part/assembly.t +++ b/t/part/assembly.t @@ -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; diff --git a/templates/webpages/part/_assembly.html b/templates/webpages/part/_assembly.html index 92a0d0db3..2428a5135 100644 --- a/templates/webpages/part/_assembly.html +++ b/templates/webpages/part/_assembly.html @@ -43,7 +43,7 @@ [% 'Part' | $T8 %]: [% L.part_picker('add_items[+].parts_id' , '' , style='width: 300px' , class="add_assembly_item_input") %][% L.hidden_tag('add_items[].qty_as_number', 1) %] [%- L.button_tag("kivi.Part.add_assembly_item()", LxERP.t8("Add")) %] - [% L.button_tag('kivi.Part.show_multi_items_dialog("assembly")', LxERP.t8('Add multiple items')) %] + [% L.button_tag('kivi.Part.show_multi_items_dialog("assembly",' _ SELF.part.id _ ')', LxERP.t8('Add multiple items')) %] [% ELSE %] diff --git a/templates/webpages/part/_assortment.html b/templates/webpages/part/_assortment.html index d9dbefc44..866b147c2 100644 --- a/templates/webpages/part/_assortment.html +++ b/templates/webpages/part/_assortment.html @@ -42,7 +42,7 @@ [% 'Part' | $T8 %]: [% L.part_picker('add_items[+].parts_id' , '' , style='width: 300px' , class="add_assortment_item_input") %][% L.hidden_tag('add_items[].qty_as_number', 1) %] [%- L.button_tag("kivi.Part.add_assortment_item()", LxERP.t8("Add")) %] - [% L.button_tag('kivi.Part.show_multi_items_dialog("assortment")', LxERP.t8('Add multiple items')) %] + [% L.button_tag('kivi.Part.show_multi_items_dialog("assortment",' _ SELF.part.id _ ')', LxERP.t8('Add multiple items')) %] [% ELSE %] diff --git a/templates/webpages/part/_multi_items_dialog.html b/templates/webpages/part/_multi_items_dialog.html index 185248dce..02bf33ae9 100644 --- a/templates/webpages/part/_multi_items_dialog.html +++ b/templates/webpages/part/_multi_items_dialog.html @@ -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); }