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