Pflichtenhefte: PDFs zu Arbeitskopie und Versionen erzeugen
[kivitendo-erp.git] / SL / DB / RequirementSpec.pm
1 package SL::DB::RequirementSpec;
2
3 use strict;
4
5 use Carp;
6 use Rose::DB::Object::Helpers;
7
8 use SL::DB::MetaSetup::RequirementSpec;
9 use SL::DB::Manager::RequirementSpec;
10 use SL::Locale::String;
11 use SL::Util qw(_hashify);
12
13 __PACKAGE__->meta->add_relationship(
14   items            => {
15     type           => 'one to many',
16     class          => 'SL::DB::RequirementSpecItem',
17     column_map     => { id => 'requirement_spec_id' },
18   },
19   text_blocks      => {
20     type           => 'one to many',
21     class          => 'SL::DB::RequirementSpecTextBlock',
22     column_map     => { id => 'requirement_spec_id' },
23   },
24   versioned_copies => {
25     type           => 'one to many',
26     class          => 'SL::DB::RequirementSpec',
27     column_map     => { id => 'working_copy_id' },
28   },
29 );
30
31 __PACKAGE__->meta->initialize;
32
33 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
34
35 sub validate {
36   my ($self) = @_;
37
38   my @errors;
39   push @errors, t8('The title is missing.') if !$self->title;
40
41   return @errors;
42 }
43
44 sub _before_save_initialize_not_null_columns {
45   my ($self) = @_;
46
47   $self->previous_section_number(0) if !defined $self->previous_section_number;
48   $self->previous_fb_number(0)      if !defined $self->previous_fb_number;
49
50   return 1;
51 }
52
53 sub text_blocks_sorted {
54   my ($self, %params) = _hashify(1, @_);
55
56   my @text_blocks = @{ $self->text_blocks };
57   @text_blocks    = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
58   @text_blocks    = sort { $a->position        <=> $b->position            } @text_blocks;
59
60   return wantarray ? @text_blocks : \@text_blocks;
61 }
62
63 sub sections_sorted {
64   my ($self, @rest) = @_;
65
66   croak "This sub is not a writer" if @rest;
67
68   my @sections = sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items };
69   return wantarray ? @sections : \@sections;
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 wantarray ? @copies : \@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, $source, %attributes) = @_;
115
116   croak "Missing parameter 'source'" unless $source;
117
118   # Copy attributes.
119   $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)),
120                            %attributes);
121
122   # Clone text blocks.
123   $self->text_blocks(map { Rose::DB::Object::Helpers::clone_and_reset($_) } @{ $source->text_blocks });
124
125   # Save new object -- we need its ID for the items.
126   $self->save;
127
128   my %id_to_clone;
129
130   # Clone items.
131   my $clone_item;
132   $clone_item = sub {
133     my ($item) = @_;
134     my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
135     $cloned->requirement_spec_id($self->id);
136     $cloned->children(map { $clone_item->($_) } @{ $item->children });
137
138     $id_to_clone{ $item->id } = $cloned;
139
140     return $cloned;
141   };
142
143   $self->items(map { $clone_item->($_) } @{ $source->sections });
144
145   # Save the items -- need to do that before setting dependencies.
146   $self->save;
147
148   # Set dependencies.
149   foreach my $item (@{ $source->items }) {
150     next unless @{ $item->dependencies };
151     $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
152   }
153
154   $self->update_attributes(%attributes);
155
156   return $self;
157 }
158
159 sub delete_items {
160   my ($self) = @_;
161
162   my $worker = sub {
163     # First convert all items to sections so that deleting won't
164     # violate foreign key constraints on parent_id.
165     SL::DB::Manager::RequirementSpecItem->update_all(
166       set   => { parent_id => undef, item_type => 'section' },
167       where => [
168         requirement_spec_id => $self->id,
169         '!parent_id'        => undef,
170       ]);
171
172     # Now delete all items in one go.
173     SL::DB::Manager::RequirementSpecItem->delete_all(where => [ requirement_spec_id => $self->id ]);
174
175     # Last clear values in ourself.
176     $self->items([]);
177   };
178
179   return $self->db->in_transaction ? $worker->() : $self->db->do_transaction($worker);
180 }
181
182 sub previous_version {
183   my ($self) = @_;
184
185   my $and    = $self->version_id ? " AND (version_id <> ?)" : "";
186   my $id     = $self->db->dbh->selectrow_array(<<SQL, undef, $self->id, ($self->version_id) x !!$self->version_id);
187    SELECT MAX(id)
188    FROM requirement_specs
189    WHERE (working_copy_id = ?) $and
190 SQL
191
192   return $id ? SL::DB::RequirementSpec->new(id => $id)->load : undef;
193 }
194
195 sub is_working_copy {
196   my ($self) = @_;
197
198   return !$self->working_copy_id;
199 }
200
201 sub next_version_number {
202   my ($self) = @_;
203   my $max_number = $self->db->dbh->selectrow_array(<<SQL, undef, $self->id);
204     SELECT COALESCE(MAX(ver.version_number), 0)
205     FROM requirement_spec_versions ver
206     JOIN requirement_specs rs ON (rs.version_id = ver.id)
207     WHERE rs.working_copy_id = ?
208 SQL
209
210   return $max_number + 1;
211 }
212
213 sub create_version {
214   my ($self, %attributes) = @_;
215
216   croak "Cannot work on a versioned copy" if $self->working_copy_id;
217
218   my ($copy, $version);
219   my $ok = $self->db->do_transaction(sub {
220     delete $attributes{version_number};
221
222     $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number)->save;
223     $copy    = $self->create_copy;
224     $copy->update_attributes(version_id => $version->id, working_copy_id => $self->id);
225     $self->update_attributes(version_id => $version->id);
226
227     1;
228   });
229
230   return $ok ? ($copy, $version) : ();
231 }
232
233 sub invalidate_version {
234   my ($self, %params) = @_;
235
236   croak "Cannot work on a versioned copy" if $self->working_copy_id;
237
238   return if !$self->id || !$self->version_id;
239   $self->update_attributes(version_id => undef);
240 }
241
242 1;