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