Typos in LinkedRecords Dokumentation
[kivitendo-erp.git] / SL / DB / Helper / LinkedRecords.pm
index 3c26b01..bb21ecf 100644 (file)
@@ -8,6 +8,7 @@ our @EXPORT = qw(linked_records link_to_record);
 
 use Carp;
 use Sort::Naturally;
+use SL::DBUtils;
 
 use SL::DB::Helper::Mappings;
 use SL::DB::RecordLink;
@@ -30,7 +31,19 @@ sub _linked_records_implementation {
   my $self     = shift;
   my %params   = @_;
 
-  my $wanted   = $params{direction} || croak("Missing parameter `direction'");
+  my $wanted   = $params{direction};
+
+  if (!$wanted) {
+    if ($params{to} && $params{from}) {
+      $wanted = 'both';
+    } elsif ($params{to}) {
+      $wanted = 'to';
+    } elsif ($params{from}) {
+      $wanted = 'from';
+    } else {
+      $wanted = 'both';
+    }
+  }
 
   if ($wanted eq 'both') {
     my $both       = delete($params{both});
@@ -76,7 +89,7 @@ sub _linked_records_implementation {
   };
 
   # If no 'via' is given then use a simple(r) method for querying the wanted objects.
-  if (!$params{via}) {
+  if (!$params{via} && !$params{recursive}) {
     my @query = ( "${myself}_table" => $my_table,
                   "${myself}_id"    => $self->id );
     push @query, ( "${wanted}_table" => $wanted_tables ) if $wanted_tables;
@@ -85,31 +98,75 @@ sub _linked_records_implementation {
   }
 
   # 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 ]) };
+  if ($params{via}) {
+    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;
     }
 
-    @sources = @new_sources;
-    %seen    = map { ($_->meta->table . $_->id => 1) } @sources;
-    shift @targets;
+    my %wanted_tables_map = map  { ($_ => 1) } @{ $wanted_tables };
+    return [ grep { $wanted_tables_map{$_->meta->table} } @sources ];
   }
 
-  my %wanted_tables_map = map  { ($_ => 1) } @{ $wanted_tables };
-  return [ grep { $wanted_tables_map{$_->meta->table} } @sources ];
+  # And lastly recursive mode
+  if ($params{recursive}) {
+    # don't use rose retrieval here. too slow.
+    # instead use recursive sql to get all the linked record_links entrys, and retrieve the objects from there
+    my $query = <<"";
+      WITH RECURSIVE record_links_rec_${wanted}(id, from_table, from_id, to_table, to_id, depth, path, cycle) AS (
+        SELECT id, from_table, from_id, to_table, to_id,
+          1, ARRAY[id], false
+        FROM record_links
+        WHERE ${myself}_id = ? and ${myself}_table = ?
+      UNION ALL
+        SELECT rl.id, rl.from_table, rl.from_id, rl.to_table, rl.to_id,
+          rlr.depth + 1, path || rl.id, rl.id = ANY(path)
+        FROM record_links rl, record_links_rec_${wanted} rlr
+        WHERE rlr.${wanted}_id = rl.${myself}_id AND rlr.${wanted}_table = rl.${myself}_table AND NOT cycle
+      )
+      SELECT DISTINCT ON (${wanted}_table, ${wanted}_id)
+        id, from_table, from_id, to_table, to_id, path, depth FROM record_links_rec_${wanted}
+      WHERE NOT cycle
+      ORDER BY ${wanted}_table, ${wanted}_id, depth ASC;
+
+    my $links     = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, $self->id, $self->meta->table);
+
+    return [] unless @$links;
+
+    my $link_objs = SL::DB::Manager::RecordLink->get_all(query => [ id => [ map { $_->{id} } @$links ] ]);
+    my @objects = map { $get_objects->($_) } @$link_objs;
+
+    if ($params{save_path}) {
+       my %links_by_id = map { $_->{id} => $_ } @$links;
+       for (@objects) {
+         my $link = $links_by_id{$_->{_record_link}->id};
+         my $intermediate_links = SL::DB::Manager::RecordLink->get_all(query => [ id => $link->{path} ]);
+         $_->{_record_link_path}     = $link->{path};
+         $_->{_record_link_obj_path} = [ map { $get_objects->($_) } @$intermediate_links ];
+         $_->{_record_link_depth}    = $link->{depth};
+       }
+    }
+
+    return \@objects;
+  }
 }
 
 sub link_to_record {
@@ -132,7 +189,7 @@ sub link_to_record {
                );
 
     my $link = SL::DB::Manager::RecordLink->find_by(and => [ %data ]);
-    push @links, $link ? $link : SL::DB::RecordLink->new(%data)->save unless $link;
+    push @links, $link ? $link : SL::DB::RecordLink->new(%data)->save;
   }
 
   return wantarray ? @links : $links[0];
@@ -193,8 +250,8 @@ sub sort_linked_records {
 
   my $today     = DateTime->today_local;
   my $date_xtor = sub {
-      $_[0]->can('transdate_as_date') ? $_[0]->transdate_as_date
-    : $_[0]->can('itime_as_date')     ? $_[0]->itime_as_date
+      $_[0]->can('transdate_as_date') ? $_[0]->transdate
+    : $_[0]->can('itime_as_date')     ? $_[0]->itime->clone->truncate(to => 'day')
     :                                   $today;
   };
   my $date_comparator = sub {
@@ -234,32 +291,94 @@ __END__
 
 SL::DB::Helper::LinkedRecords - Mixin for retrieving linked records via the table C<record_links>
 
+SYNOPSIS
+
+  # In SL::DB::<Object>
+  use SL::DB::Helper::LinkedRecords;
+
+  # later in consumer code
+  # retrieve all links in both directions
+  my @linked_objects = $order->linked_records;
+
+  # only links to Invoices
+  my @linked_objects = $order->linked_records(
+    to        => 'Invoice',
+  );
+
+  # more than one target
+  my @linked_objects = $order->linked_records(
+    to        => [ 'Invoice', 'Order' ],
+  );
+
+  # more than one direction
+  my @linked_objects = $order->linked_records(
+    both      => 'Invoice',
+  );
+
+  # more than one direction and different targets
+  my @linked_objects = $order->linked_records(
+    to        => 'Invoice',
+    from      => 'Order',
+  );
+
+  # via over known classes
+  my @linked_objects = $order->linked_records(
+    to        => 'Invoice',
+    via       => 'DeliveryOrder',
+  );
+  my @linked_objects = $order->linked_records(
+    to        => 'Invoice',
+    via       => [ 'Order', 'DeliveryOrder' ],
+  );
+
+  # recursive
+  my @linked_objects = $order->linked_records(
+    recursive => 1,
+  );
+
+
+  # limit direction when further params contain additional keys
+  my %params = (to => 'Invoice', from => 'Order');
+  my @linked_objects = $order->linked_records(
+    direction => 'to',
+    %params,
+  );
+
+  # add a new link
+  $order->link_to_record($invoice);
+  $order->link_to_record($purchase_order, bidirectional => 1);
+
+
 =head1 FUNCTIONS
 
 =over 4
 
 =item C<linked_records %params>
 
-Retrieves records linked from or to C<$self> via the table
-C<record_links>. The mandatory parameter C<direction> (either C<from>,
-C<to> or C<both>) determines whether the function retrieves records
-that link to C<$self> (for C<direction> = C<to>) or that are linked
-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 (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.
-
-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
+Retrieves records linked from or to C<$self> via the table C<record_links>.
+
+The optional parameter C<direction> (either C<from>, C<to> or C<both>)
+determines whether the function retrieves records that link to C<$self> (for
+C<direction> = C<to>) or that are linked 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 (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 no parameter C<direction> is given, but any of C<to>, C<from> or C<both>,
+then C<direction> is inferred accordingly. If neither are given, C<direction> is
+set to C<both>.
+
+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:
@@ -267,23 +386,81 @@ 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        => 'Invoice');
+  my $invoices = $order->linked_records(
+    direction => 'to',
+    to        => 'Invoice',
+  );
 
 Retrieving all invoices from a quotation no matter whether or not
-orders or delivery orders where created:
+orders or delivery orders were created:
 
-  my $invoices = $quotation->linked_records(direction => 'to',
-                                            to        => 'Invoice',
-                                            via       => [ 'Order', 'DeliveryOrder' ]);
+  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        => 'Invoice',
-                                        query     => [ transdate => DateTime->today_local ]);
+  my $invoices = $order->linked_records(
+    direction => 'to',
+    to        => 'Invoice',
+    query     => [ transdate => DateTime->today_local ],
+  );
+
+In case you don't know or care which or how many objects are visited the flag
+C<recursive> can be used. It searches all reachable objects in the given direction:
+
+  my $records = $order->linked_records(
+    direction => 'to',
+    recursive => 1,
+  );
+
+Only link chains of the same type will be considered. So even with direction
+both, this
+
+  order 1 ---> invoice <--- order 2
+
+started from order 1 will only find invoice. If an object is found both in each
+direction, only one copy will be returned. The recursion is cycle protected,
+and will not recurse infinitely. Cycles are defined by the same link being
+visited twice, so this
+
+
+  order 1 ---> order 2 <--> delivery order
+                 |
+                 `--------> invoice
+
+will find the path o1 -> o2 -> do -> o2 -> i without considering it a cycle.
+
+The optional extra flag C<save_path> will give you extra information saved in
+the returned objects:
+
+  my $records = $order->linked_records(
+    direction => 'to',
+    recursive => 1,
+    save_path => 1,
+  );
+
+Every record will have two fields set:
+
+=over 2
+
+=item C<_record_link_path>
+
+An array with the ids of the visited links. The shortest paths will be
+preferred, so in the previous example this would contain the ids of o1-o2 and
+o2-i.
+
+=item C<_record_link_depth>
+
+Recursion depth when this object was found. Equal to the number of ids in
+C<_record_link_path>
+
+=back
+
 
 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
@@ -329,7 +506,7 @@ If C<$params{bidirectional}> is trueish then another link will be
 created with the roles of C<from> and C<to> reversed. This link will
 also only be created if it doesn't exist already.
 
-In scalar contenxt returns either the existing link or the newly
+In scalar context returns either the existing link or the newly
 created one as an instance of C<SL::DB::RecordLink>. In array context
 it returns an array of links (one entry if C<$params{bidirectional}>
 is falsish and two entries if it is trueish).
@@ -358,15 +535,15 @@ Sort by the record's running number.
 
 =item * C<date>
 
-Sort by the date the record was created or applies to.
+Sort by the transdate of the record was created or applies to.
 
-=back
+Note: If the latter has a default setting it will always mask the creation time.
 
-Returns a hash reference.
+=back
 
-Can be called both as a class or as an instance function.
+Returns an array reference.
 
-This function is not exported.
+Can only be called both as a class function since it is not exported.
 
 =back
 
@@ -379,8 +556,16 @@ L</link_to_record>.
 
 Nothing here yet.
 
+=head1 TODO
+
+ * C<recursive> should take a query param depth and cut off there
+ * C<recursive> uses partial distinct which is known to be not terribly fast on
+   a million entry table. replace with a better statement if this ever becomes
+   an issue.
+
 =head1 AUTHOR
 
 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
 
 =cut