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