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