Allgemeine Presenter-Methode für records (ar/ap/gl)
[kivitendo-erp.git] / SL / Presenter / Record.pm
1 package SL::Presenter::Record;
2
3 use strict;
4
5 use parent qw(Exporter);
6
7 use Exporter qw(import);
8 our @EXPORT = qw(grouped_record_list empty_record_list record_list record);
9
10 use SL::Util;
11
12 use Carp;
13 use List::Util qw(first);
14
15 sub _arrayify {
16   my ($array) = @_;
17   return []     if !defined $array;
18   return $array if ref $array;
19   return [ $array ];
20 }
21
22 sub record {
23   my ($self, $record, %params) = @_;
24
25   my %grouped = _group_records( [ $record ] ); # pass $record as arrayref
26   my $type    = (keys %grouped)[0];
27
28   return $self->sales_invoice(   $record, %params) if $type eq 'sales_invoices';
29   return $self->purchase_invoice($record, %params) if $type eq 'purchase_invoices';
30   return $self->ar_transaction(  $record, %params) if $type eq 'ar_transactions';
31   return $self->ap_transaction(  $record, %params) if $type eq 'ap_transactions';
32   return $self->gl_transaction(  $record, %params) if $type eq 'gl_transactions';
33
34   return '';
35 }
36
37 sub grouped_record_list {
38   my ($self, $list, %params) = @_;
39
40   %params    = map { exists $params{$_} ? ($_ => $params{$_}) : () } qw(edit_record_links with_columns object_id object_model);
41
42   my %groups = _sort_grouped_lists(_group_records($list));
43   my $output = '';
44
45   $output .= _requirement_spec_list(       $self, $groups{requirement_specs},        %params) if $groups{requirement_specs};
46   $output .= _sales_quotation_list(        $self, $groups{sales_quotations},         %params) if $groups{sales_quotations};
47   $output .= _sales_order_list(            $self, $groups{sales_orders},             %params) if $groups{sales_orders};
48   $output .= _sales_delivery_order_list(   $self, $groups{sales_delivery_orders},    %params) if $groups{sales_delivery_orders};
49   $output .= _sales_invoice_list(          $self, $groups{sales_invoices},           %params) if $groups{sales_invoices};
50   $output .= _ar_transaction_list(         $self, $groups{ar_transactions},          %params) if $groups{ar_transactions};
51
52   $output .= _request_quotation_list(      $self, $groups{purchase_quotations},      %params) if $groups{purchase_quotations};
53   $output .= _purchase_order_list(         $self, $groups{purchase_orders},          %params) if $groups{purchase_orders};
54   $output .= _purchase_delivery_order_list($self, $groups{purchase_delivery_orders}, %params) if $groups{purchase_delivery_orders};
55   $output .= _purchase_invoice_list(       $self, $groups{purchase_invoices},        %params) if $groups{purchase_invoices};
56   $output .= _ap_transaction_list(         $self, $groups{ap_transactions},          %params) if $groups{ap_transactions};
57
58   $output .= _sepa_collection_list(        $self, $groups{sepa_collections},         %params) if $groups{sepa_collections};
59   $output .= _sepa_transfer_list(          $self, $groups{sepa_transfers},           %params) if $groups{sepa_transfers};
60
61   $output  = $self->render('presenter/record/grouped_record_list', %params, output => $output);
62
63   return $output;
64 }
65
66 sub empty_record_list {
67   my ($self, %params) = @_;
68   return $self->grouped_record_list([], %params);
69 }
70
71 sub record_list {
72   my ($self, $list, %params) = @_;
73
74   my @columns;
75
76   if (ref($params{columns}) eq 'ARRAY') {
77     @columns = map {
78       if (ref($_) eq 'ARRAY') {
79         { title => $_->[0], data => $_->[1], link => $_->[2] }
80       } else {
81         $_;
82       }
83     } @{ delete $params{columns} };
84
85   } else {
86     croak "Wrong type for 'columns' argument: not an array reference";
87   }
88
89   my %with_columns = map { ($_ => 1) } @{ _arrayify($params{with_columns}) };
90   if ($with_columns{record_link_direction}) {
91     push @columns, {
92       title => $::locale->text('Link direction'),
93       data  => sub {
94           $_[0]->{_record_link_depth} > 1
95         ? $::locale->text('Row was linked to another record')
96         : $_[0]->{_record_link_direction} eq 'from'
97         ? $::locale->text('Row was source for current record')
98         : $::locale->text('Row was created from current record') },
99     };
100   }
101
102   my %column_meta   = map { $_->name => $_ } @{ $list->[0]->meta->columns       };
103   my %relationships = map { $_->name => $_ } @{ $list->[0]->meta->relationships };
104
105   my $call = sub {
106     my ($obj, $method, @args) = @_;
107     $obj->$method(@args);
108   };
109
110   my @data;
111   foreach my $obj (@{ $list }) {
112     my @row;
113
114     foreach my $spec (@columns) {
115       my %cell;
116
117       my $method       =  $spec->{column} || $spec->{data};
118       my $meta         =  $column_meta{ $spec->{data} };
119       my $type         =  ref $meta;
120       my $relationship =  $relationships{ $spec->{data} };
121       my $rel_type     =  !$relationship ? '' : $relationship->class;
122       $rel_type        =~ s/^SL::DB:://;
123       $rel_type        =  SL::Util::snakify($rel_type);
124
125       if (ref($spec->{data}) eq 'CODE') {
126         $cell{value} = $spec->{data}->($obj);
127
128       } else {
129         $cell{value} = $rel_type && $self->can($rel_type)                                       ? $self->$rel_type($obj->$method, display => 'table-cell')
130                      : $type eq 'Rose::DB::Object::Metadata::Column::Date'                      ? $call->($obj, $method . '_as_date')
131                      : $type =~ m/^Rose::DB::Object::Metadata::Column::(?:Float|Numeric|Real)$/ ? $::form->format_amount(\%::myconfig, $call->($obj, $method), 2)
132                      : $type eq 'Rose::DB::Object::Metadata::Column::Boolean'                   ? $call->($obj, $method . '_as_bool_yn')
133                      : $type =~ m/^Rose::DB::Object::Metadata::Column::(?:Integer|Serial)$/     ? $spec->{data} * 1
134                      :                                                                            $call->($obj, $method);
135       }
136
137       $cell{alignment} = 'right' if $type =~ m/int|serial|float|real|numeric/;
138
139       push @row, \%cell;
140     }
141
142     push @data, { columns => \@row, record_link => $obj->{_record_link} };
143   }
144
145   my @header =
146     map +{ value     => $columns[$_]->{title},
147            alignment => $data[0]->{columns}->[$_]->{alignment},
148          }, (0..scalar(@columns) - 1);
149
150   return $self->render(
151     'presenter/record/record_list',
152     %params,
153     TABLE_HEADER => \@header,
154     TABLE_ROWS   => \@data,
155   );
156 }
157
158 #
159 # private methods
160 #
161
162 sub _group_records {
163   my ($list) = @_;
164
165   my %matchers = (
166     requirement_specs        => sub { (ref($_[0]) eq 'SL::DB::RequirementSpec')                                         },
167     sales_quotations         => sub { (ref($_[0]) eq 'SL::DB::Order')           &&  $_[0]->is_type('sales_quotation')   },
168     sales_orders             => sub { (ref($_[0]) eq 'SL::DB::Order')           &&  $_[0]->is_type('sales_order')       },
169     sales_delivery_orders    => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder')   &&  $_[0]->is_sales                     },
170     sales_invoices           => sub { (ref($_[0]) eq 'SL::DB::Invoice')         &&  $_[0]->invoice                      },
171     ar_transactions          => sub { (ref($_[0]) eq 'SL::DB::Invoice')         && !$_[0]->invoice                      },
172     purchase_quotations      => sub { (ref($_[0]) eq 'SL::DB::Order')           &&  $_[0]->is_type('request_quotation') },
173     purchase_orders          => sub { (ref($_[0]) eq 'SL::DB::Order')           &&  $_[0]->is_type('purchase_order')    },
174     purchase_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder')   && !$_[0]->is_sales                     },
175     purchase_invoices        => sub { (ref($_[0]) eq 'SL::DB::PurchaseInvoice') &&  $_[0]->invoice                      },
176     ap_transactions          => sub { (ref($_[0]) eq 'SL::DB::PurchaseInvoice') && !$_[0]->invoice                      },
177     sepa_collections         => sub { (ref($_[0]) eq 'SL::DB::SepaExportItem')  &&  $_[0]->ar_id                        },
178     sepa_transfers           => sub { (ref($_[0]) eq 'SL::DB::SepaExportItem')  &&  $_[0]->ap_id                        },
179     gl_transactions          => sub { (ref($_[0]) eq 'SL::DB::GLTransaction')                                           },
180   );
181
182   my %groups;
183
184   foreach my $record (@{ $list || [] }) {
185     my $type         = (first { $matchers{$_}->($record) } keys %matchers) || 'other';
186     $groups{$type} ||= [];
187     push @{ $groups{$type} }, $record;
188   }
189
190   return %groups;
191 }
192
193 sub _sort_grouped_lists {
194   my (%groups) = @_;
195
196   foreach my $group (keys %groups) {
197     next unless @{ $groups{$group} };
198     if ($groups{$group}->[0]->can('compare_to')) {
199       $groups{$group} = [ sort { $a->compare_to($b)    } @{ $groups{$group} } ];
200     } else {
201       $groups{$group} = [ sort { $a->date <=> $b->date } @{ $groups{$group} } ];
202     }
203   }
204
205   return %groups;
206 }
207
208 sub _requirement_spec_list {
209   my ($self, $list, %params) = @_;
210
211   return $self->record_list(
212     $list,
213     title   => $::locale->text('Requirement specs'),
214     type    => 'requirement_spec',
215     columns => [
216       [ $::locale->text('Requirement spec number'), sub { $self->requirement_spec($_[0], display => 'table-cell') } ],
217       [ $::locale->text('Customer'),                'customer'                                                      ],
218       [ $::locale->text('Title'),                   'title'                                                         ],
219       [ $::locale->text('Project'),                 'project',                                                      ],
220       [ $::locale->text('Status'),                  sub { $_[0]->status->description }                              ],
221     ],
222     %params,
223   );
224 }
225
226 sub _sales_quotation_list {
227   my ($self, $list, %params) = @_;
228
229   return $self->record_list(
230     $list,
231     title   => $::locale->text('Sales Quotations'),
232     type    => 'sales_quotation',
233     columns => [
234       [ $::locale->text('Quotation Date'),          'transdate'                                                                ],
235       [ $::locale->text('Quotation Number'),        sub { $self->sales_quotation($_[0], display => 'table-cell') }   ],
236       [ $::locale->text('Customer'),                'customer'                                                                 ],
237       [ $::locale->text('Net amount'),              'netamount'                                                                ],
238       [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
239       [ $::locale->text('Project'),                 'globalproject', ],
240       [ $::locale->text('Closed'),                  'closed'                                                                   ],
241     ],
242     %params,
243   );
244 }
245
246 sub _request_quotation_list {
247   my ($self, $list, %params) = @_;
248
249   return $self->record_list(
250     $list,
251     title   => $::locale->text('Request Quotations'),
252     type    => 'request_quotation',
253     columns => [
254       [ $::locale->text('Quotation Date'),          'transdate'                                                                ],
255       [ $::locale->text('Quotation Number'),        sub { $self->request_quotation($_[0], display => 'table-cell') }   ],
256       [ $::locale->text('Vendor'),                  'vendor'                                                                   ],
257       [ $::locale->text('Net amount'),              'netamount'                                                                ],
258       [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
259       [ $::locale->text('Project'),                 'globalproject', ],
260       [ $::locale->text('Closed'),                  'closed'                                                                   ],
261     ],
262     %params,
263   );
264 }
265
266 sub _sales_order_list {
267   my ($self, $list, %params) = @_;
268
269   return $self->record_list(
270     $list,
271     title   => $::locale->text('Sales Orders'),
272     type    => 'sales_order',
273     columns => [
274       [ $::locale->text('Order Date'),              'transdate'                                                                ],
275       [ $::locale->text('Order Number'),            sub { $self->sales_order($_[0], display => 'table-cell') }   ],
276       [ $::locale->text('Quotation'),               'quonumber' ],
277       [ $::locale->text('Customer'),                'customer'                                                                 ],
278       [ $::locale->text('Net amount'),              'netamount'                                                                ],
279       [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
280       [ $::locale->text('Project'),                 'globalproject', ],
281       [ $::locale->text('Closed'),                  'closed'                                                                   ],
282     ],
283     %params,
284   );
285 }
286
287 sub _purchase_order_list {
288   my ($self, $list, %params) = @_;
289
290   return $self->record_list(
291     $list,
292     title   => $::locale->text('Purchase Orders'),
293     type    => 'purchase_order',
294     columns => [
295       [ $::locale->text('Order Date'),              'transdate'                                                                ],
296       [ $::locale->text('Order Number'),            sub { $self->purchase_order($_[0], display => 'table-cell') }   ],
297       [ $::locale->text('Request for Quotation'),   'quonumber' ],
298       [ $::locale->text('Vendor'),                  'vendor'                                                                 ],
299       [ $::locale->text('Net amount'),              'netamount'                                                                ],
300       [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
301       [ $::locale->text('Project'),                 'globalproject', ],
302       [ $::locale->text('Closed'),                  'closed'                                                                   ],
303     ],
304     %params,
305   );
306 }
307
308 sub _sales_delivery_order_list {
309   my ($self, $list, %params) = @_;
310
311   return $self->record_list(
312     $list,
313     title   => $::locale->text('Sales Delivery Orders'),
314     type    => 'sales_delivery_order',
315     columns => [
316       [ $::locale->text('Delivery Order Date'),     'transdate'                                                                ],
317       [ $::locale->text('Delivery Order Number'),   sub { $self->sales_delivery_order($_[0], display => 'table-cell') } ],
318       [ $::locale->text('Order Number'),            'ordnumber' ],
319       [ $::locale->text('Customer'),                'customer'                                                                 ],
320       [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
321       [ $::locale->text('Project'),                 'globalproject', ],
322       [ $::locale->text('Delivered'),               'delivered'                                                                ],
323       [ $::locale->text('Closed'),                  'closed'                                                                   ],
324     ],
325     %params,
326   );
327 }
328
329 sub _purchase_delivery_order_list {
330   my ($self, $list, %params) = @_;
331
332   return $self->record_list(
333     $list,
334     title   => $::locale->text('Purchase Delivery Orders'),
335     type    => 'purchase_delivery_order',
336     columns => [
337       [ $::locale->text('Delivery Order Date'),     'transdate'                                                                ],
338       [ $::locale->text('Delivery Order Number'),   sub { $self->purchase_delivery_order($_[0], display => 'table-cell') } ],
339       [ $::locale->text('Order Number'),            'ordnumber' ],
340       [ $::locale->text('Vendor'),                  'vendor'                                                                 ],
341       [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
342       [ $::locale->text('Project'),                 'globalproject', ],
343       [ $::locale->text('Delivered'),               'delivered'                                                                ],
344       [ $::locale->text('Closed'),                  'closed'                                                                   ],
345     ],
346     %params,
347   );
348 }
349
350 sub _sales_invoice_list {
351   my ($self, $list, %params) = @_;
352
353   return $self->record_list(
354     $list,
355     title   => $::locale->text('Sales Invoices'),
356     type    => 'sales_invoice',
357     columns => [
358       [ $::locale->text('Invoice Date'),            'transdate'               ],
359       [ $::locale->text('Type'),                    sub { $_[0]->displayable_type } ],
360       [ $::locale->text('Invoice Number'),          sub { $self->sales_invoice($_[0], display => 'table-cell') } ],
361       [ $::locale->text('Quotation Number'),        'quonumber' ],
362       [ $::locale->text('Order Number'),            'ordnumber' ],
363       [ $::locale->text('Customer'),                'customer'                ],
364       [ $::locale->text('Net amount'),              'netamount'               ],
365       [ $::locale->text('Paid'),                    'paid'                    ],
366       [ $::locale->text('Transaction description'), 'transaction_description' ],
367     ],
368     %params,
369   );
370 }
371
372 sub _purchase_invoice_list {
373   my ($self, $list, %params) = @_;
374
375   return $self->record_list(
376     $list,
377     title   => $::locale->text('Purchase Invoices'),
378     type    => 'purchase_invoice',
379     columns => [
380       [ $::locale->text('Invoice Date'),                 'transdate'               ],
381       [ $::locale->text('Invoice Number'),               sub { $self->purchase_invoice($_[0], display => 'table-cell') } ],
382       [ $::locale->text('Request for Quotation Number'), 'quonumber' ],
383       [ $::locale->text('Order Number'),                 'ordnumber' ],
384       [ $::locale->text('Vendor'),                       'vendor'                 ],
385       [ $::locale->text('Net amount'),                   'netamount'               ],
386       [ $::locale->text('Paid'),                         'paid'                    ],
387       [ $::locale->text('Transaction description'),      'transaction_description' ],
388     ],
389     %params,
390   );
391 }
392
393 sub _ar_transaction_list {
394   my ($self, $list, %params) = @_;
395
396   return $self->record_list(
397     $list,
398     title   => $::locale->text('AR Transactions'),
399     type    => 'ar_transaction',
400     columns => [
401       [ $::locale->text('Invoice Date'),            'transdate'               ],
402       [ $::locale->text('Type'),                    sub { $_[0]->displayable_type } ],
403       [ $::locale->text('Invoice Number'),          sub { $self->ar_transaction($_[0], display => 'table-cell') } ],
404       [ $::locale->text('Customer'),                'customer'                ],
405       [ $::locale->text('Net amount'),              'netamount'               ],
406       [ $::locale->text('Paid'),                    'paid'                    ],
407       [ $::locale->text('Transaction description'), 'transaction_description' ],
408     ],
409     %params,
410   );
411 }
412
413 sub _ap_transaction_list {
414   my ($self, $list, %params) = @_;
415
416   return $self->record_list(
417     $list,
418     title   => $::locale->text('AP Transactions'),
419     type    => 'ap_transaction',
420     columns => [
421       [ $::locale->text('Invoice Date'),            'transdate'                      ],
422       [ $::locale->text('Invoice Number'),          sub { $self->ap_transaction($_[0 ], display => 'table-cell') } ],
423       [ $::locale->text('Vendor'),                  'vendor'                         ],
424       [ $::locale->text('Net amount'),              'netamount'                      ],
425       [ $::locale->text('Paid'),                    'paid'                           ],
426       [ $::locale->text('Transaction description'), 'transaction_description'        ],
427     ],
428     %params,
429   );
430 }
431
432 sub _sepa_export_list {
433   my ($self, $list, %params) = @_;
434
435   my ($source, $destination) = $params{type} eq 'sepa_transfer' ? qw(our vc)                                 : qw(vc our);
436   $params{title}             = $params{type} eq 'sepa_transfer' ? $::locale->text('Bank transfers via SEPA') : $::locale->text('Bank collections via SEPA');
437   $params{with_columns}      = [ grep { $_ ne 'record_link_direction' } @{ $params{with_columns} || [] } ];
438
439   delete $params{edit_record_links};
440
441   return $self->record_list(
442     $list,
443     columns => [
444       [ $::locale->text('Export Number'),    'sepa_export',                                  ],
445       [ $::locale->text('Execution date'),   'execution_date'                                ],
446       [ $::locale->text('Export date'),      sub { $_[0]->sepa_export->itime->to_kivitendo } ],
447       [ $::locale->text('Source BIC'),       "${source}_bic"                                 ],
448       [ $::locale->text('Source IBAN'),      "${source}_iban"                                ],
449       [ $::locale->text('Destination BIC'),  "${destination}_bic"                            ],
450       [ $::locale->text('Destination IBAN'), "${destination}_iban"                           ],
451       [ $::locale->text('Amount'),           'amount'                                        ],
452     ],
453     %params,
454   );
455 }
456
457 sub _sepa_transfer_list {
458   my ($self, $list, %params) = @_;
459   _sepa_export_list($self, $list, %params, type => 'sepa_transfer');
460 }
461
462 sub _sepa_collection_list {
463   my ($self, $list, %params) = @_;
464   _sepa_export_list($self, $list, %params, type => 'sepa_collection');
465 }
466
467 1;
468
469 __END__
470
471 =pod
472
473 =encoding utf8
474
475 =head1 NAME
476
477 SL::Presenter::Record - Presenter module for lists of
478 sales/purchase/general ledger record Rose::DB objects
479
480 =head1 SYNOPSIS
481
482   # Retrieve a number of documents from somewhere, e.g.
483   my $order   = SL::DB::Manager::Order->get_first(where => [ SL::DB::Manager::Order->type_filter('sales_order') ]);
484   my $records = $order->linked_records(destination => 'to');
485
486   # Give HTML representation:
487   my $html = SL::Presenter->get->grouped_record_list($records);
488
489 =head1 OVERVIEW
490
491 TODO
492
493 =head1 FUNCTIONS
494
495 =over 4
496
497 =item C<record>
498
499 Returns a rendered version (actually an instance of
500 L<SL::Presenter::EscapedText>) of a single ar, ap or gl object.
501
502 Example:
503   # fetch the record from a random acc_trans object and print its link (could be ar, ap or gl)
504   my $record = SL::DB::Manager::AccTransaction->get_first()->record;
505   my $html   = SL::Presenter->get->record($record, display => 'inline');
506
507 =item C<grouped_record_list $list, %params>
508
509 =item C<empty_record_list>
510
511 Returns a rendered version (actually an instance of
512 L<SL::Presenter::EscapedText>) of an empty list of records. Is usually
513 only called by L<grouped_record_list> if its list is empty.
514
515 =item C<grouped_record_list $list, %params>
516
517 Given a number of Rose::DB objects in the array reference C<$list>
518 this function first groups them by type. Then it calls L<record_list>
519 with each non-empty type-specific sub-list and the appropriate
520 parameters for outputting a list of those records.
521
522 Returns a rendered version (actually an instance of
523 L<SL::Presenter::EscapedText>) of all the lists.
524
525 The order in which the records are grouped is:
526
527 =over 2
528
529 =item * sales quotations
530
531 =item * sales orders
532
533 =item * sales delivery orders
534
535 =item * sales invoices
536
537 =item * AR transactions
538
539 =item * requests for quotations
540
541 =item * purchase orders
542
543 =item * purchase delivery orders
544
545 =item * purchase invoices
546
547 =item * AP transactions
548
549 =item * SEPA collections
550
551 =item * SEPA transfers
552
553 =back
554
555 Objects of unknown types are skipped.
556
557 Parameters are passed to C<record_list> include C<with_objects> and
558 C<edit_record_links>.
559
560 =item C<record_list $list, %params>
561
562 Returns a rendered version (actually an instance of
563 L<SL::Presenter::EscapedText>) of a list of records. This list
564 consists of a heading and a tabular representation of the list.
565
566 The parameters include:
567
568 =over 2
569
570 =item C<title>
571
572 Mandatory. The title to use in the heading. Must already be
573 translated.
574
575 =item C<columns>
576
577 Mandatory. An array reference of column specs to output. Each column
578 spec can be either an array reference or a hash reference.
579
580 If a column spec is an array reference then the first element is the
581 column's name shown in the table header. It must already be translated.
582
583 The second element can be either a string or a code reference. A
584 string is taken as the name of a function to call on the Rose::DB
585 object for the current row. Its return value is formatted depending on
586 the column's type (e.g. dates are output as the user expects them,
587 floating point numbers are rounded to two decimal places and
588 right-aligned etc). If it is a code reference then that code is called
589 with the object as the first argument. Its return value should be an
590 instance of L<SL::Presenter::EscapedText> and contain the rendered
591 representation of the content to output.
592
593 The third element, if present, can be a link to which the column will
594 be linked.
595
596 If the column spec is a hash reference then the same arguments are
597 expected. The corresponding hash keys are C<title>, C<data> and
598 C<link>.
599
600 =item C<with_columns>
601
602 Can be set by the caller to indicate additional columns to
603 be listed. Currently supported:
604
605 =over 2
606
607 =item C<record_link_destination>
608
609 The record link destination. Requires that the records to be listed have
610 been retrieved via the L<SL::DB::Helper::LinkedRecords> helper.
611
612 =back
613
614 =item C<edit_record_links>
615
616 If trueish additional controls will be rendered that allow the user to
617 remove and add record links. Requires that the records to be listed have
618 been retrieved via the L<SL::DB::Helper::LinkedRecords> helper.
619
620 =back
621
622 =back
623
624 =head1 BUGS
625
626 Nothing here yet.
627
628 =head1 AUTHOR
629
630 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
631
632 =cut