ActsAsList: mit remove_from_list() entfernte Items auf position = -1 setzen
[kivitendo-erp.git] / SL / DB / Helper / LinkedRecords.pm
index 1f91718..3fb5948 100644 (file)
@@ -1,18 +1,32 @@
-package SL::DB::Helpers::LinkedRecords;
+package SL::DB::Helper::LinkedRecords;
 
 use strict;
 
 require Exporter;
 our @ISA    = qw(Exporter);
-our @EXPORT = qw(linked_records link_to_record linked_records_sorted);
+our @EXPORT = qw(linked_records link_to_record);
 
 use Carp;
 use Sort::Naturally;
 
-use SL::DB::Helpers::Mappings;
+use SL::DB::Helper::Mappings;
 use SL::DB::RecordLink;
 
 sub linked_records {
+  my ($self, %params) = @_;
+
+  my %sort_spec       = ( by  => delete($params{sort_by}),
+                          dir => delete($params{sort_dir}) );
+  my $filter          =  delete $params{filter};
+
+  my $records         = _linked_records_implementation($self, %params);
+  $records            = filter_linked_records($self, $filter, @{ $records })                       if $filter;
+  $records            = sort_linked_records($self, $sort_spec{by}, $sort_spec{dir}, @{ $records }) if $sort_spec{by};
+
+  return $records;
+}
+
+sub _linked_records_implementation {
   my $self     = shift;
   my %params   = @_;
 
@@ -23,43 +37,74 @@ sub linked_records {
     my %from_to    = ( from => delete($params{from}) || $both,
                        to   => delete($params{to})   || $both);
 
-    my @records    = (@{ $self->linked_records(%params, direction => 'from', from => $from_to{from}) },
-                      @{ $self->linked_records(%params, direction => 'to',   to   => $from_to{to}  ) });
+    my @records    = (@{ _linked_records_implementation($self, %params, direction => 'from', from => $from_to{from}) },
+                      @{ _linked_records_implementation($self, %params, direction => 'to',   to   => $from_to{to}  ) });
 
     my %record_map = map { ( ref($_) . $_->id => $_ ) } @records;
 
     return [ values %record_map ];
   }
 
-  my $myself   = $wanted eq 'from' ? 'to' : $wanted eq 'to' ? 'from' : croak("Invalid parameter `direction'");
+  if ($params{via}) {
+    croak("Cannot use 'via' without '${wanted}_table'")             if !$params{$wanted};
+    croak("Cannot use 'via' with '${wanted}_table' being an array") if ref $params{$wanted};
+  }
 
-  my $my_table = SL::DB::Helpers::Mappings::get_table_for_package(ref($self));
+  my $myself           = $wanted eq 'from' ? 'to' : $wanted eq 'to' ? 'from' : croak("Invalid parameter `direction'");
+  my $my_table         = SL::DB::Helper::Mappings::get_table_for_package(ref($self));
 
-  my @query    = ( "${myself}_table" => $my_table,
-                   "${myself}_id"    => $self->id );
+  my $sub_wanted_table = "${wanted}_table";
+  my $sub_wanted_id    = "${wanted}_id";
 
+  my ($wanted_classes, $wanted_tables);
   if ($params{$wanted}) {
-    my $wanted_classes = ref($params{$wanted}) eq 'ARRAY' ? $params{$wanted} : [ $params{$wanted} ];
-    my $wanted_tables  = [ map { SL::DB::Helpers::Mappings::get_table_for_package($_) || croak("Invalid parameter `${wanted}'") } @{ $wanted_classes } ];
-    push @query, ("${wanted}_table" => $wanted_tables);
+    $wanted_classes = ref($params{$wanted}) eq 'ARRAY' ? $params{$wanted} : [ $params{$wanted} ];
+    $wanted_tables  = [ map { SL::DB::Helper::Mappings::get_table_for_package($_) || croak("Invalid parameter `${wanted}'") } @{ $wanted_classes } ];
   }
 
-  my $links            = SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]);
+  my @get_objects_query = ref($params{query}) eq 'ARRAY' ? @{ $params{query} } : ();
+  my $get_objects       = sub {
+    my $manager_class = SL::DB::Helper::Mappings::get_manager_package_for_table($_[0]->$sub_wanted_table);
+    my $object_class  = SL::DB::Helper::Mappings::get_package_for_table($_[0]->$sub_wanted_table);
+    eval "require " . $object_class . "; 1;";
+    return @{ $manager_class->get_all(query => [ id => $_[0]->$sub_wanted_id, @get_objects_query ]) };
+  };
 
-  my $sub_wanted_table = "${wanted}_table";
-  my $sub_wanted_id    = "${wanted}_id";
+  # If no 'via' is given then use a simple(r) method for querying the wanted objects.
+  if (!$params{via}) {
+    my @query = ( "${myself}_table" => $my_table,
+                  "${myself}_id"    => $self->id );
+    push @query, ( "${wanted}_table" => $wanted_tables ) if $wanted_tables;
 
-  my $records          = [];
-  @query               = ref($params{query}) eq 'ARRAY' ? @{ $params{query} } : ();
+    return [ map { $get_objects->($_) } @{ SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]) } ];
+  }
 
-  foreach my $link (@{ $links }) {
-    my $manager_class = SL::DB::Helpers::Mappings::get_manager_package_for_table($link->$sub_wanted_table);
-    my $object_class  = SL::DB::Helpers::Mappings::get_package_for_table($link->$sub_wanted_table);
-    eval "require " . $object_class . "; 1;";
-    push @{ $records }, @{ $manager_class->get_all(query => [ id => $link->$sub_wanted_id, @query ]) };
+  # More complex handling for the 'via' case.
+  my @sources = ( $self );
+  my @targets = map { SL::DB::Helper::Mappings::get_table_for_package($_) } @{ ref($params{via}) ? $params{via} : [ $params{via} ] };
+  push @targets, @{ $wanted_tables } if $wanted_tables;
+
+  my %seen = map { ($_->meta->table . $_->id => 1) } @sources;
+
+  while (@targets) {
+    my @new_sources = @sources;
+    foreach my $src (@sources) {
+      my @query = ( "${myself}_table" => $src->meta->table,
+                    "${myself}_id"    => $src->id,
+                    "${wanted}_table" => \@targets );
+      push @new_sources,
+           map  { $get_objects->($_) }
+           grep { !$seen{$_->$sub_wanted_table . $_->$sub_wanted_id} }
+           @{ SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]) };
+    }
+
+    @sources = @new_sources;
+    %seen    = map { ($_->meta->table . $_->id => 1) } @sources;
+    shift @targets;
   }
 
-  return $records;
+  my %wanted_tables_map = map  { ($_ => 1) } @{ $wanted_tables };
+  return [ grep { $wanted_tables_map{$_->meta->table} } @sources ];
 }
 
 sub link_to_record {
@@ -88,12 +133,6 @@ sub link_to_record {
   return wantarray ? @links : $links[0];
 }
 
-sub linked_records_sorted {
-  my ($self, $sort_by, $sort_dir, %params) = @_;
-
-  return sort_linked_records($self, $sort_by, $sort_dir, $self->linked_records(%params));
-}
-
 sub sort_linked_records {
   my ($self_or_class, $sort_by, $sort_dir, @records) = @_;
 
@@ -165,6 +204,19 @@ sub sort_linked_records {
   return [ sort($comparator @records) ];
 }
 
+sub filter_linked_records {
+  my ($self_or_class, $filter, @records) = @_;
+
+  if ($filter eq 'accessible') {
+    my $employee = SL::DB::Manager::Employee->current;
+    @records     = grep { !$_->can('may_be_accessed') || $_->may_be_accessed($employee) } @records;
+  } else {
+    croak "Unsupported filter parameter '${filter}'";
+  }
+
+  return \@records;
+}
+
 1;
 
 __END__
@@ -173,7 +225,7 @@ __END__
 
 =head1 NAME
 
-SL::DB::Helpers::LinkedRecords - Mixin for retrieving linked records via the table C<record_links>
+SL::DB::Helper::LinkedRecords - Mixin for retrieving linked records via the table C<record_links>
 
 =head1 FUNCTIONS
 
@@ -189,25 +241,60 @@ from C<$self> (for C<direction> = C<from>). For C<direction = both>
 all records linked from or to C<$self> are returned.
 
 The optional parameter C<from> or C<to> (same as C<direction>)
-contains the package names of Rose models for table limitation. It can
-be a single model name as a single scalar or multiple model names in
-an array reference in which case all links matching any of the model
-names will be returned.
+contains the package names of Rose models for table limitation (the
+prefix C<SL::DB::> is optional). It can be a single model name as a
+single scalar or multiple model names in an array reference in which
+case all links matching any of the model names will be returned.
 
-If you only need invoices created from an order C<$order> then the
-call could look like this:
+The optional parameter C<via> can be used to retrieve all documents
+that may have intermediate documents inbetween. It is an array
+reference of Rose package names for the models that may be
+intermediate link targets. One example is retrieving all invoices for
+a given quotation no matter whether or not orders and delivery orders
+have been created. If C<via> is given then C<from> or C<to> (depending
+on C<direction>) must be given as well, and it must then not be an
+array reference.
+
+Examples:
+
+If you only need invoices created directly from an order C<$order> (no
+delivery orders inbetween) then the call could look like this:
 
   my $invoices = $order->linked_records(direction => 'to',
-                                        to        => 'SL::DB::Invoice');
+                                        to        => 'Invoice');
+
+Retrieving all invoices from a quotation no matter whether or not
+orders or delivery orders where created:
+
+  my $invoices = $quotation->linked_records(direction => 'to',
+                                            to        => 'Invoice',
+                                            via       => [ 'Order', 'DeliveryOrder' ]);
 
 The optional parameter C<query> can be used to limit the records
 returned. The following call limits the earlier example to invoices
 created today:
 
   my $invoices = $order->linked_records(direction => 'to',
-                                        to        => 'SL::DB::Invoice',
+                                        to        => 'Invoice',
                                         query     => [ transdate => DateTime->today_local ]);
 
+The optional parameters C<$params{sort_by}> and C<$params{sort_dir}>
+can be used in order to sort the result. If C<$params{sort_by}> is
+trueish then the result is sorted by calling L</sort_linked_records>.
+
+The optional parameter C<$params{filter}> controls whether or not the
+result is filtered. Supported values are:
+
+=over 2
+
+=item C<accessible>
+
+Removes all objects for which the function C<may_be_accessed> from the
+mixin L<SL::DB::Helper::MayBeAccessed> exists and returns falsish for
+the current employee.
+
+=back
+
 Returns an array reference.
 
 =item C<link_to_record $record, %params>
@@ -259,19 +346,12 @@ Can be called both as a class or as an instance function.
 
 This function is not exported.
 
-=item C<linked_records_sorted $sort_by, $sort_dir, %params>
-
-Returns the result of L</linked_records> sorted by
-L</sort_linked_records>. C<%params> is passed to
-L</linked_records>. C<$sort_by> and C<$sort_dir> are passed to
-L</sort_linked_records>.
-
 =back
 
 =head1 EXPORTS
 
-This mixin exports the functions L</linked_records>,
-L</link_to_record> and L</linked_records_sorted>.
+This mixin exports the functions L</linked_records> and
+L</link_to_record>.
 
 =head1 BUGS