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>