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