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