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 record);
13 use List::Util qw(first);
17 return [] if !defined $array;
18 return $array if ref $array;
23 my ($self, $record, %params) = @_;
25 my %grouped = _group_records( [ $record ] ); # pass $record as arrayref
26 my $type = (keys %grouped)[0];
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';
37 sub grouped_record_list {
38 my ($self, $list, %params) = @_;
40 %params = map { exists $params{$_} ? ($_ => $params{$_}) : () } qw(edit_record_links with_columns object_id object_model);
42 my %groups = _sort_grouped_lists(_group_records($list));
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};
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};
58 $output .= _bank_transactions( $self, $groups{bank_transactions}, %params) if $groups{bank_transactions};
60 $output .= _sepa_collection_list( $self, $groups{sepa_collections}, %params) if $groups{sepa_collections};
61 $output .= _sepa_transfer_list( $self, $groups{sepa_transfers}, %params) if $groups{sepa_transfers};
63 $output .= _letter_list( $self, $groups{letters}, %params) if $groups{letters};
65 $output = $self->render('presenter/record/grouped_record_list', %params, output => $output);
70 sub empty_record_list {
71 my ($self, %params) = @_;
72 return $self->grouped_record_list([], %params);
76 my ($self, $list, %params) = @_;
80 if (ref($params{columns}) eq 'ARRAY') {
82 if (ref($_) eq 'ARRAY') {
83 { title => $_->[0], data => $_->[1], link => $_->[2] }
87 } @{ delete $params{columns} };
90 croak "Wrong type for 'columns' argument: not an array reference";
93 my %with_columns = map { ($_ => 1) } @{ _arrayify($params{with_columns}) };
94 if ($with_columns{record_link_direction}) {
96 title => $::locale->text('Link direction'),
98 $_[0]->{_record_link_depth} > 1
99 ? $::locale->text('Row was linked to another record')
100 : $_[0]->{_record_link_direction} eq 'from'
101 ? $::locale->text('Row was source for current record')
102 : $::locale->text('Row was created from current record') },
106 my %column_meta = map { $_->name => $_ } @{ $list->[0]->meta->columns };
107 my %relationships = map { $_->name => $_ } @{ $list->[0]->meta->relationships };
110 my ($obj, $method, @args) = @_;
111 $obj->$method(@args);
115 foreach my $obj (@{ $list }) {
118 foreach my $spec (@columns) {
121 my $method = $spec->{column} || $spec->{data};
122 my $meta = $column_meta{ $spec->{data} };
123 my $type = ref $meta;
124 my $relationship = $relationships{ $spec->{data} };
125 my $rel_type = !$relationship ? '' : $relationship->class;
126 $rel_type =~ s/^SL::DB:://;
127 $rel_type = SL::Util::snakify($rel_type);
129 if (ref($spec->{data}) eq 'CODE') {
130 $cell{value} = $spec->{data}->($obj);
133 $cell{value} = $rel_type && $self->can($rel_type) ? $self->$rel_type($obj->$method, display => 'table-cell')
134 : $type eq 'Rose::DB::Object::Metadata::Column::Date' ? $call->($obj, $method . '_as_date')
135 : $type =~ m/^Rose::DB::Object::Metadata::Column::(?:Float|Numeric|Real)$/ ? $::form->format_amount(\%::myconfig, $call->($obj, $method), 2)
136 : $type eq 'Rose::DB::Object::Metadata::Column::Boolean' ? $call->($obj, $method . '_as_bool_yn')
137 : $type =~ m/^Rose::DB::Object::Metadata::Column::(?:Integer|Serial)$/ ? $spec->{data} * 1
138 : $call->($obj, $method);
141 $cell{alignment} = 'right' if $type =~ m/int|serial|float|real|numeric/;
146 push @data, { columns => \@row, record_link => $obj->{_record_link} };
150 map +{ value => $columns[$_]->{title},
151 alignment => $data[0]->{columns}->[$_]->{alignment},
152 }, (0..scalar(@columns) - 1);
154 return $self->render(
155 'presenter/record/record_list',
157 TABLE_HEADER => \@header,
158 TABLE_ROWS => \@data,
170 requirement_specs => sub { (ref($_[0]) eq 'SL::DB::RequirementSpec') },
171 sales_quotations => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('sales_quotation') },
172 sales_orders => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('sales_order') },
173 sales_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder') && $_[0]->is_sales },
174 sales_invoices => sub { (ref($_[0]) eq 'SL::DB::Invoice') && $_[0]->invoice },
175 ar_transactions => sub { (ref($_[0]) eq 'SL::DB::Invoice') && !$_[0]->invoice },
176 purchase_quotations => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('request_quotation') },
177 purchase_orders => sub { (ref($_[0]) eq 'SL::DB::Order') && $_[0]->is_type('purchase_order') },
178 purchase_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder') && !$_[0]->is_sales },
179 purchase_invoices => sub { (ref($_[0]) eq 'SL::DB::PurchaseInvoice') && $_[0]->invoice },
180 ap_transactions => sub { (ref($_[0]) eq 'SL::DB::PurchaseInvoice') && !$_[0]->invoice },
181 sepa_collections => sub { (ref($_[0]) eq 'SL::DB::SepaExportItem') && $_[0]->ar_id },
182 sepa_transfers => sub { (ref($_[0]) eq 'SL::DB::SepaExportItem') && $_[0]->ap_id },
183 gl_transactions => sub { (ref($_[0]) eq 'SL::DB::GLTransaction') },
184 bank_transactions => sub { (ref($_[0]) eq 'SL::DB::BankTransaction') && $_[0]->id },
185 letters => sub { (ref($_[0]) eq 'SL::DB::Letter') && $_[0]->id },
190 foreach my $record (@{ $list || [] }) {
191 my $type = (first { $matchers{$_}->($record) } keys %matchers) || 'other';
192 $groups{$type} ||= [];
193 push @{ $groups{$type} }, $record;
199 sub _sort_grouped_lists {
202 foreach my $group (keys %groups) {
203 next unless @{ $groups{$group} };
204 if ($groups{$group}->[0]->can('compare_to')) {
205 $groups{$group} = [ sort { $a->compare_to($b) } @{ $groups{$group} } ];
207 $groups{$group} = [ sort { $a->date <=> $b->date } @{ $groups{$group} } ];
214 sub _requirement_spec_list {
215 my ($self, $list, %params) = @_;
217 return $self->record_list(
219 title => $::locale->text('Requirement specs'),
220 type => 'requirement_spec',
222 [ $::locale->text('Requirement spec number'), sub { $self->requirement_spec($_[0], display => 'table-cell') } ],
223 [ $::locale->text('Customer'), 'customer' ],
224 [ $::locale->text('Title'), 'title' ],
225 [ $::locale->text('Project'), 'project', ],
226 [ $::locale->text('Status'), sub { $_[0]->status->description } ],
232 sub _sales_quotation_list {
233 my ($self, $list, %params) = @_;
235 return $self->record_list(
237 title => $::locale->text('Sales Quotations'),
238 type => 'sales_quotation',
240 [ $::locale->text('Quotation Date'), 'transdate' ],
241 [ $::locale->text('Quotation Number'), sub { $self->sales_quotation($_[0], display => 'table-cell') } ],
242 [ $::locale->text('Customer'), 'customer' ],
243 [ $::locale->text('Net amount'), 'netamount' ],
244 [ $::locale->text('Transaction description'), 'transaction_description' ],
245 [ $::locale->text('Project'), 'globalproject', ],
246 [ $::locale->text('Closed'), 'closed' ],
252 sub _request_quotation_list {
253 my ($self, $list, %params) = @_;
255 return $self->record_list(
257 title => $::locale->text('Request Quotations'),
258 type => 'request_quotation',
260 [ $::locale->text('Quotation Date'), 'transdate' ],
261 [ $::locale->text('Quotation Number'), sub { $self->request_quotation($_[0], display => 'table-cell') } ],
262 [ $::locale->text('Vendor'), 'vendor' ],
263 [ $::locale->text('Net amount'), 'netamount' ],
264 [ $::locale->text('Transaction description'), 'transaction_description' ],
265 [ $::locale->text('Project'), 'globalproject', ],
266 [ $::locale->text('Closed'), 'closed' ],
272 sub _sales_order_list {
273 my ($self, $list, %params) = @_;
275 return $self->record_list(
277 title => $::locale->text('Sales Orders'),
278 type => 'sales_order',
280 [ $::locale->text('Order Date'), 'transdate' ],
281 [ $::locale->text('Order Number'), sub { $self->sales_order($_[0], display => 'table-cell') } ],
282 [ $::locale->text('Quotation'), 'quonumber' ],
283 [ $::locale->text('Customer'), 'customer' ],
284 [ $::locale->text('Net amount'), 'netamount' ],
285 [ $::locale->text('Transaction description'), 'transaction_description' ],
286 [ $::locale->text('Project'), 'globalproject', ],
287 [ $::locale->text('Closed'), 'closed' ],
293 sub _purchase_order_list {
294 my ($self, $list, %params) = @_;
296 return $self->record_list(
298 title => $::locale->text('Purchase Orders'),
299 type => 'purchase_order',
301 [ $::locale->text('Order Date'), 'transdate' ],
302 [ $::locale->text('Order Number'), sub { $self->purchase_order($_[0], display => 'table-cell') } ],
303 [ $::locale->text('Request for Quotation'), 'quonumber' ],
304 [ $::locale->text('Vendor'), 'vendor' ],
305 [ $::locale->text('Net amount'), 'netamount' ],
306 [ $::locale->text('Transaction description'), 'transaction_description' ],
307 [ $::locale->text('Project'), 'globalproject', ],
308 [ $::locale->text('Closed'), 'closed' ],
314 sub _sales_delivery_order_list {
315 my ($self, $list, %params) = @_;
317 return $self->record_list(
319 title => $::locale->text('Sales Delivery Orders'),
320 type => 'sales_delivery_order',
322 [ $::locale->text('Delivery Order Date'), 'transdate' ],
323 [ $::locale->text('Delivery Order Number'), sub { $self->sales_delivery_order($_[0], display => 'table-cell') } ],
324 [ $::locale->text('Order Number'), 'ordnumber' ],
325 [ $::locale->text('Customer'), 'customer' ],
326 [ $::locale->text('Transaction description'), 'transaction_description' ],
327 [ $::locale->text('Project'), 'globalproject', ],
328 [ $::locale->text('Delivered'), 'delivered' ],
329 [ $::locale->text('Closed'), 'closed' ],
335 sub _purchase_delivery_order_list {
336 my ($self, $list, %params) = @_;
338 return $self->record_list(
340 title => $::locale->text('Purchase Delivery Orders'),
341 type => 'purchase_delivery_order',
343 [ $::locale->text('Delivery Order Date'), 'transdate' ],
344 [ $::locale->text('Delivery Order Number'), sub { $self->purchase_delivery_order($_[0], display => 'table-cell') } ],
345 [ $::locale->text('Order Number'), 'ordnumber' ],
346 [ $::locale->text('Vendor'), 'vendor' ],
347 [ $::locale->text('Transaction description'), 'transaction_description' ],
348 [ $::locale->text('Project'), 'globalproject', ],
349 [ $::locale->text('Delivered'), 'delivered' ],
350 [ $::locale->text('Closed'), 'closed' ],
356 sub _sales_invoice_list {
357 my ($self, $list, %params) = @_;
359 return $self->record_list(
361 title => $::locale->text('Sales Invoices'),
362 type => 'sales_invoice',
364 [ $::locale->text('Invoice Date'), 'transdate' ],
365 [ $::locale->text('Type'), sub { $_[0]->displayable_type } ],
366 [ $::locale->text('Invoice Number'), sub { $self->sales_invoice($_[0], display => 'table-cell') } ],
367 [ $::locale->text('Quotation Number'), 'quonumber' ],
368 [ $::locale->text('Order Number'), 'ordnumber' ],
369 [ $::locale->text('Customer'), 'customer' ],
370 [ $::locale->text('Net amount'), 'netamount' ],
371 [ $::locale->text('Paid'), 'paid' ],
372 [ $::locale->text('Transaction description'), 'transaction_description' ],
378 sub _purchase_invoice_list {
379 my ($self, $list, %params) = @_;
381 return $self->record_list(
383 title => $::locale->text('Purchase Invoices'),
384 type => 'purchase_invoice',
386 [ $::locale->text('Invoice Date'), 'transdate' ],
387 [ $::locale->text('Invoice Number'), sub { $self->purchase_invoice($_[0], display => 'table-cell') } ],
388 [ $::locale->text('Request for Quotation Number'), 'quonumber' ],
389 [ $::locale->text('Order Number'), 'ordnumber' ],
390 [ $::locale->text('Vendor'), 'vendor' ],
391 [ $::locale->text('Net amount'), 'netamount' ],
392 [ $::locale->text('Paid'), 'paid' ],
393 [ $::locale->text('Transaction description'), 'transaction_description' ],
399 sub _ar_transaction_list {
400 my ($self, $list, %params) = @_;
402 return $self->record_list(
404 title => $::locale->text('AR Transactions'),
405 type => 'ar_transaction',
407 [ $::locale->text('Invoice Date'), 'transdate' ],
408 [ $::locale->text('Type'), sub { $_[0]->displayable_type } ],
409 [ $::locale->text('Invoice Number'), sub { $self->ar_transaction($_[0], display => 'table-cell') } ],
410 [ $::locale->text('Customer'), 'customer' ],
411 [ $::locale->text('Net amount'), 'netamount' ],
412 [ $::locale->text('Paid'), 'paid' ],
413 [ $::locale->text('Transaction description'), 'transaction_description' ],
419 sub _ap_transaction_list {
420 my ($self, $list, %params) = @_;
422 return $self->record_list(
424 title => $::locale->text('AP Transactions'),
425 type => 'ap_transaction',
427 [ $::locale->text('Invoice Date'), 'transdate' ],
428 [ $::locale->text('Invoice Number'), sub { $self->ap_transaction($_[0 ], display => 'table-cell') } ],
429 [ $::locale->text('Vendor'), 'vendor' ],
430 [ $::locale->text('Net amount'), 'netamount' ],
431 [ $::locale->text('Paid'), 'paid' ],
432 [ $::locale->text('Transaction description'), 'transaction_description' ],
438 sub _bank_transactions {
439 my ($self, $list, %params) = @_;
441 return $self->record_list(
443 title => $::locale->text('Bank transactions'),
444 type => 'bank_transactions',
446 [ $::locale->text('Transdate'), 'transdate' ],
447 [ $::locale->text('Local Bank Code'), sub { $self->bank_code($_[0]->local_bank_account) } ],
448 [ $::locale->text('Local account number'), sub { $self->account_number($_[0]->local_bank_account) } ],
449 [ $::locale->text('Remote Bank Code'), 'remote_bank_code' ],
450 [ $::locale->text('Remote account number'),'remote_account_number' ],
451 [ $::locale->text('Valutadate'), 'valutadate' ],
452 [ $::locale->text('Amount'), 'amount' ],
453 [ $::locale->text('Currency'), sub { $_[0]->currency->name } ],
454 [ $::locale->text('Remote name'), 'remote_name' ],
455 [ $::locale->text('Purpose'), 'purpose' ],
461 sub _sepa_export_list {
462 my ($self, $list, %params) = @_;
464 my ($source, $destination) = $params{type} eq 'sepa_transfer' ? qw(our vc) : qw(vc our);
465 $params{title} = $params{type} eq 'sepa_transfer' ? $::locale->text('Bank transfers via SEPA') : $::locale->text('Bank collections via SEPA');
466 $params{with_columns} = [ grep { $_ ne 'record_link_direction' } @{ $params{with_columns} || [] } ];
468 delete $params{edit_record_links};
470 return $self->record_list(
473 [ $::locale->text('Export Number'), 'sepa_export', ],
474 [ $::locale->text('Execution date'), 'execution_date' ],
475 [ $::locale->text('Export date'), sub { $_[0]->sepa_export->itime->to_kivitendo } ],
476 [ $::locale->text('Source BIC'), "${source}_bic" ],
477 [ $::locale->text('Source IBAN'), "${source}_iban" ],
478 [ $::locale->text('Destination BIC'), "${destination}_bic" ],
479 [ $::locale->text('Destination IBAN'), "${destination}_iban" ],
480 [ $::locale->text('Amount'), 'amount' ],
486 sub _sepa_transfer_list {
487 my ($self, $list, %params) = @_;
488 _sepa_export_list($self, $list, %params, type => 'sepa_transfer');
491 sub _sepa_collection_list {
492 my ($self, $list, %params) = @_;
493 _sepa_export_list($self, $list, %params, type => 'sepa_collection');
497 my ($self, $list, %params) = @_;
499 return $self->record_list(
501 title => $::locale->text('Letters'),
504 [ $::locale->text('Date'), 'date' ],
505 [ $::locale->text('Letternumber'), sub { $self->letter($_[0], display => 'table-cell') } ],
506 [ $::locale->text('Customer'), 'customer' ],
507 [ $::locale->text('Reference'), 'reference' ],
508 [ $::locale->text('Subject'), 'subject' ],
524 SL::Presenter::Record - Presenter module for lists of
525 sales/purchase/general ledger record Rose::DB objects
529 # Retrieve a number of documents from somewhere, e.g.
530 my $order = SL::DB::Manager::Order->get_first(where => [ SL::DB::Manager::Order->type_filter('sales_order') ]);
531 my $records = $order->linked_records(destination => 'to');
533 # Give HTML representation:
534 my $html = SL::Presenter->get->grouped_record_list($records);
546 Returns a rendered version (actually an instance of
547 L<SL::Presenter::EscapedText>) of a single ar, ap or gl object.
550 # fetch the record from a random acc_trans object and print its link (could be ar, ap or gl)
551 my $record = SL::DB::Manager::AccTransaction->get_first()->record;
552 my $html = SL::Presenter->get->record($record, display => 'inline');
554 =item C<grouped_record_list $list, %params>
556 =item C<empty_record_list>
558 Returns a rendered version (actually an instance of
559 L<SL::Presenter::EscapedText>) of an empty list of records. Is usually
560 only called by L<grouped_record_list> if its list is empty.
562 =item C<grouped_record_list $list, %params>
564 Given a number of Rose::DB objects in the array reference C<$list>
565 this function first groups them by type. Then it calls L<record_list>
566 with each non-empty type-specific sub-list and the appropriate
567 parameters for outputting a list of those records.
569 Returns a rendered version (actually an instance of
570 L<SL::Presenter::EscapedText>) of all the lists.
572 The order in which the records are grouped is:
576 =item * sales quotations
580 =item * sales delivery orders
582 =item * sales invoices
584 =item * AR transactions
586 =item * requests for quotations
588 =item * purchase orders
590 =item * purchase delivery orders
592 =item * purchase invoices
594 =item * AP transactions
596 =item * SEPA collections
598 =item * SEPA transfers
602 Objects of unknown types are skipped.
604 Parameters are passed to C<record_list> include C<with_objects> and
605 C<edit_record_links>.
607 =item C<record_list $list, %params>
609 Returns a rendered version (actually an instance of
610 L<SL::Presenter::EscapedText>) of a list of records. This list
611 consists of a heading and a tabular representation of the list.
613 The parameters include:
619 Mandatory. The title to use in the heading. Must already be
624 Mandatory. An array reference of column specs to output. Each column
625 spec can be either an array reference or a hash reference.
627 If a column spec is an array reference then the first element is the
628 column's name shown in the table header. It must already be translated.
630 The second element can be either a string or a code reference. A
631 string is taken as the name of a function to call on the Rose::DB
632 object for the current row. Its return value is formatted depending on
633 the column's type (e.g. dates are output as the user expects them,
634 floating point numbers are rounded to two decimal places and
635 right-aligned etc). If it is a code reference then that code is called
636 with the object as the first argument. Its return value should be an
637 instance of L<SL::Presenter::EscapedText> and contain the rendered
638 representation of the content to output.
640 The third element, if present, can be a link to which the column will
643 If the column spec is a hash reference then the same arguments are
644 expected. The corresponding hash keys are C<title>, C<data> and
647 =item C<with_columns>
649 Can be set by the caller to indicate additional columns to
650 be listed. Currently supported:
654 =item C<record_link_destination>
656 The record link destination. Requires that the records to be listed have
657 been retrieved via the L<SL::DB::Helper::LinkedRecords> helper.
661 =item C<edit_record_links>
663 If trueish additional controls will be rendered that allow the user to
664 remove and add record links. Requires that the records to be listed have
665 been retrieved via the L<SL::DB::Helper::LinkedRecords> helper.
677 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>