1 package SL::Presenter::Record;
5 use parent qw(Exporter);
7 use Exporter qw(import);
8 our @EXPORT = qw(grouped_record_list empty_record_list record_list);
13 use List::Util qw(first);
17 return [] if !defined $array;
18 return $array if ref $array;
22 sub grouped_record_list {
23 my ($self, $list, %params) = @_;
25 %params = map { exists $params{$_} ? ($_ => $params{$_}) : () } qw(edit_record_links with_columns object_id object_model);
27 my %groups = _sort_grouped_lists(_group_records($list));
30 $output .= _sales_quotation_list( $self, $groups{sales_quotations}, %params) if $groups{sales_quotations};
31 $output .= _sales_order_list( $self, $groups{sales_orders}, %params) if $groups{sales_orders};
32 $output .= _sales_delivery_order_list( $self, $groups{sales_delivery_orders}, %params) if $groups{sales_delivery_orders};
33 $output .= _sales_invoice_list( $self, $groups{sales_invoices}, %params) if $groups{sales_invoices};
34 $output .= _ar_transaction_list( $self, $groups{ar_transactions}, %params) if $groups{ar_transactions};
36 $output .= _request_quotation_list( $self, $groups{purchase_quotations}, %params) if $groups{purchase_quotations};
37 $output .= _purchase_order_list( $self, $groups{purchase_orders}, %params) if $groups{purchase_orders};
38 $output .= _purchase_delivery_order_list($self, $groups{purchase_delivery_orders}, %params) if $groups{purchase_delivery_orders};
39 $output .= _purchase_invoice_list( $self, $groups{purchase_invoices}, %params) if $groups{purchase_invoices};
40 $output .= _ar_transaction_list( $self, $groups{ar_transactions}, %params) if $groups{ar_transactions};
42 $output .= _sepa_collection_list( $self, $groups{sepa_collections}, %params) if $groups{sepa_collections};
43 $output .= _sepa_transfer_list( $self, $groups{sepa_transfers}, %params) if $groups{sepa_transfers};
45 $output = $self->render('presenter/record/grouped_record_list', %params, output => $output);
50 sub empty_record_list {
51 my ($self, %params) = @_;
52 return $self->grouped_record_list([], %params);
56 my ($self, $list, %params) = @_;
60 if (ref($params{columns}) eq 'ARRAY') {
62 if (ref($_) eq 'ARRAY') {
63 { title => $_->[0], data => $_->[1], link => $_->[2] }
67 } @{ delete $params{columns} };
70 croak "Wrong type for 'columns' argument: not an array reference";
73 my %with_columns = map { ($_ => 1) } @{ _arrayify($params{with_columns}) };
74 if ($with_columns{record_link_direction}) {
76 title => $::locale->text('Link direction'),
77 data => sub { $_[0]->{_record_link_direction} eq 'from' ? $::locale->text('Row was source for current record') : $::locale->text('Row was created from current record') },
81 my %column_meta = map { $_->name => $_ } @{ $list->[0]->meta->columns };
82 my %relationships = map { $_->name => $_ } @{ $list->[0]->meta->relationships };
85 my ($obj, $method, @args) = @_;
90 foreach my $obj (@{ $list }) {
93 foreach my $spec (@columns) {
96 my $method = $spec->{column} || $spec->{data};
97 my $meta = $column_meta{ $spec->{data} };
99 my $relationship = $relationships{ $spec->{data} };
100 my $rel_type = !$relationship ? '' : $relationship->class;
101 $rel_type =~ s/^SL::DB:://;
102 $rel_type = SL::Util::snakify($rel_type);
104 if (ref($spec->{data}) eq 'CODE') {
105 $cell{value} = $spec->{data}->($obj);
108 $cell{value} = $rel_type && $self->can($rel_type) ? $self->$rel_type($obj->$method, display => 'table-cell')
109 : $type eq 'Rose::DB::Object::Metadata::Column::Date' ? $call->($obj, $method . '_as_date')
110 : $type =~ m/^Rose::DB::Object::Metadata::Column::(?:Float|Numeric|Real)$/ ? $::form->format_amount(\%::myconfig, $call->($obj, $method), 2)
111 : $type eq 'Rose::DB::Object::Metadata::Column::Boolean' ? $call->($obj, $method . '_as_bool_yn')
112 : $type =~ m/^Rose::DB::Object::Metadata::Column::(?:Integer|Serial)$/ ? $spec->{data} * 1
113 : $call->($obj, $method);
116 $cell{alignment} = 'right' if $type =~ m/int|serial|float|real|numeric/;
121 push @data, { columns => \@row, record_link => $obj->{_record_link} };
125 map +{ value => $columns[$_]->{title},
126 alignment => $data[0]->{columns}->[$_]->{alignment},
127 }, (0..scalar(@columns) - 1);
129 return $self->render(
130 'presenter/record/record_list',
132 TABLE_HEADER => \@header,
133 TABLE_ROWS => \@data,
145 sales_quotations => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('sales_quotation') },
146 sales_orders => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('sales_order') },
147 sales_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder') && $_[0]->is_sales },
148 sales_invoices => sub { (ref($_[0]) eq 'SL::DB::Invoice') && $_[0]->invoice },
149 ar_transactions => sub { (ref($_[0]) eq 'SL::DB::Invoice') && !$_[0]->invoice },
150 purchase_quotations => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('request_quotation') },
151 purchase_orders => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('purchase_order') },
152 purchase_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder') && !$_[0]->is_sales },
153 purchase_invoices => sub { (ref($_[0]) eq 'SL::DB::PurchaseInvoice') && $_[0]->invoice },
154 ap_transactions => sub { (ref($_[0]) eq 'SL::DB::PurchaseInvoice') && !$_[0]->invoice },
155 sepa_collections => sub { (ref($_[0]) eq 'SL::DB::SepaExportItem') && $_[0]->ar_id },
156 sepa_transfers => sub { (ref($_[0]) eq 'SL::DB::SepaExportItem') && $_[0]->ap_id },
161 foreach my $record (@{ $list || [] }) {
162 my $type = (first { $matchers{$_}->($record) } keys %matchers) || 'other';
163 $groups{$type} ||= [];
164 push @{ $groups{$type} }, $record;
170 sub _sort_grouped_lists {
173 foreach my $group (keys %groups) {
174 next unless @{ $groups{$group} };
175 if ($groups{$group}->[0]->can('compare_to')) {
176 $groups{$group} = [ sort { $a->compare_to($b) } @{ $groups{$group} } ];
178 $groups{$group} = [ sort { $a->date <=> $b->date } @{ $groups{$group} } ];
185 sub _sales_quotation_list {
186 my ($self, $list, %params) = @_;
188 return $self->record_list(
190 title => $::locale->text('Sales Quotations'),
191 type => 'sales_quotation',
193 [ $::locale->text('Quotation Date'), 'transdate' ],
194 [ $::locale->text('Quotation Number'), sub { $self->sales_quotation($_[0], display => 'table-cell') } ],
195 [ $::locale->text('Customer'), 'customer' ],
196 [ $::locale->text('Net amount'), 'netamount' ],
197 [ $::locale->text('Transaction description'), 'transaction_description' ],
198 [ $::locale->text('Project'), 'globalproject', ],
199 [ $::locale->text('Closed'), 'closed' ],
205 sub _request_quotation_list {
206 my ($self, $list, %params) = @_;
208 return $self->record_list(
210 title => $::locale->text('Request Quotations'),
211 type => 'request_quotation',
213 [ $::locale->text('Quotation Date'), 'transdate' ],
214 [ $::locale->text('Quotation Number'), sub { $self->request_quotation($_[0], display => 'table-cell') } ],
215 [ $::locale->text('Vendor'), 'vendor' ],
216 [ $::locale->text('Net amount'), 'netamount' ],
217 [ $::locale->text('Transaction description'), 'transaction_description' ],
218 [ $::locale->text('Project'), 'globalproject', ],
219 [ $::locale->text('Closed'), 'closed' ],
225 sub _sales_order_list {
226 my ($self, $list, %params) = @_;
228 return $self->record_list(
230 title => $::locale->text('Sales Orders'),
231 type => 'sales_order',
233 [ $::locale->text('Order Date'), 'transdate' ],
234 [ $::locale->text('Order Number'), sub { $self->sales_order($_[0], display => 'table-cell') } ],
235 [ $::locale->text('Quotation'), 'quonumber' ],
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' ],
246 sub _purchase_order_list {
247 my ($self, $list, %params) = @_;
249 return $self->record_list(
251 title => $::locale->text('Purchase Orders'),
252 type => 'purchase_order',
254 [ $::locale->text('Order Date'), 'transdate' ],
255 [ $::locale->text('Order Number'), sub { $self->purchase_order($_[0], display => 'table-cell') } ],
256 [ $::locale->text('Request for Quotation'), 'quonumber' ],
257 [ $::locale->text('Vendor'), 'vendor' ],
258 [ $::locale->text('Net amount'), 'netamount' ],
259 [ $::locale->text('Transaction description'), 'transaction_description' ],
260 [ $::locale->text('Project'), 'globalproject', ],
261 [ $::locale->text('Closed'), 'closed' ],
267 sub _sales_delivery_order_list {
268 my ($self, $list, %params) = @_;
270 return $self->record_list(
272 title => $::locale->text('Sales Delivery Orders'),
273 type => 'sales_delivery_order',
275 [ $::locale->text('Delivery Order Date'), 'transdate' ],
276 [ $::locale->text('Delivery Order Number'), sub { $self->sales_delivery_order($_[0], display => 'table-cell') } ],
277 [ $::locale->text('Order Number'), 'ordnumber' ],
278 [ $::locale->text('Customer'), 'customer' ],
279 [ $::locale->text('Transaction description'), 'transaction_description' ],
280 [ $::locale->text('Project'), 'globalproject', ],
281 [ $::locale->text('Delivered'), 'delivered' ],
282 [ $::locale->text('Closed'), 'closed' ],
288 sub _purchase_delivery_order_list {
289 my ($self, $list, %params) = @_;
291 return $self->record_list(
293 title => $::locale->text('Purchase Delivery Orders'),
294 type => 'purchase_delivery_order',
296 [ $::locale->text('Delivery Order Date'), 'transdate' ],
297 [ $::locale->text('Delivery Order Number'), sub { $self->purchase_delivery_order($_[0], display => 'table-cell') } ],
298 [ $::locale->text('Order Number'), 'ordnumber' ],
299 [ $::locale->text('Vendor'), 'vendor' ],
300 [ $::locale->text('Transaction description'), 'transaction_description' ],
301 [ $::locale->text('Project'), 'globalproject', ],
302 [ $::locale->text('Delivered'), 'delivered' ],
303 [ $::locale->text('Closed'), 'closed' ],
309 sub _sales_invoice_list {
310 my ($self, $list, %params) = @_;
312 return $self->record_list(
314 title => $::locale->text('Sales Invoices'),
315 type => 'sales_invoice',
317 [ $::locale->text('Invoice Date'), 'transdate' ],
318 [ $::locale->text('Invoice Number'), sub { $self->sales_invoice($_[0], display => 'table-cell') } ],
319 [ $::locale->text('Quotation Number'), 'quonumber' ],
320 [ $::locale->text('Order Number'), 'ordnumber' ],
321 [ $::locale->text('Customer'), 'customer' ],
322 [ $::locale->text('Net amount'), 'netamount' ],
323 [ $::locale->text('Paid'), 'paid' ],
324 [ $::locale->text('Transaction description'), 'transaction_description' ],
330 sub _purchase_invoice_list {
331 my ($self, $list, %params) = @_;
333 return $self->record_list(
335 title => $::locale->text('Purchase Invoices'),
336 type => 'purchase_invoice',
338 [ $::locale->text('Invoice Date'), 'transdate' ],
339 [ $::locale->text('Invoice Number'), sub { $self->purchase_invoice($_[0], display => 'table-cell') } ],
340 [ $::locale->text('Request for Quotation Number'), 'quonumber' ],
341 [ $::locale->text('Order Number'), 'ordnumber' ],
342 [ $::locale->text('Vendor'), 'vendor' ],
343 [ $::locale->text('Net amount'), 'netamount' ],
344 [ $::locale->text('Paid'), 'paid' ],
345 [ $::locale->text('Transaction description'), 'transaction_description' ],
351 sub _ar_transaction_list {
352 my ($self, $list, %params) = @_;
354 return $self->record_list(
356 title => $::locale->text('AR Transactions'),
357 type => 'ar_transaction',
359 [ $::locale->text('Invoice Date'), 'transdate' ],
360 [ $::locale->text('Invoice Number'), sub { $self->ar_transaction($_[0], display => 'table-cell') } ],
361 [ $::locale->text('Customer'), 'customer' ],
362 [ $::locale->text('Net amount'), 'netamount' ],
363 [ $::locale->text('Paid'), 'paid' ],
364 [ $::locale->text('Transaction description'), 'transaction_description' ],
370 sub _ap_transaction_list {
371 my ($self, $list, %params) = @_;
373 return $self->record_list(
375 title => $::locale->text('AP Transactions'),
376 type => 'ap_transaction',
378 [ $::locale->text('Invoice Date'), 'transdate' ],
379 [ $::locale->text('Invoice Number'), sub { $self->ap_transaction($_[0 ], display => 'table-cell') } ],
380 [ $::locale->text('Vendor'), 'vendor' ],
381 [ $::locale->text('Net amount'), 'netamount' ],
382 [ $::locale->text('Paid'), 'paid' ],
383 [ $::locale->text('Transaction description'), 'transaction_description' ],
389 sub _sepa_export_list {
390 my ($self, $list, %params) = @_;
392 my ($source, $destination) = $params{type} eq 'sepa_transfer' ? qw(our vc) : qw(vc our);
393 $params{title} = $params{type} eq 'sepa_transfer' ? $::locale->text('Bank transfers via SEPA') : $::locale->text('Bank collections via SEPA');
394 $params{with_columns} = [ grep { $_ ne 'record_link_direction' } @{ $params{with_columns} || [] } ];
396 delete $params{edit_record_links};
398 return $self->record_list(
401 [ $::locale->text('Export Number'), 'sepa_export', ],
402 [ $::locale->text('Execution date'), 'execution_date' ],
403 [ $::locale->text('Export date'), sub { $_[0]->sepa_export->itime->to_kivitendo } ],
404 [ $::locale->text('Source BIC'), "${source}_bic" ],
405 [ $::locale->text('Source IBAN'), "${source}_iban" ],
406 [ $::locale->text('Destination BIC'), "${destination}_bic" ],
407 [ $::locale->text('Destination IBAN'), "${destination}_iban" ],
408 [ $::locale->text('Amount'), 'amount' ],
414 sub _sepa_transfer_list {
415 my ($self, $list, %params) = @_;
416 _sepa_export_list($self, $list, %params, type => 'sepa_transfer');
419 sub _sepa_collection_list {
420 my ($self, $list, %params) = @_;
421 _sepa_export_list($self, $list, %params, type => 'sepa_collection');
434 SL::Presenter::Record - Presenter module for lists of
435 sales/purchase/general ledger record Rose::DB objects
439 # Retrieve a number of documents from somewhere, e.g.
440 my $order = SL::DB::Manager::Order->get_first(where => [ SL::DB::Manager::Order->type_filter('sales_order') ]);
441 my $records = $order->linked_records(destination => 'to');
443 # Give HTML representation:
444 my $html = SL::Presenter->get->grouped_record_list($records);
454 =item C<empty_record_list>
456 Returns a rendered version (actually an instance of
457 L<SL::Presenter::EscapedText>) of an empty list of records. Is usually
458 only called by L<grouped_record_list> if its list is empty.
460 =item C<grouped_record_list $list, %params>
462 Given a number of Rose::DB objects in the array reference C<$list>
463 this function first groups them by type. Then it calls L<record_list>
464 with each non-empty type-specific sub-list and the appropriate
465 parameters for outputting a list of those records.
467 Returns a rendered version (actually an instance of
468 L<SL::Presenter::EscapedText>) of all the lists.
470 The order in which the records are grouped is:
474 =item * sales quotations
478 =item * sales delivery orders
480 =item * sales invoices
482 =item * AR transactions
484 =item * requests for quotations
486 =item * purchase orders
488 =item * purchase delivery orders
490 =item * purchase invoices
492 =item * AP transactions
494 =item * SEPA collections
496 =item * SEPA transfers
500 Objects of unknown types are skipped.
502 Parameters are passed to C<record_list> include C<with_objects> and
503 C<edit_record_links>.
505 =item C<record_list $list, %params>
507 Returns a rendered version (actually an instance of
508 L<SL::Presenter::EscapedText>) of a list of records. This list
509 consists of a heading and a tabular representation of the list.
511 The parameters include:
517 Mandatory. The title to use in the heading. Must already be
522 Mandatory. An array reference of column specs to output. Each column
523 spec can be either an array reference or a hash reference.
525 If a column spec is an array reference then the first element is the
526 column's name shown in the table header. It must already be translated.
528 The second element can be either a string or a code reference. A
529 string is taken as the name of a function to call on the Rose::DB
530 object for the current row. Its return value is formatted depending on
531 the column's type (e.g. dates are output as the user expects them,
532 floating point numbers are rounded to two decimal places and
533 right-aligned etc). If it is a code reference then that code is called
534 with the object as the first argument. Its return value should be an
535 instance of L<SL::Presenter::EscapedText> and contain the rendered
536 representation of the content to output.
538 The third element, if present, can be a link to which the column will
541 If the column spec is a hash reference then the same arguments are
542 expected. The corresponding hash keys are C<title>, C<data> and
545 =item C<with_columns>
547 Can be set by the caller to indicate additional columns to
548 list. Currently supported:
552 =item C<record_link_destination>
554 The record link destination. Requires that the records to list have
555 been retrieved via the L<SL::DB::Helper::LinkedRecords> helper.
559 =item C<edit_record_links>
561 If trueish additional controls will be rendered that allow the user to
562 remove and add record links. Requires that the records to list have
563 been retrieved via the L<SL::DB::Helper::LinkedRecords> helper.
575 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>