Pflichtenhefte: Beim Einfügen ganzer Vorlagen Nummerierung neu vergeben
[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->fb_number(undef) if $params->{paste_template};
169     $cloned->children(map { $clone_item->($_) } @{ $item->children });
170
171     $id_to_clone{ $item->id } = $cloned;
172
173     return $cloned;
174   };
175
176   $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
177
178   if (!$params->{paste_template}) {
179     $self->items($paste_template_result{sections});
180   } else {
181     $self->add_items($paste_template_result{sections});
182   }
183
184   # Save the items -- need to do that before setting dependencies.
185   $self->save;
186
187   # Set dependencies.
188   foreach my $item (@{ $source->items }) {
189     next unless @{ $item->dependencies };
190     $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
191   }
192
193   $self->update_attributes(%attributes) unless $params->{paste_template};
194
195   return %paste_template_result;
196 }
197
198 sub copy_from {
199   my ($self, $source, %attributes) = @_;
200
201   $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
202 }
203
204 sub paste_template {
205   my ($self, $template) = @_;
206
207   $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
208 }
209
210 sub highest_version {
211   my ($self) = @_;
212
213   return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
214 }
215
216 sub is_working_copy {
217   my ($self) = @_;
218
219   return !$self->working_copy_id;
220 }
221
222 sub next_version_number {
223   my ($self) = @_;
224
225   return max(0, map { $_->version->version_number } @{ $self->versioned_copies }) + 1;
226 }
227
228 sub create_version {
229   my ($self, %attributes) = @_;
230
231   croak "Cannot work on a versioned copy" if $self->working_copy_id;
232
233   my ($copy, $version);
234   my $ok = $self->db->with_transaction(sub {
235     delete $attributes{version_number};
236
237     $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number)->save;
238     $copy    = $self->create_copy;
239     $copy->update_attributes(version_id => $version->id, working_copy_id => $self->id);
240     $self->update_attributes(version_id => $version->id);
241
242     1;
243   });
244
245   return $ok ? ($copy, $version) : ();
246 }
247
248 sub invalidate_version {
249   my ($self, %params) = @_;
250
251   croak "Cannot work on a versioned copy" if $self->working_copy_id;
252
253   return if !$self->id || !$self->version_id;
254   $self->update_attributes(version_id => undef);
255 }
256
257 1;
258 __END__
259
260 =pod
261
262 =encoding utf8
263
264 =head1 NAME
265
266 SL::DB::RequirementSpec - RDBO model for requirement specs
267
268 =head1 OVERVIEW
269
270 The database structure behind requirement specs is a bit involved. The
271 important thing is how working copy/versions are handled.
272
273 The table contains three important columns: C<id> (which is also the
274 primary key), C<working_copy_id> and C<version_id>. C<working_copy_id>
275 is a self-referencing column: it can be C<NULL>, but if it isn't then
276 it contains another requirement spec C<id>. C<version_id> on the other
277 hand references the table C<requirement_spec_versions>.
278
279 The design is as follows:
280
281 =over 2
282
283 =item * The user is always working on a working copy. The working copy
284 is identified in the database by having C<working_copy_id> set to
285 C<NULL>.
286
287 =item * All other entries in this table are referred to as I<versioned
288 copies>. A versioned copy is a copy of a working frozen at the moment
289 in time it was created. Each versioned copy refers back to the working
290 copy it belongs to: each has its C<working_copy_id> set.
291
292 =item * Each versioned copy must reference an entry in the table
293 C<requirement_spec_versions>. Meaning: for each versioned copy
294 C<version_id> must not be C<NULL>.
295
296 =item * Directly after creating a versioned copy even the working copy
297 itself points to a certain version via its C<version_id> column: to
298 the same version that the versioned copy just created points
299 to. However, any modification that will be visible to the customer
300 (text, positioning etc but not internal things like time/cost
301 estimation changes) will cause the working copy to be set to 'no
302 version' again. This is achieved via before save hooks in Perl.
303
304 =back
305
306 =head1 DATABASE TRIGGERS AND CHECKS
307
308 Several database triggers and consistency checks exist that manage
309 requirement specs, their items and their dependencies. These are
310 described here instead of in the individual files for the other RDBO
311 models.
312
313 =head2 DELETION
314
315 When you delete a requirement spec all of its dependencies (items,
316 text blocks, versions etc.) are deleted by triggers.
317
318 When you delete an item (either a section or a (sub-)function block)
319 all of its children will be deleted as well. This will trigger the
320 same trigger resulting in a recursive deletion with the bottom-most
321 items being deleted first. Their item dependencies are deleted as
322 well.
323
324 =head2 UPDATING
325
326 Whenever you update a requirement spec item a trigger will fire that
327 will update the parent's C<time_estimation> column. This also happens
328 when an item is deleted or updated.
329
330 =head2 CONSISTENCY CHECKS
331
332 Several consistency checks are applied to requirement spec items:
333
334 =over 2
335
336 =item * Column C<requirement_spec_item.item_type> can only contain one of
337 the values C<section>, C<function-block> or C<sub-function-block>.
338
339 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
340 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
341 NULL> otherwise.
342
343 =back
344
345 =head1 FUNCTIONS
346
347 =over 4
348
349 =item C<copy_from $source, %attributes>
350
351 Copies everything (basic attributes like type/title/customer, items,
352 text blocks, time/cost estimation) save for the versions from the
353 other requirement spec object C<$source> into C<$self> and saves
354 it. This is done within a transaction.
355
356 C<%attributes> are attributes that are assigned to C<$self> after all
357 the basic attributes from C<$source> have been assigned.
358
359 This function can be used for resetting a working copy to a specific
360 version. Example:
361
362  my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
363  my $versioned_copy   = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
364
365   $requirement_spec->copy_from(
366     $versioned_copy,
367     version_id => $versioned_copy->version_id,
368   );
369
370 =item C<create_copy>
371
372 Creates and returns a copy of C<$self>. The copy is already
373 saved. Creating the copy happens within a transaction.
374
375 =item C<create_version %attributes>
376
377 Prerequisites: C<$self> must be a working copy (see the overview),
378 not a versioned copy.
379
380 This function creates a new version for C<$self>. This involves
381 several steps:
382
383 =over 2
384
385 =item 1. The next version number is calculated using
386 L</next_version_number>.
387
388 =item 2. An instance of L<SL::DB::RequirementSpecVersion> is
389 created. Its attributes are copied from C<%attributes> save for the
390 version number which is taken from step 1.
391
392 =item 3. A copy of C<$self> is created with L</create_copy>.
393
394 =item 4. The version instance created in step is assigned to the copy
395 from step 3.
396
397 =item 5. The C<version_id> in C<$self> is set to the copy's ID from
398 step 3.
399
400 =back
401
402 All this is done within a transaction.
403
404 In case of success a two-element list is returned consisting of the
405 copy & version objects created in steps 3 and 2 respectively. In case
406 of a failure an empty list will be returned.
407
408 =item C<displayable_name>
409
410 Returns a human-readable name for this instance consisting of the type
411 and the title.
412
413 =item C<highest_version>
414
415 Given a working copy C<$self> this function returns the versioned copy
416 of C<$self> with the highest version number. If such a version exist
417 its instance is returned. Otherwise C<undef> is returned.
418
419 This can be used for calculating the difference between the working
420 copy and the last version created for it.
421
422 =item C<invalidate_version>
423
424 Prerequisites: C<$self> must be a working copy (see the overview),
425 not a versioned copy.
426
427 Sets the C<version_id> field to C<undef> and saves C<$self>.
428
429 =item C<is_working_copy>
430
431 Returns trueish if C<$self> is a working copy and not a versioned
432 copy. The condition for this is that C<working_copy_id> is C<undef>.
433
434 =item C<next_version_number>
435
436 Calculates and returns the next version number for this requirement
437 spec. Version numbers start at 1 and are incremented by one for each
438 version created for it, no matter whether or not it has been reverted
439 to a previous version since. It boils down to this pseudo-code:
440
441   if (has_never_had_a_version)
442     return 1
443   else
444     return max(version_number for all versions for this requirement spec) + 1
445
446 =item C<sections>
447
448 An alias for L</sections_sorted>.
449
450 =item C<sections_sorted>
451
452 Returns an array reference of requirement spec items that do not have
453 a parent -- meaning that are sections.
454
455 This is not a writer. Use the C<items> relationship for that.
456
457 =item C<text_blocks_sorted %params>
458
459 Returns an array reference of text blocks sorted by their positional
460 column in ascending order. If the C<output_position> parameter is
461 given then only the text blocks belonging to that C<output_position>
462 are returned.
463
464 =item C<validate>
465
466 Validate values before saving. Returns list or human-readable error
467 messages (if any).
468
469 =item C<versioned_copies_sorted %params>
470
471 Returns an array reference of versioned copies sorted by their version
472 number in ascending order. If the C<max_version_number> parameter is
473 given then only the versioned copies whose version number is less than
474 or equal to C<max_version_number> are returned.
475
476 =back
477
478 =head1 BUGS
479
480 Nothing here yet.
481
482 =head1 AUTHOR
483
484 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
485
486 =cut