]> wagnertech.de Git - mfinanz.git/blob - SL/DB/Helper/LinkedRecords.pm
restart apache2 in postinst
[mfinanz.git] / SL / DB / Helper / LinkedRecords.pm
1 package SL::DB::Helper::LinkedRecords;
2
3 use strict;
4
5 require Exporter;
6 our @ISA    = qw(Exporter);
7 our @EXPORT = qw(linked_records link_to_record sales_order_centric_linked_records);
8
9 use Carp;
10 use List::MoreUtils qw(any none);
11 use List::UtilsBy qw(uniq_by);
12 use Sort::Naturally;
13 use SL::DBUtils;
14
15 use SL::DB::Helper::Mappings;
16 use SL::DB::RecordLink;
17
18 sub linked_records {
19   my ($self, %params) = @_;
20
21   my %sort_spec       = ( by  => delete($params{sort_by}),
22                           dir => delete($params{sort_dir}) );
23   my $filter          =  delete $params{filter};
24
25   my $records         = _linked_records_implementation($self, %params);
26   $records            = filter_linked_records($self, $filter, @{ $records })                       if $filter;
27   $records            = sort_linked_records($self, $sort_spec{by}, $sort_spec{dir}, @{ $records }) if $sort_spec{by};
28
29   return $records;
30 }
31
32 sub _linked_records_implementation {
33   my $self     = shift;
34   my %params   = @_;
35
36   my $wanted   = $params{direction};
37
38   if (!$wanted) {
39     if ($params{to} && $params{from}) {
40       $wanted = 'both';
41     } elsif ($params{to}) {
42       $wanted = 'to';
43     } elsif ($params{from}) {
44       $wanted = 'from';
45     } else {
46       $wanted = 'both';
47     }
48   }
49
50   if ($wanted eq 'both') {
51     my $both       = delete($params{both});
52     my %from_to    = ( from => delete($params{from}) || $both,
53                        to   => delete($params{to})   || $both);
54
55     if ($params{batch} && $params{by_id}) {
56       my %results;
57       my @links = (
58         _linked_records_implementation($self, %params, direction => 'from', from => $from_to{from}),
59         _linked_records_implementation($self, %params, direction => 'to',   to   => $from_to{to}  ),
60       );
61
62       for my $by_id (@links) {
63         for (keys %$by_id) {
64           $results{$_} = defined $results{$_}
65                        ? [ uniq_by { $_->id } @{ $results{$_} }, @{ $by_id->{$_} } ]
66                        : $by_id->{$_};
67         }
68       }
69
70       return \%results;
71     } else {
72       my @records    = (@{ _linked_records_implementation($self, %params, direction => 'from', from => $from_to{from}) },
73                         @{ _linked_records_implementation($self, %params, direction => 'to',   to   => $from_to{to}  ) });
74
75       my %record_map = map { ( ref($_) . $_->id => $_ ) } @records;
76
77       return [ values %record_map ];
78     }
79   }
80
81   if ($params{via}) {
82     croak("Cannot use 'via' without '${wanted}_table'")             if !$params{$wanted};
83     croak("Cannot use 'via' with '${wanted}_table' being an array") if ref $params{$wanted};
84   }
85
86   my $myself           = $wanted eq 'from' ? 'to' : $wanted eq 'to' ? 'from' : croak("Invalid parameter `direction'");
87   my $my_table         = SL::DB::Helper::Mappings::get_table_for_package(ref($self));
88
89   my $sub_wanted_table = "${wanted}_table";
90   my $sub_wanted_id    = "${wanted}_id";
91   my $sub_myself_id    = "${myself}_id";
92
93   my ($wanted_classes, $wanted_tables);
94   if ($params{$wanted}) {
95     $wanted_classes = ref($params{$wanted}) eq 'ARRAY' ? $params{$wanted} : [ $params{$wanted} ];
96     $wanted_tables  = [ map { SL::DB::Helper::Mappings::get_table_for_package($_) || croak("Invalid parameter `${wanted}'") } @{ $wanted_classes } ];
97   }
98
99   my @get_objects_query = ref($params{query}) eq 'ARRAY' ? @{ $params{query} } : ();
100   my $get_objects       = sub {
101     my ($links)        = @_;
102     return [] unless @$links;
103
104     my %classes;
105     push @{ $classes{ $_->$sub_wanted_table } //= [] }, $_->$sub_wanted_id for @$links;
106
107     my @objs;
108     for (keys %classes) {
109       my $manager_class = SL::DB::Helper::Mappings::get_manager_package_for_table($_);
110       my $object_class  = SL::DB::Helper::Mappings::get_package_for_table($_);
111       eval "require " . $object_class . "; 1;";
112
113       push @objs, @{ $manager_class->get_all(
114         query         => [ id => $classes{$_}, @get_objects_query ],
115         (with_objects => $params{with_objects}) x !!$params{with_objects},
116         inject_results => 1,
117       ) };
118     }
119
120     my %objs_by_id = map { $_->id => $_ } @objs;
121
122     for (@$links) {
123       if ('ARRAY' eq ref $objs_by_id{$_->$sub_wanted_id}->{_record_link}) {
124         push @{ $objs_by_id{$_->$sub_wanted_id}->{_record_link_direction} }, $wanted;
125         push @{ $objs_by_id{$_->$sub_wanted_id}->{_record_link          } }, $_;
126       } elsif ($objs_by_id{$_->$sub_wanted_id}->{_record_link}) {
127         $objs_by_id{$_->$sub_wanted_id}->{_record_link_direction} = [
128           $objs_by_id{$_->$sub_wanted_id}->{_record_link_direction},
129           $wanted,
130         ];
131         $objs_by_id{$_->$sub_wanted_id}->{_record_link}           = [
132           $objs_by_id{$_->$sub_wanted_id}->{_record_link},
133           $_,
134         ];
135       } else {
136         $objs_by_id{$_->$sub_wanted_id}->{_record_link_direction} = $wanted;
137         $objs_by_id{$_->$sub_wanted_id}->{_record_link}           = $_;
138       }
139     }
140
141     return \@objs;
142   };
143
144   # If no 'via' is given then use a simple(r) method for querying the wanted objects.
145   if (!$params{via} && !$params{recursive}) {
146     my @query = ( "${myself}_table" => $my_table,
147                   "${myself}_id"    => $params{batch} ? $params{batch} : $self->id );
148     push @query, ( "${wanted}_table" => $wanted_tables ) if $wanted_tables;
149
150     my $links = SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]);
151     my $objs  = $get_objects->($links);
152
153     if ($params{batch} && $params{by_id}) {
154       return {
155         map {
156           my $id = $_;
157           $_ => [
158             grep {
159               $_->{_record_link}->$sub_myself_id == $id
160             } @$objs
161           ]
162         } @{ $params{batch} }
163       }
164     } else {
165       return $objs;
166     }
167   }
168
169   # More complex handling for the 'via' case.
170   if ($params{via}) {
171     die 'batch mode is not supported with via' if $params{batch};
172
173     my @sources = ( $self );
174     my @targets = map { SL::DB::Helper::Mappings::get_table_for_package($_) } @{ ref($params{via}) ? $params{via} : [ $params{via} ] };
175     push @targets, @{ $wanted_tables } if $wanted_tables;
176
177     my %seen = map { ($_->meta->table . $_->id => 1) } @sources;
178
179     while (@targets) {
180       my @new_sources = @sources;
181       foreach my $src (@sources) {
182         my @query = ( "${myself}_table" => $src->meta->table,
183                       "${myself}_id"    => $src->id,
184                       "${wanted}_table" => \@targets );
185         push @new_sources,
186              @{ $get_objects->([
187                grep { !$seen{$_->$sub_wanted_table . $_->$sub_wanted_id} }
188                @{ SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]) }
189              ]) };
190       }
191
192       @sources = @new_sources;
193       %seen    = map { ($_->meta->table . $_->id => 1) } @sources;
194       shift @targets;
195     }
196
197     my %wanted_tables_map = map  { ($_ => 1) } @{ $wanted_tables };
198     return [ grep { $wanted_tables_map{$_->meta->table} } @sources ];
199   }
200
201   # And lastly recursive mode
202   if ($params{recursive}) {
203     my ($id_token, @ids);
204     if ($params{batch}) {
205       $id_token = sprintf 'IN (%s)', join ', ', ('?') x @{ $params{batch} };
206       @ids      = @{ $params{batch} };
207     } else {
208       $id_token = '= ?';
209       @ids      = ($self->id);
210     }
211
212     # don't use rose retrieval here. too slow.
213     # instead use recursive sql to get all the linked record_links entries and retrieve the objects from there
214     my $query = <<"";
215       WITH RECURSIVE record_links_rec_${wanted}(id, from_table, from_id, to_table, to_id, depth, path, cycle) AS (
216         SELECT id, from_table, from_id, to_table, to_id,
217           1, ARRAY[id], false
218         FROM record_links
219         WHERE ${myself}_id $id_token and ${myself}_table = ?
220       UNION ALL
221         SELECT rl.id, rl.from_table, rl.from_id, rl.to_table, rl.to_id,
222           rlr.depth + 1, path || rl.id, rl.id = ANY(path)
223         FROM record_links rl, record_links_rec_${wanted} rlr
224         WHERE rlr.${wanted}_id = rl.${myself}_id AND rlr.${wanted}_table = rl.${myself}_table AND NOT cycle
225       )
226       SELECT DISTINCT ON (${wanted}_table, ${wanted}_id)
227         id, from_table, from_id, to_table, to_id, path, depth FROM record_links_rec_${wanted}
228       WHERE NOT cycle
229       ORDER BY ${wanted}_table, ${wanted}_id, depth ASC;
230
231     my $links     = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, @ids, $self->meta->table);
232
233     if (!@$links) {
234       return $params{by_id} ? {} : [];
235     }
236
237     my $link_objs = SL::DB::Manager::RecordLink->get_all(query => [ id => [ map { $_->{id} } @$links ] ]);
238     my $objects = $get_objects->($link_objs);
239
240     my %links_by_id = map { $_->{id} => $_ } @$links;
241
242     if ($params{save_path}) {
243        for (@$objects) {
244          for my $record_link ('ARRAY' eq ref $_->{_record_link} ? @{ $_->{_record_link} } : $_->{_record_link}) {
245            my $link = $links_by_id{$record_link->id};
246            my $intermediate_links = SL::DB::Manager::RecordLink->get_all(query => [ id => $link->{path} ]);
247            $_->{_record_link_path}     = $link->{path};
248            $_->{_record_link_obj_path} = $get_objects->($intermediate_links);
249            $_->{_record_link_depth}    = $link->{depth};
250          }
251        }
252     }
253
254     if ($params{batch} && $params{by_id}) {
255       my %link_obj_by_id = map { $_->id => $_ } @$link_objs;
256       return +{
257         map {
258          my $id = $_;
259          $id => [
260            grep {
261              any {
262                $link_obj_by_id{
263                  $links_by_id{$_->id}->{path}->[0]
264                 }->$sub_myself_id == $id
265              } 'ARRAY' eq $_->{_record_link} ? @{ $_->{_record_link} } : $_->{_record_link}
266            } @$objects
267          ]
268         } @{ $params{batch} }
269       };
270     } else {
271       return $objects;
272     }
273   }
274 }
275
276 sub link_to_record {
277   my $self   = shift;
278   my $other  = shift;
279   my %params = @_;
280
281   croak "self has no id"  unless $self->id;
282   croak "other has no id" unless $other->id;
283
284   my @directions = ([ 'from', 'to' ]);
285   push @directions, [ 'to', 'from' ] if $params{bidirectional};
286   my @links;
287
288   foreach my $direction (@directions) {
289     my %data = ( $direction->[0] . "_table" => SL::DB::Helper::Mappings::get_table_for_package(ref($self)),
290                  $direction->[0] . "_id"    => $self->id,
291                  $direction->[1] . "_table" => SL::DB::Helper::Mappings::get_table_for_package(ref($other)),
292                  $direction->[1] . "_id"    => $other->id,
293                );
294
295     my $link = SL::DB::Manager::RecordLink->find_by(and => [ %data ]);
296     push @links, $link ? $link : SL::DB::RecordLink->new(%data)->save;
297   }
298
299   return wantarray ? @links : $links[0];
300 }
301
302 sub sort_linked_records {
303   my ($self_or_class, $sort_by, $sort_dir, @records) = @_;
304
305   @records  = @{ $records[0] } if (1 == scalar(@records)) && (ref($records[0]) eq 'ARRAY');
306   $sort_dir = $sort_dir * 1 ? 1 : -1;
307
308   my %numbers = ( 'SL::DB::SalesProcess'    => sub { $_[0]->id },
309                   'SL::DB::Order'           => sub { $_[0]->record_number },
310                   'SL::DB::DeliveryOrder'   => sub { $_[0]->donumber },
311                   'SL::DB::Invoice'         => sub { $_[0]->invnumber },
312                   'SL::DB::PurchaseInvoice' => sub { $_[0]->invnumber },
313                   'SL::DB::RequirementSpec' => sub { $_[0]->id },
314                   'SL::DB::Letter'          => sub { $_[0]->letternumber },
315                   'SL::DB::ShopOrder'       => sub { $_[0]->shop_ordernumber },
316                   'SL::DB::EmailJournal'    => sub { $_[0]->id },
317                   'SL::DB::Dunning'         => sub { $_[0]->dunning_id },
318                   'SL::DB::GLTransaction'   => sub { $_[0]->reference },
319                   UNKNOWN                   => '9999999999999999',
320                 );
321   my $number_xtor = sub {
322     my $number = $numbers{ ref($_[0]) };
323     $number    = $number->($_[0]) if ref($number) eq 'CODE';
324     return $number || $numbers{UNKNOWN};
325   };
326   my $number_comparator = sub {
327     my $number_a = $number_xtor->($a);
328     my $number_b = $number_xtor->($b);
329
330     ncmp($number_a, $number_b) * $sort_dir;
331   };
332
333   my %scores;
334   %scores = ( 'SL::DB::SalesProcess'      => 10,
335               'SL::DB::RequirementSpec'   => 15,
336               'SL::DB::Order'             => sub { $scores{ $_[0]->type } },
337               sales_quotation             => 20,
338               sales_order_intake          => 25,
339               sales_order                 => 30,
340               sales_delivery_order        => 40,
341               'SL::DB::DeliveryOrder'     => sub { $scores{ $_[0]->type } },
342               'SL::DB::Invoice'           => 50,
343               request_quotation           => 120,
344               purchase_quotation_intake   => 125,
345               purchase_order              => 130,
346               purchase_order_confirmation => 135,
347               purchase_delivery_order     => 140,
348               'SL::DB::PurchaseInvoice'   => 150,
349               'SL::DB::GLTransaction'     => 170,
350               'SL::DB::Letter'            => 200,
351               'SL::DB::ShopOrder'         => 250,
352               'SL::DB::EmailJournal'      => 300,
353               'SL::DB::Dunning'           => 350,
354               UNKNOWN                     => 999,
355             );
356   my $score_xtor = sub {
357     my $score = $scores{ ref($_[0]) };
358     $score    = $score->($_[0]) if ref($score) eq 'CODE';
359     return $score || $scores{UNKNOWN};
360   };
361   my $type_comparator = sub {
362     my $score_a = $score_xtor->($a);
363     my $score_b = $score_xtor->($b);
364
365     $score_a == $score_b ? $number_comparator->() : ($score_a <=> $score_b) * $sort_dir;
366   };
367
368   my $today     = DateTime->today_local;
369   my $date_xtor = sub {
370       $_[0]->can('transdate_as_date') ? $_[0]->transdate
371     : $_[0]->can('itime_as_date')     ? $_[0]->itime->clone->truncate(to => 'day')
372     :                                   $today;
373   };
374   my $date_comparator = sub {
375     my $date_a = $date_xtor->($a);
376     my $date_b = $date_xtor->($b);
377
378     ($date_a <=> $date_b) * $sort_dir;
379   };
380
381   my $comparator = $sort_by eq 'number' ? $number_comparator
382                  : $sort_by eq 'date'   ? $date_comparator
383                  :                        $type_comparator;
384
385   return [ sort($comparator @records) ];
386 }
387
388 sub filter_linked_records {
389   my ($self_or_class, $filter, @records) = @_;
390
391   if ($filter eq 'accessible') {
392     my $employee = SL::DB::Manager::Employee->current;
393     @records     = grep { !$_->can('may_be_accessed') || $_->may_be_accessed($employee) } @records;
394   } else {
395     croak "Unsupported filter parameter '${filter}'";
396   }
397
398   return \@records;
399 }
400
401 sub sales_order_centric_linked_records {
402   my ($self, %params) = @_;
403
404   my $with_sales_quotations = $params{with_sales_quotations};
405   my $with_myself           = $params{with_myself};
406
407   my $all_linked_records = $self->linked_records(direction => 'both', recursive => 1, save_path => 1);
408
409   if (!$with_sales_quotations) {
410     $all_linked_records = [ grep { !('SL::DB::Order' eq ref $_ && $_->is_sales && $_->quotation) } @$all_linked_records ];
411   }
412
413   if ($with_myself) {
414     $self->{_record_link_to_myself} = 1;
415     push @$all_linked_records, $self;
416   }
417
418   my $filtered_orders = [ grep {
419     'SL::DB::Order' eq ref $_ &&
420     ($_->is_type('sales_order') || $_->is_type('sales_order_intake')) &&
421     $_->{_record_link_direction} eq 'from'
422   } @$all_linked_records ];
423
424   # no orders no call to linked_records via batch mode
425   # but instead return default list
426   return $all_linked_records unless scalar @$filtered_orders;
427
428   # we have an order, therefore get the tree view from the top (order)
429   my $id_ref = [ map { $_->id } @$filtered_orders ];
430
431   my $linked_records = SL::DB::Order->new->linked_records(direction => 'to', recursive => 1, batch => $id_ref);
432
433   # Remove entries that are already in all_linked_records.
434   $linked_records = [ grep { my $id = $_->id; none { $_->id == $id } @$all_linked_records } @$linked_records ];
435
436   # Remove quotations if requested.
437   if (!$with_sales_quotations) {
438     $linked_records = [ grep { !('SL::DB::Order' eq ref $_ && $_->is_sales && $_->quotation) } @$linked_records ];
439   }
440
441   # Mark or remove myself.
442   if ($with_myself) {
443     $_->{_record_link_to_myself}  = 1 for grep { $_->id == $self->id } @$linked_records;
444   } else {
445     $linked_records = [ grep { $_->id != $self->id } @$linked_records ];
446   }
447
448   # All remaining links found via order are two more steps away from myself.
449   $_->{_record_link_depth} += 2 for @{ $linked_records };
450
451   push @{ $linked_records }, @$all_linked_records;
452
453   return $linked_records;
454 }
455
456 1;
457
458 __END__
459
460 =encoding utf8
461
462 =head1 NAME
463
464 SL::DB::Helper::LinkedRecords - Mixin for retrieving linked records via the table C<record_links>
465
466 =head1 SYNOPSIS
467
468   # In SL::DB::<Object>
469   use SL::DB::Helper::LinkedRecords;
470
471   # later in consumer code
472   # retrieve all links in both directions
473   my @linked_objects = $order->linked_records;
474
475   # only links to Invoices
476   my @linked_objects = $order->linked_records(
477     to        => 'Invoice',
478   );
479
480   # more than one target
481   my @linked_objects = $order->linked_records(
482     to        => [ 'Invoice', 'Order' ],
483   );
484
485   # more than one direction
486   my @linked_objects = $order->linked_records(
487     both      => 'Invoice',
488   );
489
490   # more than one direction and different targets
491   my @linked_objects = $order->linked_records(
492     to        => 'Invoice',
493     from      => 'Order',
494   );
495
496   # via over known classes
497   my @linked_objects = $order->linked_records(
498     to        => 'Invoice',
499     via       => 'DeliveryOrder',
500   );
501   my @linked_objects = $order->linked_records(
502     to        => 'Invoice',
503     via       => [ 'Order', 'DeliveryOrder' ],
504   );
505
506   # recursive
507   my @linked_objects = $order->linked_records(
508     recursive => 1,
509   );
510
511
512   # limit direction when further params contain additional keys
513   my %params = (to => 'Invoice', from => 'Order');
514   my @linked_objects = $order->linked_records(
515     direction => 'to',
516     %params,
517   );
518
519   # get order centric linked records
520   $invoice->sales_order_centric_linked_records(
521     with_myself           => 1,
522     with_sales_quotations => 1
523   );
524
525   # add a new link
526   $order->link_to_record($invoice);
527   $order->link_to_record($purchase_order, bidirectional => 1);
528
529
530 =head1 FUNCTIONS
531
532 =over 4
533
534 =item C<linked_records %params>
535
536 Retrieves records linked from or to C<$self> via the table C<record_links>.
537
538 The optional parameter C<direction> (either C<from>, C<to> or C<both>)
539 determines whether the function retrieves records that link to C<$self> (for
540 C<direction> = C<to>) or that are linked from C<$self> (for C<direction> =
541 C<from>). For C<direction = both> all records linked from or to C<$self> are
542 returned.
543
544 The optional parameter C<from> or C<to> (same as C<direction>) contains the
545 package names of Rose models for table limitation (the prefix C<SL::DB::> is
546 optional). It can be a single model name as a single scalar or multiple model
547 names in an array reference in which case all links matching any of the model
548 names will be returned.
549
550 If no parameter C<direction> is given, but any of C<to>, C<from> or C<both>,
551 then C<direction> is inferred accordingly. If neither are given, C<direction> is
552 set to C<both>.
553
554 The optional parameter C<via> can be used to retrieve all documents that may
555 have intermediate documents inbetween. It is an array reference of Rose package
556 names for the models that may be intermediate link targets. One example is
557 retrieving all invoices for a given quotation no matter whether or not orders
558 and delivery orders have been created. If C<via> is given then C<from> or C<to>
559 (depending on C<direction>) must be given as well, and it must then not be an
560 array reference.
561
562 Examples:
563
564 If you only need invoices created directly from an order C<$order> (no
565 delivery orders in between) then the call could look like this:
566
567   my $invoices = $order->linked_records(
568     direction => 'to',
569     to        => 'Invoice',
570   );
571
572 Retrieving all invoices from a quotation no matter whether or not
573 orders or delivery orders were created:
574
575   my $invoices = $quotation->linked_records(
576     direction => 'to',
577     to        => 'Invoice',
578     via       => [ 'Order', 'DeliveryOrder' ],
579   );
580
581 The optional parameter C<query> can be used to limit the records
582 returned. The following call limits the earlier example to invoices
583 created today:
584
585   my $invoices = $order->linked_records(
586     direction => 'to',
587     to        => 'Invoice',
588     query     => [ transdate => DateTime->today_local ],
589   );
590
591 In case you don't know or care which or how many objects are visited the flag
592 C<recursive> can be used. It searches all reachable objects in the given direction:
593
594   my $records = $order->linked_records(
595     direction => 'to',
596     recursive => 1,
597   );
598
599 Only link chains of the same type will be considered. So even with direction
600 both, this
601
602   order 1 ---> invoice <--- order 2
603
604 started from order 1 will only find invoice. If an object is found both in each
605 direction, only one copy will be returned. The recursion is cycle protected,
606 and will not recurse infinitely. Cycles are defined by the same link being
607 visited twice, so this
608
609
610   order 1 ---> order 2 <--> delivery order
611                  |
612                  `--------> invoice
613
614 will find the path o1 -> o2 -> do -> o2 -> i without considering it a cycle.
615
616 The optional extra flag C<save_path> will give you extra information saved in
617 the returned objects:
618
619   my $records = $order->linked_records(
620     direction => 'to',
621     recursive => 1,
622     save_path => 1,
623   );
624
625 Every record will have two fields set:
626
627 =over 2
628
629 =item C<_record_link_path>
630
631 An array with the ids of the visited links. The shortest paths will be
632 preferred, so in the previous example this would contain the ids of o1-o2 and
633 o2-i.
634
635 =item C<_record_link_depth>
636
637 Recursion depth when this object was found. Equal to the number of ids in
638 C<_record_link_path>
639
640 =back
641
642 Since record_links is comparatively expensive to call, you will want to cache
643 the results for multiple objects if you know in advance you'll need them.
644
645 You can pass the optional argument C<batch> with an array ref of ids which will
646 be used instead of the id of the invocant. You still need to call it as a
647 method on a valid object, because table information is inferred from there.
648
649 C<batch> mode will currenty not work with C<via>.
650
651 The optional flag C<by_id> will return the objects sorted into a hash instead
652 of a plain array. Calling C<<recursive => 1, batch => [1,2], by_id => 1>> on
653  order 1:
654
655   order 1 --> delivery order 1 --> invoice 1
656   order 2 --> delivery order 2 --> invoice 2
657
658 will give you:
659
660   { 1 => [ delivery order 1, invoice 1 ],
661     2 => [ delivery order 2, invoice 1 ], }
662
663 you may then cache these as you see fit.
664
665
666 The optional parameters C<$params{sort_by}> and C<$params{sort_dir}>
667 can be used in order to sort the result. If C<$params{sort_by}> is
668 trueish then the result is sorted by calling L</sort_linked_records>.
669
670 The optional parameter C<$params{filter}> controls whether or not the
671 result is filtered. Supported values are:
672
673 =over 2
674
675 =item C<accessible>
676
677 Removes all objects for which the function C<may_be_accessed> from the
678 mixin L<SL::DB::Helper::MayBeAccessed> exists and returns falsish for
679 the current employee.
680
681 =back
682
683 Returns an array reference. Each element returned is a Rose::DB
684 instance. Additionally several elements in the element returned are
685 set to special values:
686
687 =over 2
688
689 =item C<_record_link_direction>
690
691 Either C<from> or C<to> indicating the direction. C<from> means that
692 this object is the source in the link.
693
694 =item C<_record_link>
695
696 The actual database link object (an instance of L<SL::DB::RecordLink>).
697
698 =back
699
700 =item C<link_to_record $record, %params>
701
702 Will create an entry in the table C<record_links> with the C<from>
703 side being C<$self> and the C<to> side being C<$record>. Will only
704 insert a new entry if such a link does not already exist.
705
706 If C<$params{bidirectional}> is trueish then another link will be
707 created with the roles of C<from> and C<to> reversed. This link will
708 also only be created if it doesn't exist already.
709
710 In scalar context returns either the existing link or the newly
711 created one as an instance of C<SL::DB::RecordLink>. In array context
712 it returns an array of links (one entry if C<$params{bidirectional}>
713 is falsish and two entries if it is trueish).
714
715 =item C<sort_linked_records $sort_by, $sort_dir, @records>
716
717 Sorts linked records by C<$sort_by> in the direction given by
718 C<$sort_dir> (trueish = ascending, falsish = descending). C<@records>
719 can be either a single array reference or or normal array.
720
721 C<$sort_by> can be one of the following strings:
722
723 =over 2
724
725 =item * C<type>
726
727 Sort by type first and by record number second. The type order
728 reflects the order in which records are usually processed by the
729 employees: sales processes, sales quotations, sales orders, sales
730 delivery orders, invoices; requests for quotation, purchase orders,
731 purchase delivery orders, purchase invoices.
732
733 =item * C<number>
734
735 Sort by the record's running number.
736
737 =item * C<date>
738
739 Sort by the transdate of the record was created or applies to.
740
741 Note: If the latter has a default setting it will always mask the creation time.
742
743 =back
744
745 Returns an array reference.
746
747 Can only be called as a class function since it is not exported.
748
749 =item C<sales_order_centric_linked_records %params>
750
751 Get linked records from the view of a reachable sales order in the path
752 prior to this record. If no sales order is found, recursive linked records
753 for both directions are returned.
754
755 =over 2
756
757 =item * C<with_sales_quotations>
758
759 Since the view from a sales order is requested, normally no sales quotation
760 prior to the sales order will be returned. If this parameter is truish, then
761 a sales quotation would be included.
762 This parameter is optional and defaults to false.
763
764 =item * C<with_myself>
765
766 The records returned by L</linked_records> do not include the requesting record
767 itself. However, if you want to display the same linked records for every
768 record in the same workflow, it can be usefull to include the record itself.
769 C<_record_link_to_myself> will be set to a truish value in that record in
770 that case.
771 This parameter is optional and defaults to false.
772
773 =back
774
775 =back
776
777 =head1 EXPORTS
778
779 This mixin exports the functions L</linked_records>,
780 L</sales_order_centric_linked_records> and
781 L</link_to_record>.
782
783 =head1 BUGS
784
785 Nothing here yet.
786
787 =head1 TODO
788
789  * C<recursive> should take a query param depth and cut off there
790  * C<recursive> uses partial distinct which is known to be not terribly fast on
791    a million entry table. replace with a better statement if this ever becomes
792    an issue.
793
794 =head1 AUTHOR
795
796 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
797 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
798
799 =cut