Pflichtenhefte: beim Kopieren, Einfügen zusätzliche Artikel berücksichtigen
[kivitendo-erp.git] / SL / DB / RequirementSpec.pm
1 package SL::DB::RequirementSpec;
2
3 use strict;
4
5 use Carp;
6 use List::Util qw(max reduce);
7 use Rose::DB::Object::Helpers;
8
9 use SL::DB::MetaSetup::RequirementSpec;
10 use SL::DB::Manager::RequirementSpec;
11 use SL::DB::Helper::LinkedRecords;
12 use SL::Locale::String;
13 use SL::Util qw(_hashify);
14
15 __PACKAGE__->meta->add_relationship(
16   items            => {
17     type           => 'one to many',
18     class          => 'SL::DB::RequirementSpecItem',
19     column_map     => { id => 'requirement_spec_id' },
20   },
21   text_blocks      => {
22     type           => 'one to many',
23     class          => 'SL::DB::RequirementSpecTextBlock',
24     column_map     => { id => 'requirement_spec_id' },
25   },
26   versioned_copies => {
27     type           => 'one to many',
28     class          => 'SL::DB::RequirementSpec',
29     column_map     => { id => 'working_copy_id' },
30   },
31   versions         => {
32     type           => 'one to many',
33     class          => 'SL::DB::RequirementSpecVersion',
34     column_map     => { id => 'requirement_spec_id' },
35   },
36   working_copy_versions => {
37     type           => 'one to many',
38     class          => 'SL::DB::RequirementSpecVersion',
39     column_map     => { id => 'working_copy_id' },
40   },
41   orders           => {
42     type           => 'one to many',
43     class          => 'SL::DB::RequirementSpecOrder',
44     column_map     => { id => 'requirement_spec_id' },
45   },
46   parts            => {
47     type           => 'one to many',
48     class          => 'SL::DB::RequirementSpecPart',
49     column_map     => { id => 'requirement_spec_id' },
50   },
51 );
52
53 __PACKAGE__->meta->initialize;
54
55 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
56
57 sub validate {
58   my ($self) = @_;
59
60   my @errors;
61   push @errors, t8('The title is missing.') if !$self->title;
62
63   return @errors;
64 }
65
66 sub _before_save_initialize_not_null_columns {
67   my ($self) = @_;
68
69   for (qw(previous_section_number previous_fb_number previous_picture_number)) {
70     $self->$_(0) if !defined $self->$_;
71   }
72
73   return 1;
74 }
75
76 sub version {
77   my ($self) = @_;
78
79   croak "Not a writer" if scalar(@_) > 1;
80
81   return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
82 }
83
84 sub text_blocks_sorted {
85   my ($self, %params) = _hashify(1, @_);
86
87   my @text_blocks = @{ $self->text_blocks };
88   @text_blocks    = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
89   @text_blocks    = sort { $a->position        <=> $b->position            } @text_blocks;
90
91   return \@text_blocks;
92 }
93
94 sub sections_sorted {
95   my ($self, @rest) = @_;
96
97   croak "This sub is not a writer" if @rest;
98
99   return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
100 }
101
102 sub sections { &sections_sorted; }
103
104 sub orders_sorted {
105   my ($self, %params) = _hashify(1, @_);
106   my $by              = $params{by} || 'itime';
107
108   return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
109 }
110
111 sub displayable_name {
112   my ($self) = @_;
113
114   return sprintf('%s: "%s"', $self->type->description, $self->title);
115 }
116
117 sub versioned_copies_sorted {
118   my ($self, %params) = _hashify(1, @_);
119
120   my @copies = @{ $self->versioned_copies };
121   @copies    = grep { $_->version->version_number <=  $params{max_version_number} } @copies if $params{max_version_number};
122   @copies    = sort { $a->version->version_number <=> $b->version->version_number } @copies;
123
124   return \@copies;
125 }
126
127 sub parts_sorted {
128   my ($self, @rest) = @_;
129
130   croak "This sub is not a writer" if @rest;
131
132   return [ sort { $a->position <=> $b->position } @{ $self->parts } ];
133 }
134
135 sub create_copy {
136   my ($self, %params) = @_;
137
138   return $self->_create_copy(%params) if $self->db->in_transaction;
139
140   my $copy;
141   if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
142     $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
143     return undef;
144   }
145
146   return $copy;
147 }
148
149 sub _create_copy {
150   my ($self, %params) = @_;
151
152   my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
153   $copy->copy_from($self, %params);
154
155   return $copy;
156 }
157
158 sub _copy_from {
159   my ($self, $params, %attributes) = @_;
160
161   my $source = $params->{source};
162
163   croak "Missing parameter 'source'" unless $source;
164
165   # Copy attributes.
166   if (!$params->{paste_template}) {
167     $self->assign_attributes(map({ ($_ => $source->$_) } qw(type_id status_id customer_id project_id title hourly_rate time_estimation previous_section_number previous_fb_number previous_picture_number is_template)),
168                              %attributes);
169   }
170
171   my %paste_template_result;
172
173   # Clone text blocks and pictures.
174   my $clone_and_reset_position = sub {
175     my ($src_obj) = @_;
176     my $cloned    = Rose::DB::Object::Helpers::clone_and_reset($src_obj);
177     $cloned->position(undef);
178     return $cloned;
179   };
180
181   my $clone_text_block = sub {
182     my ($text_block) = @_;
183     my $cloned       = Rose::DB::Object::Helpers::clone_and_reset($text_block);
184     $cloned->position(undef);
185     $cloned->pictures([ map { $clone_and_reset_position->($_) } @{ $text_block->pictures_sorted } ]);
186     return $cloned;
187   };
188
189   $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
190
191   if (!$params->{paste_template}) {
192     $self->text_blocks($paste_template_result{text_blocks});
193   } else {
194     $self->add_text_blocks($paste_template_result{text_blocks});
195   }
196
197   # Clone additional parts.
198   $paste_template_result{parts} = [ map { $clone_and_reset_position->($_) } @{ $source->parts } ];
199   my $accessor                  = $params->{paste_template} ? "add_parts" : "parts";
200   $self->$accessor($paste_template_result{parts});
201
202   # Save new object -- we need its ID for the items.
203   $self->save;
204
205   my %id_to_clone;
206
207   # Clone items.
208   my $clone_item;
209   $clone_item = sub {
210     my ($item) = @_;
211     my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
212     $cloned->requirement_spec_id($self->id);
213     $cloned->position(undef);
214     $cloned->fb_number(undef) if $params->{paste_template};
215     $cloned->children(map { $clone_item->($_) } @{ $item->children });
216
217     $id_to_clone{ $item->id } = $cloned;
218
219     return $cloned;
220   };
221
222   $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
223
224   if (!$params->{paste_template}) {
225     $self->items($paste_template_result{sections});
226   } else {
227     $self->add_items($paste_template_result{sections});
228   }
229
230   # Save the items -- need to do that before setting dependencies.
231   $self->save;
232
233   # Set dependencies.
234   foreach my $item (@{ $source->items }) {
235     next unless @{ $item->dependencies };
236     $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
237   }
238
239   $self->update_attributes(%attributes) unless $params->{paste_template};
240
241   return %paste_template_result;
242 }
243
244 sub copy_from {
245   my ($self, $source, %attributes) = @_;
246
247   $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
248 }
249
250 sub paste_template {
251   my ($self, $template) = @_;
252
253   $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
254 }
255
256 sub highest_version {
257   my ($self) = @_;
258
259   return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
260 }
261
262 sub is_working_copy {
263   my ($self) = @_;
264
265   return !$self->working_copy_id;
266 }
267
268 sub next_version_number {
269   my ($self) = @_;
270
271   return 1 if !$self->id;
272
273   my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
274     SELECT MAX(v.version_number)
275     FROM requirement_spec_versions v
276     WHERE v.requirement_spec_id IN (
277       SELECT rs.id
278       FROM requirement_specs rs
279       WHERE (rs.id              = ?)
280          OR (rs.working_copy_id = ?)
281     )
282 SQL
283
284   return ($max_number // 0) + 1;
285 }
286
287 sub create_version {
288   my ($self, %attributes) = @_;
289
290   croak "Cannot work on a versioned copy" if $self->working_copy_id;
291
292   my ($copy, $version);
293   my $ok = $self->db->with_transaction(sub {
294     delete $attributes{version_number};
295
296     SL::DB::Manager::RequirementSpecVersion->update_all(
297       set   => [ working_copy_id     => undef     ],
298       where => [ requirement_spec_id => $self->id ],
299     );
300
301     $copy    = $self->create_copy(working_copy_id => $self->id);
302     $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
303
304     1;
305   });
306
307   return $ok ? ($copy, $version) : ();
308 }
309
310 sub invalidate_version {
311   my ($self, %params) = @_;
312
313   croak "Cannot work on a versioned copy" if $self->working_copy_id;
314
315   return if !$self->id;
316
317   SL::DB::Manager::RequirementSpecVersion->update_all(
318     set   => [ working_copy_id => undef     ],
319     where => [ working_copy_id => $self->id ],
320   );
321 }
322
323 sub compare_to {
324   my ($self, $other) = @_;
325
326   return $self->id <=> $other->id;
327 }
328
329 1;
330 __END__
331
332 =pod
333
334 =encoding utf8
335
336 =head1 NAME
337
338 SL::DB::RequirementSpec - RDBO model for requirement specs
339
340 =head1 OVERVIEW
341
342 The database structure behind requirement specs is a bit involved. The
343 important thing is how working copy/versions are handled.
344
345 The table contains three important columns: C<id> (which is also the
346 primary key) and C<working_copy_id>. C<working_copy_id> is a
347 self-referencing column: it can be C<NULL>, but if it isn't then it
348 contains another requirement spec C<id>.
349
350 Versions are represented similarly. The C<requirement_spec_versions>
351 table has three important columns: C<id> (the primary key),
352 C<requirement_spec_id> (references C<requirement_specs.id> and must
353 not be C<NULL>) and C<working_copy_id> (references
354 C<requirement_specs.id> as well but can be
355 C<NULL>). C<working_copy_id> points to the working copy if and only if
356 the working copy is currently equal to a versioned copy.
357
358 The design is as follows:
359
360 =over 2
361
362 =item * The user is always working on a working copy. The working copy
363 is identified in the database by having C<working_copy_id> set to
364 C<NULL>.
365
366 =item * All other entries in this table are referred to as I<versioned
367 copies>. A versioned copy is a copy of a working frozen at the moment
368 in time it was created. Each versioned copy refers back to the working
369 copy it belongs to: each has its C<working_copy_id> set.
370
371 =item * Each versioned copy must be referenced from an entry in the
372 table C<requirement_spec_versions> via
373 C<requirement_spec_id>.
374
375 =item * Directly after creating a versioned copy even the working copy
376 itself is referenced from a version via that table's
377 C<working_copy_id> column. However, any modification that will be
378 visible to the customer (text, positioning etc but not internal things
379 like time/cost estimation changes) will cause the version to be
380 disassociated from the working copy. This is achieved via before save
381 hooks in Perl.
382
383 =back
384
385 =head1 DATABASE TRIGGERS AND CHECKS
386
387 Several database triggers and consistency checks exist that manage
388 requirement specs, their items and their dependencies. These are
389 described here instead of in the individual files for the other RDBO
390 models.
391
392 =head2 DELETION
393
394 When you delete a requirement spec all of its dependencies (items,
395 text blocks, versions etc.) are deleted by triggers.
396
397 When you delete an item (either a section or a (sub-)function block)
398 all of its children will be deleted as well. This will trigger the
399 same trigger resulting in a recursive deletion with the bottom-most
400 items being deleted first. Their item dependencies are deleted as
401 well.
402
403 =head2 UPDATING
404
405 Whenever you update a requirement spec item a trigger will fire that
406 will update the parent's C<time_estimation> column. This also happens
407 when an item is deleted or updated.
408
409 =head2 CONSISTENCY CHECKS
410
411 Several consistency checks are applied to requirement spec items:
412
413 =over 2
414
415 =item * Column C<requirement_spec_item.item_type> can only contain one of
416 the values C<section>, C<function-block> or C<sub-function-block>.
417
418 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
419 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
420 NULL> otherwise.
421
422 =back
423
424 =head1 FUNCTIONS
425
426 =over 4
427
428 =item C<copy_from $source, %attributes>
429
430 Copies everything (basic attributes like type/title/customer, items,
431 text blocks, time/cost estimation) save for the versions from the
432 other requirement spec object C<$source> into C<$self> and saves
433 it. This is done within a transaction.
434
435 C<%attributes> are attributes that are assigned to C<$self> after all
436 the basic attributes from C<$source> have been assigned.
437
438 This function can be used for resetting a working copy to a specific
439 version. Example:
440
441   my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
442   my $versioned_copy   = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
443
444   $requirement_spec->copy_from($versioned_copy);
445   $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
446
447 =item C<create_copy>
448
449 Creates and returns a copy of C<$self>. The copy is already
450 saved. Creating the copy happens within a transaction.
451
452 =item C<create_version %attributes>
453
454 Prerequisites: C<$self> must be a working copy (see the overview),
455 not a versioned copy.
456
457 This function creates a new version for C<$self>. This involves
458 several steps:
459
460 =over 2
461
462 =item 1. The next version number is calculated using
463 L</next_version_number>.
464
465 =item 2. A copy of C<$self> is created with L</create_copy>.
466
467 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
468 created. Its attributes are copied from C<%attributes> save for the
469 version number which is taken from step 1.
470
471 =item 4. The version instance created in step 3 is referenced to the
472 the copy from step 2 via C<requirement_spec_id> and to the working
473 copy for which the version was created via C<working_copy_id>.
474
475 =back
476
477 All this is done within a transaction.
478
479 In case of success a two-element list is returned consisting of the
480 copy & version objects created in steps 3 and 2 respectively. In case
481 of a failure an empty list will be returned.
482
483 =item C<displayable_name>
484
485 Returns a human-readable name for this instance consisting of the type
486 and the title.
487
488 =item C<highest_version>
489
490 Given a working copy C<$self> this function returns the versioned copy
491 of C<$self> with the highest version number. If such a version exist
492 its instance is returned. Otherwise C<undef> is returned.
493
494 This can be used for calculating the difference between the working
495 copy and the last version created for it.
496
497 =item C<invalidate_version>
498
499 Prerequisites: C<$self> must be a working copy (see the overview),
500 not a versioned copy.
501
502 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
503 table containing C<$self-E<gt>id> to C<undef>.
504
505 =item C<is_working_copy>
506
507 Returns trueish if C<$self> is a working copy and not a versioned
508 copy. The condition for this is that C<working_copy_id> is C<undef>.
509
510 =item C<next_version_number>
511
512 Calculates and returns the next version number for this requirement
513 spec. Version numbers start at 1 and are incremented by one for each
514 version created for it, no matter whether or not it has been reverted
515 to a previous version since. It boils down to this pseudo-code:
516
517   if (has_never_had_a_version)
518     return 1
519   else
520     return max(version_number for all versions for this requirement spec) + 1
521
522 =item C<sections>
523
524 An alias for L</sections_sorted>.
525
526 =item C<sections_sorted>
527
528 Returns an array reference of requirement spec items that do not have
529 a parent -- meaning that are sections.
530
531 This is not a writer. Use the C<items> relationship for that.
532
533 =item C<text_blocks_sorted %params>
534
535 Returns an array reference of text blocks sorted by their positional
536 column in ascending order. If the C<output_position> parameter is
537 given then only the text blocks belonging to that C<output_position>
538 are returned.
539
540 =item C<parts_sorted>
541
542 Returns an array reference of additional parts sorted by their
543 positional column in ascending order.
544
545 =item C<validate>
546
547 Validate values before saving. Returns list or human-readable error
548 messages (if any).
549
550 =item C<versioned_copies_sorted %params>
551
552 Returns an array reference of versioned copies sorted by their version
553 number in ascending order. If the C<max_version_number> parameter is
554 given then only the versioned copies whose version number is less than
555 or equal to C<max_version_number> are returned.
556
557 =back
558
559 =head1 BUGS
560
561 Nothing here yet.
562
563 =head1 AUTHOR
564
565 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
566
567 =cut