X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FDB%2FRequirementSpec.pm;h=024e60d7b58b8fc633dbf5b4a60d1f25a16ec404;hb=c5057972d03f3546494fabe72224785e5a0a1714;hp=abeac1c0d073d74917a07a7d5c5505e4592b30c9;hpb=55e399ab36e9b688a4bfbb7b90422fe33e9e14e7;p=kivitendo-erp.git diff --git a/SL/DB/RequirementSpec.pm b/SL/DB/RequirementSpec.pm index abeac1c0d..024e60d7b 100644 --- a/SL/DB/RequirementSpec.pm +++ b/SL/DB/RequirementSpec.pm @@ -8,6 +8,12 @@ use Rose::DB::Object::Helpers; use SL::DB::MetaSetup::RequirementSpec; use SL::DB::Manager::RequirementSpec; +use SL::DB::Helper::AttrDuration; +use SL::DB::Helper::CustomVariables ( + module => 'RequirementSpecs', + cvars_alias => 1, +); +use SL::DB::Helper::LinkedRecords; use SL::Locale::String; use SL::Util qw(_hashify); @@ -27,10 +33,32 @@ __PACKAGE__->meta->add_relationship( class => 'SL::DB::RequirementSpec', column_map => { id => 'working_copy_id' }, }, + versions => { + type => 'one to many', + class => 'SL::DB::RequirementSpecVersion', + column_map => { id => 'requirement_spec_id' }, + }, + working_copy_versions => { + type => 'one to many', + class => 'SL::DB::RequirementSpecVersion', + column_map => { id => 'working_copy_id' }, + }, + orders => { + type => 'one to many', + class => 'SL::DB::RequirementSpecOrder', + column_map => { id => 'requirement_spec_id' }, + }, + parts => { + type => 'one to many', + class => 'SL::DB::RequirementSpecPart', + column_map => { id => 'requirement_spec_id' }, + }, ); __PACKAGE__->meta->initialize; +__PACKAGE__->attr_duration(qw(time_estimation)); + __PACKAGE__->before_save('_before_save_initialize_not_null_columns'); sub validate { @@ -45,12 +73,21 @@ sub validate { sub _before_save_initialize_not_null_columns { my ($self) = @_; - $self->previous_section_number(0) if !defined $self->previous_section_number; - $self->previous_fb_number(0) if !defined $self->previous_fb_number; + for (qw(previous_section_number previous_fb_number previous_picture_number)) { + $self->$_(0) if !defined $self->$_; + } return 1; } +sub version { + my ($self) = @_; + + croak "Not a writer" if scalar(@_) > 1; + + return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0]; +} + sub text_blocks_sorted { my ($self, %params) = _hashify(1, @_); @@ -71,6 +108,13 @@ sub sections_sorted { sub sections { §ions_sorted; } +sub orders_sorted { + my ($self, %params) = _hashify(1, @_); + my $by = $params{by} || 'itime'; + + return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ]; +} + sub displayable_name { my ($self) = @_; @@ -87,13 +131,21 @@ sub versioned_copies_sorted { return \@copies; } +sub parts_sorted { + my ($self, @rest) = @_; + + croak "This sub is not a writer" if @rest; + + return [ sort { $a->position <=> $b->position } @{ $self->parts } ]; +} + sub create_copy { my ($self, %params) = @_; return $self->_create_copy(%params) if $self->db->in_transaction; my $copy; - if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) { + if (!$self->db->with_transaction(sub { $copy = $self->_create_copy(%params) })) { $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2])); return undef; } @@ -104,7 +156,7 @@ sub create_copy { sub _create_copy { my ($self, %params) = @_; - my $copy = Rose::DB::Object::Helpers::clone_and_reset($self); + my $copy = $self->clone_and_reset; $copy->copy_from($self, %params); return $copy; @@ -119,17 +171,30 @@ sub _copy_from { # Copy attributes. if (!$params->{paste_template}) { - $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)), + $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 previous_picture_number is_template)), %attributes); } + # Copy custom variables. + foreach my $var (@{ $source->cvars_by_config }) { + $self->cvar_by_name($var->config->name)->value($var->value); + } + my %paste_template_result; - # Clone text blocks. + # Clone text blocks and pictures. + my $clone_and_reset_position = sub { + my ($src_obj) = @_; + my $cloned = $src_obj->clone_and_reset; + $cloned->position(undef); + return $cloned; + }; + my $clone_text_block = sub { my ($text_block) = @_; - my $cloned = Rose::DB::Object::Helpers::clone_and_reset($text_block); + my $cloned = $text_block->clone_and_reset; $cloned->position(undef); + $cloned->pictures([ map { $clone_and_reset_position->($_) } @{ $text_block->pictures_sorted } ]); return $cloned; }; @@ -141,8 +206,13 @@ sub _copy_from { $self->add_text_blocks($paste_template_result{text_blocks}); } + # Clone additional parts. + $paste_template_result{parts} = [ map { $clone_and_reset_position->($_) } @{ $source->parts } ]; + my $accessor = $params->{paste_template} ? "add_parts" : "parts"; + $self->$accessor($paste_template_result{parts}); + # Save new object -- we need its ID for the items. - $self->save; + $self->save(cascade => 1); my %id_to_clone; @@ -150,9 +220,10 @@ sub _copy_from { my $clone_item; $clone_item = sub { my ($item) = @_; - my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item); + my $cloned = $item->clone_and_reset; $cloned->requirement_spec_id($self->id); $cloned->position(undef); + $cloned->fb_number(undef) if $params->{paste_template}; $cloned->children(map { $clone_item->($_) } @{ $item->children }); $id_to_clone{ $item->id } = $cloned; @@ -209,7 +280,20 @@ sub is_working_copy { sub next_version_number { my ($self) = @_; - return max(0, map { $_->version->version_number } @{ $self->versioned_copies }) + 1; + return 1 if !$self->id; + + my ($max_number) = $self->db->dbh->selectrow_array(<id, $self->id); + SELECT MAX(v.version_number) + FROM requirement_spec_versions v + WHERE v.requirement_spec_id IN ( + SELECT rs.id + FROM requirement_specs rs + WHERE (rs.id = ?) + OR (rs.working_copy_id = ?) + ) +SQL + + return ($max_number // 0) + 1; } sub create_version { @@ -221,10 +305,13 @@ sub create_version { my $ok = $self->db->with_transaction(sub { delete $attributes{version_number}; - $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number)->save; - $copy = $self->create_copy; - $copy->update_attributes(version_id => $version->id, working_copy_id => $self->id); - $self->update_attributes(version_id => $version->id); + SL::DB::Manager::RequirementSpecVersion->update_all( + set => [ working_copy_id => undef ], + where => [ requirement_spec_id => $self->id ], + ); + + $copy = $self->create_copy(working_copy_id => $self->id); + $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save; 1; }); @@ -237,8 +324,18 @@ sub invalidate_version { croak "Cannot work on a versioned copy" if $self->working_copy_id; - return if !$self->id || !$self->version_id; - $self->update_attributes(version_id => undef); + return if !$self->id; + + SL::DB::Manager::RequirementSpecVersion->update_all( + set => [ working_copy_id => undef ], + where => [ working_copy_id => $self->id ], + ); +} + +sub compare_to { + my ($self, $other) = @_; + + return $self->id <=> $other->id; } 1; @@ -258,10 +355,17 @@ The database structure behind requirement specs is a bit involved. The important thing is how working copy/versions are handled. The table contains three important columns: C (which is also the -primary key), C and C. C -is a self-referencing column: it can be C, but if it isn't then -it contains another requirement spec C. C on the other -hand references the table C. +primary key) and C. C is a +self-referencing column: it can be C, but if it isn't then it +contains another requirement spec C. + +Versions are represented similarly. The C +table has three important columns: C (the primary key), +C (references C and must +not be C) and C (references +C as well but can be +C). C points to the working copy if and only if +the working copy is currently equal to a versioned copy. The design is as follows: @@ -276,17 +380,17 @@ copies>. A versioned copy is a copy of a working frozen at the moment in time it was created. Each versioned copy refers back to the working copy it belongs to: each has its C set. -=item * Each versioned copy must reference an entry in the table -C. Meaning: for each versioned copy -C must not be C. +=item * Each versioned copy must be referenced from an entry in the +table C via +C. =item * Directly after creating a versioned copy even the working copy -itself points to a certain version via its C column: to -the same version that the versioned copy just created points -to. However, any modification that will be visible to the customer -(text, positioning etc but not internal things like time/cost -estimation changes) will cause the working copy to be set to 'no -version' again. This is achieved via before save hooks in Perl. +itself is referenced from a version via that table's +C column. However, any modification that will be +visible to the customer (text, positioning etc but not internal things +like time/cost estimation changes) will cause the version to be +disassociated from the working copy. This is achieved via before save +hooks in Perl. =back @@ -346,13 +450,11 @@ the basic attributes from C<$source> have been assigned. This function can be used for resetting a working copy to a specific version. Example: - my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load; - my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load; + my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load; + my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load; - $requirement_spec->copy_from( - $versioned_copy, - version_id => $versioned_copy->version_id, - ); + $requirement_spec->copy_from($versioned_copy); + $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id); =item C @@ -361,7 +463,7 @@ saved. Creating the copy happens within a transaction. =item C -Prerequisites: C<$self> must be a working copy (see L), +Prerequisites: C<$self> must be a working copy (see the overview), not a versioned copy. This function creates a new version for C<$self>. This involves @@ -372,17 +474,15 @@ several steps: =item 1. The next version number is calculated using L. -=item 2. An instance of L is +=item 2. A copy of C<$self> is created with L. + +=item 3. An instance of L is created. Its attributes are copied from C<%attributes> save for the version number which is taken from step 1. -=item 3. A copy of C<$self> is created with L. - -=item 4. The version instance created in step is assigned to the copy -from step 3. - -=item 5. The C in C<$self> is set to the copy's ID from -step 3. +=item 4. The version instance created in step 3 is referenced to the +the copy from step 2 via C and to the working +copy for which the version was created via C. =back @@ -408,10 +508,11 @@ copy and the last version created for it. =item C -Prerequisites: C<$self> must be a working copy (see L), +Prerequisites: C<$self> must be a working copy (see the overview), not a versioned copy. -Sets the C field to C and saves C<$self>. +Sets any C field in the C +table containing C<$self-Eid> to C. =item C @@ -448,6 +549,11 @@ column in ascending order. If the C parameter is given then only the text blocks belonging to that C are returned. +=item C + +Returns an array reference of additional parts sorted by their +positional column in ascending order. + =item C Validate values before saving. Returns list or human-readable error