1 package SL::DB::Helpers::LinkedRecords;
 
   6 our @ISA    = qw(Exporter);
 
   7 our @EXPORT = qw(linked_records link_to_record linked_records_sorted);
 
  12 use SL::DB::Helpers::Mappings;
 
  13 use SL::DB::RecordLink;
 
  19   my $wanted   = $params{direction} || croak("Missing parameter `direction'");
 
  21   if ($wanted eq 'both') {
 
  22     my $both       = delete($params{both});
 
  23     my %from_to    = ( from => delete($params{from}) || $both,
 
  24                        to   => delete($params{to})   || $both);
 
  26     my @records    = (@{ $self->linked_records(%params, direction => 'from', from => $from_to{from}) },
 
  27                       @{ $self->linked_records(%params, direction => 'to',   to   => $from_to{to}  ) });
 
  29     my %record_map = map { ( ref($_) . $_->id => $_ ) } @records;
 
  31     return [ values %record_map ];
 
  34   my $myself   = $wanted eq 'from' ? 'to' : $wanted eq 'to' ? 'from' : croak("Invalid parameter `direction'");
 
  36   my $my_table = SL::DB::Helpers::Mappings::get_table_for_package(ref($self));
 
  38   my @query    = ( "${myself}_table" => $my_table,
 
  39                    "${myself}_id"    => $self->id );
 
  41   if ($params{$wanted}) {
 
  42     my $wanted_classes = ref($params{$wanted}) eq 'ARRAY' ? $params{$wanted} : [ $params{$wanted} ];
 
  43     my $wanted_tables  = [ map { SL::DB::Helpers::Mappings::get_table_for_package($_) || croak("Invalid parameter `${wanted}'") } @{ $wanted_classes } ];
 
  44     push @query, ("${wanted}_table" => $wanted_tables);
 
  47   my $links            = SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]);
 
  49   my $sub_wanted_table = "${wanted}_table";
 
  50   my $sub_wanted_id    = "${wanted}_id";
 
  53   @query               = ref($params{query}) eq 'ARRAY' ? @{ $params{query} } : ();
 
  55   foreach my $link (@{ $links }) {
 
  56     my $manager_class = SL::DB::Helpers::Mappings::get_manager_package_for_table($link->$sub_wanted_table);
 
  57     my $object_class  = SL::DB::Helpers::Mappings::get_package_for_table($link->$sub_wanted_table);
 
  58     eval "require " . $object_class . "; 1;";
 
  59     push @{ $records }, @{ $manager_class->get_all(query => [ id => $link->$sub_wanted_id, @query ]) };
 
  70   croak "self has no id"  unless $self->id;
 
  71   croak "other has no id" unless $other->id;
 
  73   my @directions = ([ 'from', 'to' ]);
 
  74   push @directions, [ 'to', 'from' ] if $params{bidirectional};
 
  77   foreach my $direction (@directions) {
 
  78     my %data = ( $direction->[0] . "_table" => SL::DB::Helper::Mappings::get_table_for_package(ref($self)),
 
  79                  $direction->[0] . "_id"    => $self->id,
 
  80                  $direction->[1] . "_table" => SL::DB::Helper::Mappings::get_table_for_package(ref($other)),
 
  81                  $direction->[1] . "_id"    => $other->id,
 
  84     my $link = SL::DB::Manager::RecordLink->find_by(and => [ %data ]);
 
  85     push @links, $link ? $link : SL::DB::RecordLink->new(%data)->save unless $link;
 
  88   return wantarray ? @links : $links[0];
 
  91 sub linked_records_sorted {
 
  92   my ($self, $sort_by, $sort_dir, %params) = @_;
 
  94   return sort_linked_records($self, $sort_by, $sort_dir, $self->linked_records(%params));
 
  97 sub sort_linked_records {
 
  98   my ($self_or_class, $sort_by, $sort_dir, @records) = @_;
 
 100   @records  = @{ $records[0] } if (1 == scalar(@records)) && (ref($records[0]) eq 'ARRAY');
 
 101   $sort_dir = $sort_dir * 1 ? 1 : -1;
 
 103   my %numbers = ( 'SL::DB::SalesProcess'    => sub { $_[0]->id },
 
 104                   'SL::DB::Order'           => sub { $_[0]->quotation ? $_[0]->quonumber : $_[0]->ordnumber },
 
 105                   'SL::DB::DeliveryOrder'   => sub { $_[0]->donumber },
 
 106                   'SL::DB::Invoice'         => sub { $_[0]->invnumber },
 
 107                   'SL::DB::PurchaseInvoice' => sub { $_[0]->invnumber },
 
 108                   UNKNOWN                   => '9999999999999999',
 
 110   my $number_xtor = sub {
 
 111     my $number = $numbers{ ref($_[0]) };
 
 112     $number    = $number->($_[0]) if ref($number) eq 'CODE';
 
 113     return $number || $numbers{UNKNOWN};
 
 115   my $number_comparator = sub {
 
 116     my $number_a = $number_xtor->($a);
 
 117     my $number_b = $number_xtor->($b);
 
 119     ncmp($number_a, $number_b) * $sort_dir;
 
 123   %scores = ( 'SL::DB::SalesProcess'    =>  10,
 
 124               'SL::DB::Order'           =>  sub { $scores{ $_[0]->type } },
 
 125               sales_quotation           =>  20,
 
 127               sales_delivery_order      =>  40,
 
 128               'SL::DB::DeliveryOrder'   =>  sub { $scores{ $_[0]->type } },
 
 129               'SL::DB::Invoice'         =>  50,
 
 130               request_quotation         => 120,
 
 131               purchase_order            => 130,
 
 132               purchase_delivery_order   => 140,
 
 133               'SL::DB::PurchaseInvoice' => 150,
 
 136   my $score_xtor = sub {
 
 137     my $score = $scores{ ref($_[0]) };
 
 138     $score    = $score->($_[0]) if ref($score) eq 'CODE';
 
 139     return $score || $scores{UNKNOWN};
 
 141   my $type_comparator = sub {
 
 142     my $score_a = $score_xtor->($a);
 
 143     my $score_b = $score_xtor->($b);
 
 145     $score_a == $score_b ? $number_comparator->() : ($score_a <=> $score_b) * $sort_dir;
 
 148   my $today     = DateTime->today_local;
 
 149   my $date_xtor = sub {
 
 150       $_[0]->can('transdate_as_date') ? $_[0]->transdate_as_date
 
 151     : $_[0]->can('itime_as_date')     ? $_[0]->itime_as_date
 
 154   my $date_comparator = sub {
 
 155     my $date_a = $date_xtor->($a);
 
 156     my $date_b = $date_xtor->($b);
 
 158     ($date_a <=> $date_b) * $sort_dir;
 
 161   my $comparator = $sort_by eq 'number' ? $number_comparator
 
 162                  : $sort_by eq 'date'   ? $date_comparator
 
 165   return [ sort($comparator @records) ];
 
 176 SL::DB::Helpers::LinkedRecords - Mixin for retrieving linked records via the table C<record_links>
 
 182 =item C<linked_records %params>
 
 184 Retrieves records linked from or to C<$self> via the table
 
 185 C<record_links>. The mandatory parameter C<direction> (either C<from>,
 
 186 C<to> or C<both>) determines whether the function retrieves records
 
 187 that link to C<$self> (for C<direction> = C<to>) or that are linked
 
 188 from C<$self> (for C<direction> = C<from>). For C<direction = both>
 
 189 all records linked from or to C<$self> are returned.
 
 191 The optional parameter C<from> or C<to> (same as C<direction>)
 
 192 contains the package names of Rose models for table limitation. It can
 
 193 be a single model name as a single scalar or multiple model names in
 
 194 an array reference in which case all links matching any of the model
 
 195 names will be returned.
 
 197 If you only need invoices created from an order C<$order> then the
 
 198 call could look like this:
 
 200   my $invoices = $order->linked_records(direction => 'to',
 
 201                                         to        => 'SL::DB::Invoice');
 
 203 The optional parameter C<query> can be used to limit the records
 
 204 returned. The following call limits the earlier example to invoices
 
 207   my $invoices = $order->linked_records(direction => 'to',
 
 208                                         to        => 'SL::DB::Invoice',
 
 209                                         query     => [ transdate => DateTime->today_local ]);
 
 211 Returns an array reference.
 
 213 =item C<link_to_record $record, %params>
 
 215 Will create an entry in the table C<record_links> with the C<from>
 
 216 side being C<$self> and the C<to> side being C<$record>. Will only
 
 217 insert a new entry if such a link does not already exist.
 
 219 If C<$params{bidirectional}> is trueish then another link will be
 
 220 created with the roles of C<from> and C<to> reversed. This link will
 
 221 also only be created if it doesn't exist already.
 
 223 In scalar contenxt returns either the existing link or the newly
 
 224 created one as an instance of C<SL::DB::RecordLink>. In array context
 
 225 it returns an array of links (one entry if C<$params{bidirectional}>
 
 226 is falsish and two entries if it is trueish).
 
 228 =item C<sort_linked_records $sort_by, $sort_dir, @records>
 
 230 Sorts linked records by C<$sort_by> in the direction given by
 
 231 C<$sort_dir> (trueish = ascending, falsish = descending). C<@records>
 
 232 can be either a single array reference or or normal array.
 
 234 C<$sort_by> can be one of the following strings:
 
 240 Sort by type first and by record number second. The type order
 
 241 reflects the order in which records are usually processed by the
 
 242 employees: sales processes, sales quotations, sales orders, sales
 
 243 delivery orders, invoices; requests for quotation, purchase orders,
 
 244 purchase delivery orders, purchase invoices.
 
 248 Sort by the record's running number.
 
 252 Sort by the date the record was created or applies to.
 
 256 Returns a hash reference.
 
 258 Can be called both as a class or as an instance function.
 
 260 This function is not exported.
 
 262 =item C<linked_records_sorted $sort_by, $sort_dir, %params>
 
 264 Returns the result of L</linked_records> sorted by
 
 265 L</sort_linked_records>. C<%params> is passed to
 
 266 L</linked_records>. C<$sort_by> and C<$sort_dir> are passed to
 
 267 L</sort_linked_records>.
 
 273 This mixin exports the functions L</linked_records>,
 
 274 L</link_to_record> and L</linked_records_sorted>.
 
 282 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>