Pflichtenhefte: zusätzliche Artikel zuweisen und bearbeiten können
[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_picture = sub {
175     my ($picture) = @_;
176     my $cloned    = Rose::DB::Object::Helpers::clone_and_reset($picture);
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_picture->($_) } @{ $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   # Save new object -- we need its ID for the items.
198   $self->save;
199
200   my %id_to_clone;
201
202   # Clone items.
203   my $clone_item;
204   $clone_item = sub {
205     my ($item) = @_;
206     my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
207     $cloned->requirement_spec_id($self->id);
208     $cloned->position(undef);
209     $cloned->fb_number(undef) if $params->{paste_template};
210     $cloned->children(map { $clone_item->($_) } @{ $item->children });
211
212     $id_to_clone{ $item->id } = $cloned;
213
214     return $cloned;
215   };
216
217   $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
218
219   if (!$params->{paste_template}) {
220     $self->items($paste_template_result{sections});
221   } else {
222     $self->add_items($paste_template_result{sections});
223   }
224
225   # Save the items -- need to do that before setting dependencies.
226   $self->save;
227
228   # Set dependencies.
229   foreach my $item (@{ $source->items }) {
230     next unless @{ $item->dependencies };
231     $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
232   }
233
234   $self->update_attributes(%attributes) unless $params->{paste_template};
235
236   return %paste_template_result;
237 }
238
239 sub copy_from {
240   my ($self, $source, %attributes) = @_;
241
242   $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
243 }
244
245 sub paste_template {
246   my ($self, $template) = @_;
247
248   $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
249 }
250
251 sub highest_version {
252   my ($self) = @_;
253
254   return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
255 }
256
257 sub is_working_copy {
258   my ($self) = @_;
259
260   return !$self->working_copy_id;
261 }
262
263 sub next_version_number {
264   my ($self) = @_;
265
266   return 1 if !$self->id;
267
268   my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
269     SELECT MAX(v.version_number)
270     FROM requirement_spec_versions v
271     WHERE v.requirement_spec_id IN (
272       SELECT rs.id
273       FROM requirement_specs rs
274       WHERE (rs.id              = ?)
275          OR (rs.working_copy_id = ?)
276     )
277 SQL
278
279   return ($max_number // 0) + 1;
280 }
281
282 sub create_version {
283   my ($self, %attributes) = @_;
284
285   croak "Cannot work on a versioned copy" if $self->working_copy_id;
286
287   my ($copy, $version);
288   my $ok = $self->db->with_transaction(sub {
289     delete $attributes{version_number};
290
291     SL::DB::Manager::RequirementSpecVersion->update_all(
292       set   => [ working_copy_id     => undef     ],
293       where => [ requirement_spec_id => $self->id ],
294     );
295
296     $copy    = $self->create_copy(working_copy_id => $self->id);
297     $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
298
299     1;
300   });
301
302   return $ok ? ($copy, $version) : ();
303 }
304
305 sub invalidate_version {
306   my ($self, %params) = @_;
307
308   croak "Cannot work on a versioned copy" if $self->working_copy_id;
309
310   return if !$self->id;
311
312   SL::DB::Manager::RequirementSpecVersion->update_all(
313     set   => [ working_copy_id => undef     ],
314     where => [ working_copy_id => $self->id ],
315   );
316 }
317
318 sub compare_to {
319   my ($self, $other) = @_;
320
321   return $self->id <=> $other->id;
322 }
323
324 1;
325 __END__
326
327 =pod
328
329 =encoding utf8
330
331 =head1 NAME
332
333 SL::DB::RequirementSpec - RDBO model for requirement specs
334
335 =head1 OVERVIEW
336
337 The database structure behind requirement specs is a bit involved. The
338 important thing is how working copy/versions are handled.
339
340 The table contains three important columns: C<id> (which is also the
341 primary key) and C<working_copy_id>. C<working_copy_id> is a
342 self-referencing column: it can be C<NULL>, but if it isn't then it
343 contains another requirement spec C<id>.
344
345 Versions are represented similarly. The C<requirement_spec_versions>
346 table has three important columns: C<id> (the primary key),
347 C<requirement_spec_id> (references C<requirement_specs.id> and must
348 not be C<NULL>) and C<working_copy_id> (references
349 C<requirement_specs.id> as well but can be
350 C<NULL>). C<working_copy_id> points to the working copy if and only if
351 the working copy is currently equal to a versioned copy.
352
353 The design is as follows:
354
355 =over 2
356
357 =item * The user is always working on a working copy. The working copy
358 is identified in the database by having C<working_copy_id> set to
359 C<NULL>.
360
361 =item * All other entries in this table are referred to as I<versioned
362 copies>. A versioned copy is a copy of a working frozen at the moment
363 in time it was created. Each versioned copy refers back to the working
364 copy it belongs to: each has its C<working_copy_id> set.
365
366 =item * Each versioned copy must be referenced from an entry in the
367 table C<requirement_spec_versions> via
368 C<requirement_spec_id>.
369
370 =item * Directly after creating a versioned copy even the working copy
371 itself is referenced from a version via that table's
372 C<working_copy_id> column. However, any modification that will be
373 visible to the customer (text, positioning etc but not internal things
374 like time/cost estimation changes) will cause the version to be
375 disassociated from the working copy. This is achieved via before save
376 hooks in Perl.
377
378 =back
379
380 =head1 DATABASE TRIGGERS AND CHECKS
381
382 Several database triggers and consistency checks exist that manage
383 requirement specs, their items and their dependencies. These are
384 described here instead of in the individual files for the other RDBO
385 models.
386
387 =head2 DELETION
388
389 When you delete a requirement spec all of its dependencies (items,
390 text blocks, versions etc.) are deleted by triggers.
391
392 When you delete an item (either a section or a (sub-)function block)
393 all of its children will be deleted as well. This will trigger the
394 same trigger resulting in a recursive deletion with the bottom-most
395 items being deleted first. Their item dependencies are deleted as
396 well.
397
398 =head2 UPDATING
399
400 Whenever you update a requirement spec item a trigger will fire that
401 will update the parent's C<time_estimation> column. This also happens
402 when an item is deleted or updated.
403
404 =head2 CONSISTENCY CHECKS
405
406 Several consistency checks are applied to requirement spec items:
407
408 =over 2
409
410 =item * Column C<requirement_spec_item.item_type> can only contain one of
411 the values C<section>, C<function-block> or C<sub-function-block>.
412
413 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
414 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
415 NULL> otherwise.
416
417 =back
418
419 =head1 FUNCTIONS
420
421 =over 4
422
423 =item C<copy_from $source, %attributes>
424
425 Copies everything (basic attributes like type/title/customer, items,
426 text blocks, time/cost estimation) save for the versions from the
427 other requirement spec object C<$source> into C<$self> and saves
428 it. This is done within a transaction.
429
430 C<%attributes> are attributes that are assigned to C<$self> after all
431 the basic attributes from C<$source> have been assigned.
432
433 This function can be used for resetting a working copy to a specific
434 version. Example:
435
436   my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
437   my $versioned_copy   = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
438
439   $requirement_spec->copy_from($versioned_copy);
440   $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
441
442 =item C<create_copy>
443
444 Creates and returns a copy of C<$self>. The copy is already
445 saved. Creating the copy happens within a transaction.
446
447 =item C<create_version %attributes>
448
449 Prerequisites: C<$self> must be a working copy (see the overview),
450 not a versioned copy.
451
452 This function creates a new version for C<$self>. This involves
453 several steps:
454
455 =over 2
456
457 =item 1. The next version number is calculated using
458 L</next_version_number>.
459
460 =item 2. A copy of C<$self> is created with L</create_copy>.
461
462 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
463 created. Its attributes are copied from C<%attributes> save for the
464 version number which is taken from step 1.
465
466 =item 4. The version instance created in step 3 is referenced to the
467 the copy from step 2 via C<requirement_spec_id> and to the working
468 copy for which the version was created via C<working_copy_id>.
469
470 =back
471
472 All this is done within a transaction.
473
474 In case of success a two-element list is returned consisting of the
475 copy & version objects created in steps 3 and 2 respectively. In case
476 of a failure an empty list will be returned.
477
478 =item C<displayable_name>
479
480 Returns a human-readable name for this instance consisting of the type
481 and the title.
482
483 =item C<highest_version>
484
485 Given a working copy C<$self> this function returns the versioned copy
486 of C<$self> with the highest version number. If such a version exist
487 its instance is returned. Otherwise C<undef> is returned.
488
489 This can be used for calculating the difference between the working
490 copy and the last version created for it.
491
492 =item C<invalidate_version>
493
494 Prerequisites: C<$self> must be a working copy (see the overview),
495 not a versioned copy.
496
497 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
498 table containing C<$self-E<gt>id> to C<undef>.
499
500 =item C<is_working_copy>
501
502 Returns trueish if C<$self> is a working copy and not a versioned
503 copy. The condition for this is that C<working_copy_id> is C<undef>.
504
505 =item C<next_version_number>
506
507 Calculates and returns the next version number for this requirement
508 spec. Version numbers start at 1 and are incremented by one for each
509 version created for it, no matter whether or not it has been reverted
510 to a previous version since. It boils down to this pseudo-code:
511
512   if (has_never_had_a_version)
513     return 1
514   else
515     return max(version_number for all versions for this requirement spec) + 1
516
517 =item C<sections>
518
519 An alias for L</sections_sorted>.
520
521 =item C<sections_sorted>
522
523 Returns an array reference of requirement spec items that do not have
524 a parent -- meaning that are sections.
525
526 This is not a writer. Use the C<items> relationship for that.
527
528 =item C<text_blocks_sorted %params>
529
530 Returns an array reference of text blocks sorted by their positional
531 column in ascending order. If the C<output_position> parameter is
532 given then only the text blocks belonging to that C<output_position>
533 are returned.
534
535 =item C<parts_sorted>
536
537 Returns an array reference of additional parts sorted by their
538 positional column in ascending order.
539
540 =item C<validate>
541
542 Validate values before saving. Returns list or human-readable error
543 messages (if any).
544
545 =item C<versioned_copies_sorted %params>
546
547 Returns an array reference of versioned copies sorted by their version
548 number in ascending order. If the C<max_version_number> parameter is
549 given then only the versioned copies whose version number is less than
550 or equal to C<max_version_number> are returned.
551
552 =back
553
554 =head1 BUGS
555
556 Nothing here yet.
557
558 =head1 AUTHOR
559
560 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
561
562 =cut