Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 5d711a25

Von Martin Helmling martin.helmling@octosoft.eu vor fast 8 Jahren hinzugefügt

  • ID 5d711a25d9257690164f396b25f57095776790d6
  • Vorgänger a116f27a
  • Nachfolger 899f502c

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

Unterschiede anzeigen:

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&auml;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