Revision 5d711a25
Von Martin Helmling martin.helmling@octosoft.eu vor fast 8 Jahren hinzugefügt
SL/Controller/Part.pm | ||
---|---|---|
13 | 13 |
use Data::Dumper; |
14 | 14 |
use DateTime; |
15 | 15 |
use SL::DB::History; |
16 |
use SL::DB::Helper::ValidateAssembly qw(validate_assembly); |
|
16 | 17 |
use SL::CVar; |
17 | 18 |
use Carp; |
18 | 19 |
|
... | ... | |
250 | 251 |
->html('#items_sum_diff', $::form->format_amount(\%::myconfig, $sum_diff, 2, 0)) |
251 | 252 |
->html('#items_sellprice_sum_basic', $::form->format_amount(\%::myconfig, $sellprice_sum, 2, 0)) |
252 | 253 |
->html('#items_lastcost_sum_basic', $::form->format_amount(\%::myconfig, $lastcost_sum, 2, 0)) |
253 |
->render(); |
|
254 |
->no_flash_clear->render();
|
|
254 | 255 |
} |
255 | 256 |
|
256 | 257 |
sub action_add_multi_assortment_items { |
... | ... | |
270 | 271 |
my ($self) = @_; |
271 | 272 |
|
272 | 273 |
my $item_objects = $self->parse_add_items_to_objects(part_type => 'assembly'); |
273 |
my $html = $self->render_assembly_items_to_html($item_objects); |
|
274 |
my @checked_objects; |
|
275 |
foreach my $item (@{$item_objects}) { |
|
276 |
my $errstr = validate_assembly($item->part,$self->part); |
|
277 |
$self->js->flash('error',$errstr) if $errstr; |
|
278 |
push (@checked_objects,$item) unless $errstr; |
|
279 |
} |
|
280 |
|
|
281 |
my $html = $self->render_assembly_items_to_html(\@checked_objects); |
|
274 | 282 |
|
275 | 283 |
$self->js->run('kivi.Part.close_multi_items_dialog') |
276 | 284 |
->append('#assembly_rows', $html) |
... | ... | |
313 | 321 |
->html('#items_lastcost_sum_basic', $::form->format_amount(\%::myconfig, $items_lastcost_sum, 2, 0)) |
314 | 322 |
->render; |
315 | 323 |
} |
324 |
|
|
316 | 325 |
sub action_add_assembly_item { |
317 | 326 |
my ($self) = @_; |
318 | 327 |
|
... | ... | |
321 | 330 |
carp('Too many objects passed to add_assembly_item') if @{$::form->{add_items}} > 1; |
322 | 331 |
|
323 | 332 |
my $add_item_id = $::form->{add_items}->[0]->{parts_id}; |
333 |
|
|
324 | 334 |
my $duplicate_warning = 0; # duplicates are allowed, just warn |
325 | 335 |
if ( $add_item_id && grep { $add_item_id == $_->parts_id } @{ $self->assembly_items } ) { |
326 | 336 |
$duplicate_warning++; |
... | ... | |
328 | 338 |
|
329 | 339 |
my $number_of_items = scalar @{$self->assembly_items}; |
330 | 340 |
my $item_objects = $self->parse_add_items_to_objects(part_type => 'assembly'); |
341 |
if ($add_item_id ) { |
|
342 |
foreach my $item (@{$item_objects}) { |
|
343 |
my $errstr = validate_assembly($item->part,$self->part); |
|
344 |
return $self->js->flash('error',$errstr)->render if $errstr; |
|
345 |
} |
|
346 |
} |
|
347 |
|
|
348 |
|
|
331 | 349 |
my $html = $self->render_assembly_items_to_html($item_objects, $number_of_items); |
332 | 350 |
|
333 | 351 |
$self->js->flash('info', t8("This part has already been added.")) if $duplicate_warning; |
SL/DB/Assembly.pm | ||
---|---|---|
1 |
# This file has been auto-generated only because it didn't exist. |
|
2 |
# Feel free to modify it at will; it will not be overwritten automatically. |
|
3 |
|
|
4 | 1 |
package SL::DB::Assembly; |
5 | 2 |
|
6 | 3 |
use strict; |
SL/DB/Helper/ValidateAssembly.pm | ||
---|---|---|
1 |
package SL::DB::Helper::ValidateAssembly; |
|
2 |
|
|
3 |
use strict; |
|
4 |
use parent qw(Exporter); |
|
5 |
our @EXPORT = qw(validate_assembly); |
|
6 |
|
|
7 |
use SL::Locale::String; |
|
8 |
use SL::DB::Part; |
|
9 |
use SL::DB::Assembly; |
|
10 |
|
|
11 |
sub validate_assembly { |
|
12 |
my ($new_part, $part) = @_; |
|
13 |
|
|
14 |
return t8("The assembly '#1' cannot be a part from itself.", $part->partnumber) if $new_part->id == $part->id; |
|
15 |
|
|
16 |
my @seen = ($part->id); |
|
17 |
|
|
18 |
return assembly_loop_exists(0, $new_part, @seen); |
|
19 |
} |
|
20 |
|
|
21 |
sub assembly_loop_exists { |
|
22 |
my ($depth, $new_part, @seen) = @_; |
|
23 |
|
|
24 |
return t8("Too much recursions in assembly tree (>100)") if $depth > 100; |
|
25 |
|
|
26 |
# 1. check part is an assembly |
|
27 |
return unless $new_part->is_assembly; |
|
28 |
|
|
29 |
# 2. check assembly is still in list |
|
30 |
return t8("The assembly '#1' would make a loop in assembly tree.", $new_part->partnumber) if grep { $_ == $new_part->id } @seen; |
|
31 |
|
|
32 |
# 3. add to new list |
|
33 |
|
|
34 |
push @seen, $new_part->id; |
|
35 |
|
|
36 |
# 4. go into depth for each child |
|
37 |
|
|
38 |
foreach my $assembly ($new_part->assemblies) { |
|
39 |
my $retval = assembly_loop_exists($depth + 1, $assembly->part, @seen); |
|
40 |
return $retval if $retval; |
|
41 |
} |
|
42 |
return undef; |
|
43 |
} |
|
44 |
|
|
45 |
1; |
|
46 |
|
|
47 |
__END__ |
|
48 |
|
|
49 |
=encoding utf-8 |
|
50 |
|
|
51 |
=head1 NAME |
|
52 |
|
|
53 |
SL::DB::Helper::ValidateAssembly - Mixin to check loops in assemblies |
|
54 |
|
|
55 |
=head1 SYNOPSIS |
|
56 |
|
|
57 |
SL::DB::Helper::ValidateAssembly->validate_assembly($newpart,$assembly_part); |
|
58 |
|
|
59 |
|
|
60 |
=head1 HELPER FUNCTION |
|
61 |
|
|
62 |
=over 4 |
|
63 |
|
|
64 |
=item C<validate_assembly new_part_object part_object> |
|
65 |
|
|
66 |
A new part is added to an assembly. C<new_part_object> is the part which is want to added. |
|
67 |
|
|
68 |
First it was checked if the new part is equal the actual part. |
|
69 |
Then recursively all assemblies in the assemby are checked for a loop. |
|
70 |
|
|
71 |
The function returns an error string if a loop exists or the maximum of 100 iterations is reached |
|
72 |
else on success ''. |
|
73 |
|
|
74 |
=back |
|
75 |
|
|
76 |
=head1 AUTHOR |
|
77 |
|
|
78 |
Martin Helmling E<lt>martin.helmling@opendynamic.de>E<gt> |
|
79 |
|
|
80 |
=cut |
SL/DB/Part.pm | ||
---|---|---|
176 | 176 |
SL::DB::OrderItem |
177 | 177 |
SL::DB::DeliveryOrderItem |
178 | 178 |
SL::DB::Inventory |
179 |
SL::DB::Assembly |
|
180 | 179 |
SL::DB::AssortmentItem |
181 | 180 |
); |
182 | 181 |
|
SL/Dev/Part.pm | ||
---|---|---|
40 | 40 |
my (%params) = @_; |
41 | 41 |
|
42 | 42 |
my @parts; |
43 |
my $part1 = SL::Dev::Part::create_part(partnumber => 'ap1', |
|
43 |
my $partnumber = delete $params{part1number} || 'ap1'; |
|
44 |
my $part1 = SL::Dev::Part::create_part(partnumber => $partnumber, |
|
44 | 45 |
description => 'Testpart', |
45 | 46 |
)->save; |
46 | 47 |
push(@parts, $part1); |
... | ... | |
49 | 50 |
|
50 | 51 |
for my $i ( 2 .. $number_of_parts ) { |
51 | 52 |
my $part = $parts[0]->clone_and_reset; |
52 |
$part->partnumber( ($part->partnumber // '') . " " . $i );
|
|
53 |
$part->partnumber( $partnumber . " " . $i );
|
|
53 | 54 |
$part->description( ($part->description // '') . " " . $i ); |
54 | 55 |
$part->save; |
55 | 56 |
push(@parts, $part); |
56 | 57 |
} |
57 | 58 |
|
59 |
my $assnumber = delete $params{assnumber} || 'as1'; |
|
58 | 60 |
my $assembly = SL::DB::Part->new_assembly( |
59 |
partnumber => 'as1',
|
|
61 |
partnumber => $assnumber,
|
|
60 | 62 |
description => 'Test Assembly', |
61 | 63 |
sellprice => '10', |
62 | 64 |
lastcost => '5', |
js/kivi.Part.js | ||
---|---|---|
204 | 204 |
$("#assembly_rows tr:last").find('input[type=text]').filter(':visible:first').focus(); |
205 | 205 |
}; |
206 | 206 |
|
207 |
ns.show_multi_items_dialog = function(part_type) { |
|
207 |
ns.show_multi_items_dialog = function(part_type,part_id) {
|
|
208 | 208 |
|
209 | 209 |
$('#row_table_id thead a img').remove(); |
210 | 210 |
|
... | ... | |
213 | 213 |
data: { callback: 'Part/add_multi_' + part_type + '_items', |
214 | 214 |
callback_data_id: 'ic', |
215 | 215 |
'part.part_type': part_type, |
216 |
'part.id' : part_id, |
|
216 | 217 |
}, |
217 | 218 |
id: 'jq_multi_items_dialog', |
218 | 219 |
dialog: { |
locale/de/all | ||
---|---|---|
930 | 930 |
'Department (description)' => 'Abteilung (Beschreibung)', |
931 | 931 |
'Department 1' => 'Abteilung (1)', |
932 | 932 |
'Department 2' => 'Abteilung (2)', |
933 |
'Department Id' => 'Reservierung', |
|
934 | 933 |
'Departments' => 'Abteilungen', |
935 | 934 |
'Dependencies' => 'Abhängigkeiten', |
936 | 935 |
'Dependency loop detected:' => 'Schleife in den Abhängigkeiten entdeckt:', |
... | ... | |
2845 | 2844 |
'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.', |
2846 | 2845 |
'The administration area is always accessible.' => 'Der Administrationsbereich ist immer zugänglich.', |
2847 | 2846 |
'The application "#1" was not found on the system.' => 'Die Anwendung "#1" wurde auf dem System nicht gefunden.', |
2847 |
'The assembly \'#1\' cannot be a part from itself.' => 'Das Erzeugnis \'#1\' kann kein Teil von sich selbst sein.', |
|
2848 |
'The assembly \'#1\' would make a loop in assembly tree.' => 'Das Erzeugnis \'#1\' würde eine Schleife im Erzeugnisbaum machen.', |
|
2848 | 2849 |
'The assembly doesn\'t have any items.' => 'Das Erzeugnis enthält keine Artikel.', |
2849 | 2850 |
'The assembly has been created.' => 'Das Erzeugnis wurde hergestellt.', |
2850 | 2851 |
'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 | 3261 |
'To user login' => 'Zum Benutzerlogin', |
3261 | 3262 |
'Toggle marker' => 'Markierung umschalten', |
3262 | 3263 |
'Too many results (#1 from #2).' => 'Zu viele Artikel (#1 von #2)', |
3264 |
'Too much recursions in assembly tree (>100)' => 'Zu tiefe Verschachtelung (>100) des Erzeugnisbaum', |
|
3263 | 3265 |
'Top' => 'Oben', |
3264 | 3266 |
'Top (CSS)' => 'Oben (mit CSS)', |
3265 | 3267 |
'Top (Javascript)' => 'Oben (mit Javascript)', |
t/part/assembly.t | ||
---|---|---|
8 | 8 |
use SL::DB::Part; |
9 | 9 |
use SL::DB::Assembly; |
10 | 10 |
use SL::Dev::Part; |
11 |
use SL::DB::Helper::ValidateAssembly; |
|
11 | 12 |
|
12 | 13 |
Support::TestSetup::login(); |
14 |
$::locale = Locale->new("en"); |
|
13 | 15 |
|
14 | 16 |
clear_up(); |
15 | 17 |
reset_state(); |
... | ... | |
20 | 22 |
my $assembly_item_part = SL::DB::Manager::Part->find_by( partnumber => 'ap1' ); |
21 | 23 |
|
22 | 24 |
is($assembly_part->part_type, 'assembly', 'assembly has correct type'); |
23 |
is( scalar @{$assembly_part->assemblies}, 3, 'assembly consists of two parts' );
|
|
25 |
is( scalar @{$assembly_part->assemblies}, 3, 'assembly consists of three parts' );
|
|
24 | 26 |
|
25 | 27 |
# fetch assembly item corresponding to partnumber 19000 |
26 | 28 |
my $assembly_items = $assembly_part->find_assemblies( { parts_id => $assembly_item_part->id } ) || die "can't find assembly_item"; |
... | ... | |
28 | 30 |
is($assembly_item->part->partnumber, 'ap1', 'assembly part part relation works'); |
29 | 31 |
is($assembly_item->assembly_part->partnumber, '19000', 'assembly part assembly part relation works'); |
30 | 32 |
|
33 |
|
|
34 |
|
|
35 |
my $assembly2_part = SL::Dev::Part::create_assembly( partnumber => '20000', part1number => 'ap2', assnumber => 'as2' )->save; |
|
36 |
my $retval = validate_assembly($assembly_part,$assembly2_part); |
|
37 |
ok( $retval eq undef , 'assembly 19000 can be child of assembly 20000' ); |
|
38 |
$assembly2_part->add_assemblies(SL::DB::Assembly->new(parts_id => $assembly_part->id, qty => 3, bom => 1)); |
|
39 |
$assembly2_part->save; |
|
40 |
|
|
41 |
my $assembly3_part = SL::Dev::Part::create_assembly( partnumber => '30000', part1number => 'ap3', assnumber => 'as3' )->save; |
|
42 |
$retval = validate_assembly($assembly3_part,$assembly_part); |
|
43 |
ok( $retval eq undef , 'assembly 30000 can be child of assembly 19000' ); |
|
44 |
|
|
45 |
$retval = validate_assembly($assembly3_part,$assembly2_part); |
|
46 |
ok( $retval eq undef , 'assembly 30000 can be child of assembly 20000' ); |
|
47 |
|
|
48 |
$assembly_part->add_assemblies(SL::DB::Assembly->new(parts_id => $assembly3_part->id, qty => 4, bom => 1)); |
|
49 |
$assembly_part->save; |
|
50 |
|
|
51 |
$retval = validate_assembly($assembly3_part,$assembly2_part); |
|
52 |
ok( $retval eq undef , 'assembly 30000 can be child of assembly 20000' ); |
|
53 |
|
|
54 |
$assembly2_part->add_assemblies(SL::DB::Assembly->new(parts_id => $assembly3_part->id, qty => 5, bom => 1)); |
|
55 |
$assembly2_part->save; |
|
56 |
|
|
57 |
# fetch assembly item corresponding to partnumber 20000 |
|
58 |
my $assembly2_items = $assembly2_part->find_assemblies() || die "can't find assembly_item"; |
|
59 |
is( scalar @{$assembly2_items}, 5, 'assembly2 consists of four parts' ); |
|
60 |
my $assembly2_item = $assembly2_items->[3]; |
|
61 |
is($assembly2_item->qty, 3, 'count of 3.th assembly is 3' ); |
|
62 |
is($assembly2_item->part->part_type, 'assembly', '3.th assembly \''.$assembly2_item->part->partnumber. '\' is also an assembly'); |
|
63 |
my $assembly3_items = $assembly2_item->part->find_assemblies() || die "can't find assembly_item"; |
|
64 |
is( scalar @{$assembly3_items}, 4, 'assembly3 consists of three parts' ); |
|
65 |
|
|
66 |
|
|
67 |
|
|
68 |
# check loop to itself |
|
69 |
$retval = validate_assembly($assembly_part,$assembly_part); |
|
70 |
is( $retval,"The assembly '19000' cannot be a part from itself.", 'assembly loops to itself' ); |
|
71 |
if (!$retval && $assembly_part->add_assemblies( SL::DB::Assembly->new(parts_id => $assembly_part->id, qty => 8, bom => 1))) { |
|
72 |
$assembly_part->save; |
|
73 |
} |
|
74 |
is( scalar @{$assembly_part->assemblies}, 4, 'assembly consists of three parts' ); |
|
75 |
|
|
76 |
# check indirekt loop |
|
77 |
$retval = validate_assembly($assembly2_part,$assembly_part); |
|
78 |
ok( $retval, 'assembly indirect loop' ); |
|
79 |
if (!$retval && $assembly_part->add_assemblies( SL::DB::Assembly->new(parts_id => $assembly2_part->id, qty => 9, bom => 1))) { |
|
80 |
$assembly_part->save; |
|
81 |
} |
|
82 |
is( scalar @{$assembly_part->assemblies}, 4, 'assembly consists of three parts' ); |
|
83 |
|
|
31 | 84 |
clear_up(); |
32 | 85 |
done_testing; |
33 | 86 |
|
templates/webpages/part/_assembly.html | ||
---|---|---|
43 | 43 |
<td align="right">[% 'Part' | $T8 %]:</td> |
44 | 44 |
<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> |
45 | 45 |
<td>[%- L.button_tag("kivi.Part.add_assembly_item()", LxERP.t8("Add")) %]</td> |
46 |
<td>[% L.button_tag('kivi.Part.show_multi_items_dialog("assembly")', LxERP.t8('Add multiple items')) %]</td> |
|
46 |
<td>[% L.button_tag('kivi.Part.show_multi_items_dialog("assembly",' _ SELF.part.id _ ')', LxERP.t8('Add multiple items')) %]</td>
|
|
47 | 47 |
[% ELSE %] |
48 | 48 |
<td></td> |
49 | 49 |
<td></td> |
templates/webpages/part/_assortment.html | ||
---|---|---|
42 | 42 |
<td align="right">[% 'Part' | $T8 %]:</td> |
43 | 43 |
<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> |
44 | 44 |
<td>[%- L.button_tag("kivi.Part.add_assortment_item()", LxERP.t8("Add")) %]</td> |
45 |
<td>[% L.button_tag('kivi.Part.show_multi_items_dialog("assortment")', LxERP.t8('Add multiple items')) %]</td> |
|
45 |
<td>[% L.button_tag('kivi.Part.show_multi_items_dialog("assortment",' _ SELF.part.id _ ')', LxERP.t8('Add multiple items')) %]</td>
|
|
46 | 46 |
<td></td> |
47 | 47 |
[% ELSE %] |
48 | 48 |
<td></td> |
templates/webpages/part/_multi_items_dialog.html | ||
---|---|---|
73 | 73 |
// var data = data.concat($('#multi_items_form').serializeArray()); |
74 | 74 |
var data = $('#multi_items_form').serializeArray(); |
75 | 75 |
data.push({ name: 'action', value: '[%- FORM.callback %]' }); |
76 |
data.push({ name: 'part_type', value: '[%- part_type %]' }); |
|
76 |
data.push({ name: 'part_type', value: '[%- FORM.part.part_type %]' }); |
|
77 |
data.push({ name: 'part.id' , value: '[%- FORM.part.id %]' }); |
|
77 | 78 |
$.post("controller.pl", data, kivi.eval_json_result); |
78 | 79 |
} |
79 | 80 |
|
Auch abrufbar als: Unified diff
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