use Data::Dumper;
use DateTime;
use SL::DB::History;
+use SL::DB::Helper::ValidateAssembly qw(validate_assembly);
use SL::CVar;
use Carp;
->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 {
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)
->html('#items_lastcost_sum_basic', $::form->format_amount(\%::myconfig, $items_lastcost_sum, 2, 0))
->render;
}
+
sub action_add_assembly_item {
my ($self) = @_;
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++;
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;
-# 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;
--- /dev/null
+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
SL::DB::OrderItem
SL::DB::DeliveryOrderItem
SL::DB::Inventory
- SL::DB::Assembly
SL::DB::AssortmentItem
);
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);
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',
$("#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();
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: {
'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:',
'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.',
'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)',
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();
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";
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;
<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>
<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>
// 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);
}