Bericht über Lagerbewegungen: Wurde ein Filter nach Chargennummer verwendet, so wurde...
[kivitendo-erp.git] / SL / WH.pm
1 #====================================================================
2 # LX-Office ERP
3 # Copyright (C) 2004
4 # Based on SQL-Ledger Version 2.1.9
5 # Web http://www.lx-office.org
6 #
7 #=====================================================================
8 # SQL-Ledger Accounting
9 # Copyright (C) 1999-2003
10 #
11 #  Author: Dieter Simader
12 #   Email: dsimader@sql-ledger.org
13 #     Web: http://www.sql-ledger.org
14 #
15 #  Contributors:
16 #
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
21 #
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25 # GNU General Public License for more details.
26 # You should have received a copy of the GNU General Public License
27 # along with this program; if not, write to the Free Software
28 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
29 #======================================================================
30 #
31 #  Warehouse module
32 #
33 #======================================================================
34
35 package WH;
36
37 use SL::AM;
38 use SL::DBUtils;
39 use SL::Form;
40
41 sub transfer {
42   $main::lxdebug->enter_sub();
43
44   my $self = shift;
45
46   if (!@_) {
47     $main::lxdebug->leave_sub();
48     return;
49   }
50
51   my $myconfig = \%main::myconfig;
52   my $form     = $main::form;
53
54   my $dbh      = $form->get_standard_dbh($myconfig);
55
56   my $units    = AM->retrieve_units($myconfig, $form);
57
58   my $query    = qq|SELECT * FROM transfer_type|;
59   my $sth      = prepare_execute_query($form, $dbh, $query);
60
61   my %transfer_types;
62
63   while (my $ref = $sth->fetchrow_hashref()) {
64     $transfer_types{$ref->{direction}} ||= { };
65     $transfer_types{$ref->{direction}}->{$ref->{description}} = $ref->{id};
66   }
67
68   my @part_ids  = map { $_->{parts_id} } @_;
69   my %partunits = selectall_as_map($form, $dbh, qq|SELECT id, unit FROM parts WHERE id IN (| . join(', ', map { '?' } @part_ids ) . qq|)|, 'id', 'unit', @part_ids);
70
71   my ($now)     = selectrow_query($form, $dbh, qq|SELECT current_date|);
72
73   $query = qq|INSERT INTO inventory (warehouse_id, bin_id, parts_id, chargenumber, oe_id, orderitems_id, shippingdate,
74                                      employee_id, project_id, trans_id, trans_type_id, comment, qty)
75               VALUES (?, ?, ?, ?, ?, ?, ?, (SELECT id FROM employee WHERE login = ?), ?, ?, ?, ?, ?)|;
76
77   $sth   = prepare_query($form, $dbh, $query);
78
79   my @directions = (undef, 'out', 'in', 'transfer');
80
81   while (@_) {
82     my $transfer   = shift;
83     my ($trans_id) = selectrow_query($form, $dbh, qq|SELECT nextval('id')|);
84
85     my ($direction, @values) = (0);
86
87     $direction |= 1 if ($transfer->{src_warehouse_id} && $transfer->{src_bin_id});
88     $direction |= 2 if ($transfer->{dst_warehouse_id} && $transfer->{dst_bin_id});
89
90     push @values, conv_i($transfer->{parts_id}), "$transfer->{chargenumber}", conv_i($transfer->{oe_id}), conv_i($transfer->{orderitems_id});
91     push @values, $transfer->{shippingdate} eq 'current_date' ? $now : conv_date($transfer->{shippingdate}), $form->{login}, conv_i($transfer->{project_id}), $trans_id;
92
93     if ($transfer->{transfer_type_id}) {
94       push @values, $transfer->{transfer_type_id};
95     } else {
96       push @values, $transfer_types{$directions[$direction]}->{$transfer->{transfer_type}};
97     }
98
99     push @values, "$transfer->{comment}";
100
101     $qty = $transfer->{qty};
102
103     if ($transfer->{unit}) {
104       my $partunit = $partunits{$transfer->{parts_id}};
105
106       $qty *= $units->{$transfer->{unit}}->{factor};
107       $qty /= $units->{$partunit}->{factor} || 1 if ($partunit);
108     }
109
110     if ($direction & 1) {
111       do_statement($form, $sth, $query, conv_i($transfer->{src_warehouse_id}), conv_i($transfer->{src_bin_id}), @values, $qty * -1);
112     }
113
114     if ($direction & 2) {
115       do_statement($form, $sth, $query, conv_i($transfer->{dst_warehouse_id}), conv_i($transfer->{dst_bin_id}), @values, $qty);
116     }
117   }
118
119   $sth->finish();
120
121   $dbh->commit();
122
123   $main::lxdebug->leave_sub();
124 }
125
126 sub get_warehouse_journal {
127   $main::lxdebug->enter_sub();
128
129   my $self      = shift;
130   my %filter    = @_;
131
132   my $myconfig  = \%main::myconfig;
133   my $form      = $main::form;
134
135   my $all_units = AM->retrieve_units($myconfig, $form);
136
137   # connect to database
138   my $dbh = $form->get_standard_dbh($myconfig);
139
140   # filters
141   my (@filter_ary, @filter_vars, $joins);
142
143   if ($filter{warehouse_id} ne '') {
144     push @filter_ary, "w1.id = ? OR w2.id = ?";
145     push @filter_vars, $filter{warehouse_id}, $filter{warehouse_id};
146   }
147
148   if ($filter{bin_id} ne '') {
149     push @filter_ary, "b1.id = ? OR b2.id = ?";
150     push @filter_vars, $filter{bin_id}, $filter{bin_id};
151   }
152
153   if ($filter{partnumber}) {
154     push @filter_ary, "p.partnumber ILIKE ?";
155     push @filter_vars, '%' . $filter{partnumber} . '%';
156   }
157
158   if ($filter{description}) {
159     push @filter_ary, "(p.description ILIKE ?)";
160     push @filter_vars, '%' . $filter{description} . '%';
161   }
162
163   if ($filter{chargenumber}) {
164     push @filter_ary, "i1.chargenumber ILIKE ?";
165     push @filter_vars, '%' . $filter{chargenumber} . '%';
166   }
167
168   if ($form->{fromdate}) {
169     push @filter_ary, "?::DATE <= i1.itime::DATE";
170     push @filter_vars, $form->{fromdate};
171   }
172
173   if ($form->{todate}) {
174     push @filter_ary, "?::DATE >= i1.itime::DATE";
175     push @filter_vars, $form->{todate};
176   }
177
178   if ($form->{l_employee}) {
179     $joins .= "";
180   }
181
182   # prepare qty comparison for later filtering
183   my ($f_qty_op, $f_qty, $f_qty_base_unit);
184   if ($filter{qty_op} && defined($filter{qty}) && $filter{qty_unit} && $all_units->{$filter{qty_unit}}) {
185     $f_qty_op        = $filter{qty_op};
186     $f_qty           = $filter{qty} * $all_units->{$filter{qty_unit}}->{factor};
187     $f_qty_base_unit = $all_units->{$filter{qty_unit}}->{base_unit};
188   }
189
190   map { $_ = "(${_})"; } @filter_ary;
191
192   # if of a property number or description is requested,
193   # automatically check the matching id too.
194   map { $form->{"l_${_}id"} = "Y" if ($form->{"l_${_}description"} || $form->{"l_${_}number"}); } qw(warehouse bin);
195
196   # customize shown entry for not available fields.
197   $filter{na} = '-' unless $filter{na};
198
199   # make order, search in $filter and $form
200   $form->{sort}   = $filter{sort}             unless $form->{sort};
201   $form->{order}  = ($form->{sort} = 'itime') unless $form->{sort};
202   $form->{sort}   = 'itime'                   if     $form->{sort} eq "date";
203   $form->{order}  = $filter{order}            unless $form->{order};
204   $form->{sort}  .= (($form->{order}) ? " DESC" : " ASC");
205
206   my $where_clause = join(" AND ", @filter_ary) . " AND " if (@filter_ary);
207
208   $select_tokens{'trans'} = {
209      "parts_id"             => "i1.parts_id",
210      "qty"                  => "ABS(SUM(i1.qty))",
211      "partnumber"           => "p.partnumber",
212      "partdescription"      => "p.description",
213      "bindescription"       => "b.description",
214      "chargenumber"         => "i1.chargenumber",
215      "warehousedescription" => "w.description",
216      "partunit"             => "p.unit",
217      "bin_from"             => "b1.description",
218      "bin_to"               => "b2.description",
219      "warehouse_from"       => "w1.description",
220      "warehouse_to"         => "w2.description",
221      "comment"              => "i1.comment",
222      "trans_type"           => "tt.description",
223      "trans_id"             => "i1.trans_id",
224      "date"                 => "i1.itime::DATE",
225      "itime"                => "i1.itime",
226      "employee"             => "e.name",
227      "projectnumber"        => "COALESCE(pr.projectnumber, '$filter{na}')",
228      };
229
230   $select_tokens{'out'} = {
231      "bin_to"               => "'$filter{na}'",
232      "warehouse_to"         => "'$filter{na}'",
233      };
234
235   $select_tokens{'in'} = {
236      "bin_from"             => "'$filter{na}'",
237      "warehouse_from"       => "'$filter{na}'",
238      };
239
240   # build the select clauses.
241   # take all the requested ones from the first hash and overwrite them from the out/in hashes if present.
242   for my $i ('trans', 'out', 'in') {
243     $select{$i} = join ', ', map { +/^l_/; ($select_tokens{$i}{"$'"} || $select_tokens{'trans'}{"$'"}) . " AS r_$'" }
244           ( grep( { !/qty$/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form), qw(l_parts_id l_qty l_partunit l_itime) );
245   }
246
247   my $group_clause = join ", ", map { +/^l_/; "r_$'" }
248         ( grep( { !/qty$/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form), qw(l_parts_id l_partunit l_itime) );
249
250   my $query =
251   qq|SELECT DISTINCT $select{trans}
252     FROM inventory i1
253     LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id
254     LEFT JOIN parts p ON i1.parts_id = p.id
255     LEFT JOIN bin b1 ON i1.bin_id = b1.id
256     LEFT JOIN bin b2 ON i2.bin_id = b2.id
257     LEFT JOIN warehouse w1 ON i1.warehouse_id = w1.id
258     LEFT JOIN warehouse w2 ON i2.warehouse_id = w2.id
259     LEFT JOIN transfer_type tt ON i1.trans_type_id = tt.id
260     LEFT JOIN project pr ON i1.project_id = pr.id
261     LEFT JOIN employee e ON i1.employee_id = e.id
262     WHERE $where_clause i2.qty = -i1.qty AND i2.qty > 0 AND
263           i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) = 2 )
264     GROUP BY $group_clause
265
266     UNION
267
268     SELECT DISTINCT $select{out}
269     FROM inventory i1
270     LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id
271     LEFT JOIN parts p ON i1.parts_id = p.id
272     LEFT JOIN bin b1 ON i1.bin_id = b1.id
273     LEFT JOIN bin b2 ON i2.bin_id = b2.id
274     LEFT JOIN warehouse w1 ON i1.warehouse_id = w1.id
275     LEFT JOIN warehouse w2 ON i2.warehouse_id = w2.id
276     LEFT JOIN transfer_type tt ON i1.trans_type_id = tt.id
277     LEFT JOIN project pr ON i1.project_id = pr.id
278     LEFT JOIN employee e ON i1.employee_id = e.id
279     WHERE $where_clause i1.qty < 0 AND
280           i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) = 1 )
281     GROUP BY $group_clause
282
283     UNION
284
285     SELECT DISTINCT $select{in}
286     FROM inventory i1
287     LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id
288     LEFT JOIN parts p ON i1.parts_id = p.id
289     LEFT JOIN bin b1 ON i1.bin_id = b1.id
290     LEFT JOIN bin b2 ON i2.bin_id = b2.id
291     LEFT JOIN warehouse w1 ON i1.warehouse_id = w1.id
292     LEFT JOIN warehouse w2 ON i2.warehouse_id = w2.id
293     LEFT JOIN transfer_type tt ON i1.trans_type_id = tt.id
294     LEFT JOIN project pr ON i1.project_id = pr.id
295     LEFT JOIN employee e ON i1.employee_id = e.id
296     WHERE $where_clause i1.qty > 0 AND
297           i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) = 1 )
298     GROUP BY $group_clause
299     ORDER BY r_$form->{sort}|;
300
301   my $sth = prepare_execute_query($form, $dbh, $query, @filter_vars, @filter_vars, @filter_vars);
302
303   my @contents = ();
304   while (my $ref = $sth->fetchrow_hashref(NAME_lc)) {
305     map { /^r_/; $ref->{"$'"} = $ref->{$_} } keys %$ref;
306     my $qty = $ref->{"qty"} * 1;
307
308     next unless ($qty > 0);
309
310     if ($f_qty_op) {
311       my $part_unit = $all_units->{$ref->{"partunit"}};
312       next unless ($part_unit && ($part_unit->{"base_unit"} eq $f_qty_base_unit));
313       $qty *= $part_unit->{"factor"};
314       next if (('=' eq $f_qty_op) && ($qty != $f_qty));
315       next if (('>=' eq $f_qty_op) && ($qty < $f_qty));
316       next if (('<=' eq $f_qty_op) && ($qty > $f_qty));
317     }
318
319     push @contents, $ref;
320   }
321
322   $sth->finish();
323
324   $main::lxdebug->leave_sub();
325
326   return @contents;
327 }
328
329 #
330 # This sub is the primary function to retrieve information about items in warehouses.
331 # $filter is a hashref and supports the following keys:
332 #  - warehouse_id - will return matches with this warehouse_id only
333 #  - partnumber   - will return only matches where the given string is a substring of the partnumber
334 #  - partsid      - will return matches with this parts_id only
335 #  - description  - will return only matches where the given string is a substring of the description
336 #  - chargenumber - will return only matches where the given string is a substring of the chargenumber
337 #  - charge_ids   - must be an arrayref. will return contents with these ids only
338 #  - expires_in   - will only return matches that expire within the given number of days
339 #                   will also add a column named 'has_expired' containing if the match has already expired or not
340 #  - hazardous    - will return matches with the flag hazardous only
341 #  - oil          - will return matches with the flag oil only
342 #  - qty, qty_op  - quantity filter (more info to come)
343 #  - sort, order_by - sorting (more to come)
344 #  - reservation  - will provide an extra column containing the amount reserved of this match
345 # note: reservation flag turns off warehouse_* or bin_* information. both together don't make sense, since reserved info is stored separately
346 #
347 sub get_warehouse_report {
348   $main::lxdebug->enter_sub();
349
350   my $self      = shift;
351   my %filter    = @_;
352
353   my $myconfig  = \%main::myconfig;
354   my $form      = $main::form;
355
356   my $all_units = AM->retrieve_units($myconfig, $form);
357
358   # connect to database
359   my $dbh = $form->get_standard_dbh($myconfig);
360
361   # filters
362   my (@filter_ary, @filter_vars, @wh_bin_filter_ary, @wh_bin_filter_vars, $columns, $group_by);
363
364   delete $form->{include_empty_bins} unless ($form->{l_warehousedescription} || $form->{l_bindescription});
365
366   if ($filter{warehouse_id}) {
367     push @wh_bin_filter_ary,  "w.id = ?";
368     push @wh_bin_filter_vars, $filter{warehouse_id};
369   }
370
371   if ($filter{bin_id}) {
372     push @wh_bin_filter_ary,  "b.id = ?";
373     push @wh_bin_filter_vars, $filter{bin_id};
374   }
375
376   push @filter_ary,  @wh_bin_filter_ary;
377   push @filter_vars, @wh_bin_filter_vars;
378
379   if ($filter{partnumber}) {
380     push @filter_ary,  "p.partnumber ILIKE ?";
381     push @filter_vars, '%' . $filter{partnumber} . '%';
382   }
383
384   if ($filter{description}) {
385     push @filter_ary,  "p.description ILIKE ?";
386     push @filter_vars, '%' . $filter{description} . '%';
387   }
388
389   if ($filter{partsid}) {
390     push @filter_ary,  "p.id = ?";
391     push @filter_vars, $filter{partsid};
392   }
393
394   if ($filter{chargenumber}) {
395     push @filter_ary,  "i.chargenumber ILIKE ?";
396     push @filter_vars, '%' . $filter{chargenumber} . '%';
397   }
398
399   # prepare qty comparison for later filtering
400   my ($f_qty_op, $f_qty, $f_qty_base_unit);
401
402   if ($filter{qty_op} && defined $filter{qty} && $filter{qty_unit} && $all_units->{$filter{qty_unit}}) {
403     $f_qty_op        = $filter{qty_op};
404     $f_qty           = $filter{qty} * $all_units->{$filter{qty_unit}}->{factor};
405     $f_qty_base_unit = $all_units->{$filter{qty_unit}}->{base_unit};
406   }
407
408   map { $_ = "(${_})"; } @filter_ary;
409
410   # if of a property number or description is requested,
411   # automatically check the matching id too.
412   map { $form->{"l_${_}id"} = "Y" if ($form->{"l_${_}description"} || $form->{"l_${_}number"}); } qw(warehouse bin);
413
414   # make order, search in $filter and $form
415   $form->{sort}  =  $filter{sort}  unless $form->{sort};
416   $form->{sort}  =  "parts_id"     unless $form->{sort};
417   $form->{order} =  $filter{order} unless $form->{order};
418   $form->{sort}  =~ s/ASC|DESC//; # kill stuff left in from previous queries
419   my $orderby    =  $form->{sort};
420   $form->{sort} .=  (($form->{order}) ? " DESC" : " ASC");
421
422   my $where_clause = join " AND ", ("1=1", @filter_ary);
423
424   my %select_tokens = (
425      "parts_id"              => "i.parts_id",
426      "qty"                  => "SUM(i.qty)",
427      "warehouseid"          => "i.warehouse_id",
428      "partnumber"           => "p.partnumber",
429      "partdescription"      => "p.description",
430      "bindescription"       => "b.description",
431      "binid"                => "b.id",
432      "chargenumber"         => "i.chargenumber",
433      "chargeid"             => "c.id",
434      "warehousedescription" => "w.description",
435      "partunit"             => "p.unit",
436   );
437   my $select_clause = join ', ', map { +/^l_/; "$select_tokens{$'} AS $'" }
438         ( grep( { !/qty/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
439           qw(l_parts_id l_qty l_partunit) );
440
441   my $group_clause = join ", ", map { +/^l_/; "$'" }
442         ( grep( { !/qty/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
443           qw(l_parts_id l_partunit) );
444
445   my $query =
446     qq|SELECT $select_clause
447       $columns
448       FROM inventory i
449       LEFT JOIN parts     p ON i.parts_id     = p.id
450       LEFT JOIN bin       b ON i.bin_id       = b.id
451       LEFT JOIN warehouse w ON i.warehouse_id = w.id
452       WHERE $where_clause
453       GROUP BY $group_clause $group_by
454       ORDER BY $form->{sort}|;
455
456   my $sth = prepare_execute_query($form, $dbh, $query, @filter_vars);
457
458   my (%non_empty_bins, @all_fields, @contents);
459
460   while (my $ref = $sth->fetchrow_hashref(NAME_lc)) {
461     $ref->{qty} *= 1;
462     my $qty      = $ref->{qty};
463
464     next unless ($qty > 0);
465
466     if ($f_qty_op) {
467       my $part_unit = $all_units->{$ref->{partunit}};
468       next if (!$part_unit || ($part_unit->{base_unit} ne $f_qty_base_unit));
469       $qty *= $part_unit->{factor};
470       next if (('='  eq $f_qty_op) && ($qty != $f_qty));
471       next if (('>=' eq $f_qty_op) && ($qty <  $f_qty));
472       next if (('<=' eq $f_qty_op) && ($qty >  $f_qty));
473     }
474
475     if ($form->{include_empty_bins}) {
476       $non_empty_bins{$ref->{binid}} = 1;
477       @all_fields                    = keys %{ $ref } unless (@all_fields);
478     }
479
480     push @contents, $ref;
481   }
482
483   $sth->finish();
484
485   if ($form->{include_empty_bins}) {
486     $query =
487       qq|SELECT
488            w.id AS warehouseid, w.description AS warehousedescription,
489            b.id AS binid, b.description AS bindescription
490          FROM bin b
491          LEFT JOIN warehouse w ON (b.warehouse_id = w.id)|;
492
493     @filter_ary  = @wh_bin_filter_ary;
494     @filter_vars = @wh_bin_filter_vars;
495
496     my @non_empty_bin_ids = keys %non_empty_bins;
497     if (@non_empty_bin_ids) {
498       push @filter_ary,  qq|NOT b.id IN (| . join(', ', map { '?' } @non_empty_bin_ids) . qq|)|;
499       push @filter_vars, @non_empty_bin_ids;
500     }
501
502     $query .= qq| WHERE | . join(' AND ', map { "($_)" } @filter_ary) if (@filter_ary);
503
504     $sth    = prepare_execute_query($form, $dbh, $query, @filter_vars);
505
506     while ($ref = $sth->fetchrow_hashref()) {
507       map { $ref->{$_} ||= "" } @all_fields;
508       push @contents, $ref;
509     }
510     $sth->finish();
511
512     if (grep { $orderby eq $_ } qw(bindescription warehousedescription)) {
513       @contents = sort { ($a->{$orderby} cmp $b->{$orderby}) * (($form->{order}) ? 1 : -1) } @contents;
514     }
515   }
516
517   $main::lxdebug->leave_sub();
518
519   return @contents;
520 }
521
522 sub convert_qty_op {
523   $main::lxdebug->enter_sub();
524
525   my ($self, $qty_op) = @_;
526
527   if (!$qty_op || ($qty_op eq "dontcare")) {
528     $main::lxdebug->leave_sub();
529     return undef;
530   }
531
532   if ($qty_op eq "atleast") {
533     $qty_op = '>=';
534   } elsif ($qty_op eq "atmost") {
535     $qty_op = '<=';
536   } else {
537     $qty_op = '=';
538   }
539
540   $main::lxdebug->leave_sub();
541
542   return $qty_op;
543 }
544
545 sub retrieve_transfer_types {
546   $main::lxdebug->enter_sub();
547
548   my $self      = shift;
549   my $direction = shift;
550
551   my $myconfig  = \%main::myconfig;
552   my $form      = $main::form;
553
554   my $dbh       = $form->get_standard_dbh($myconfig);
555
556   my $types     = selectall_hashref_query($form, $dbh, qq|SELECT * FROM transfer_type WHERE direction = ? ORDER BY sortkey|, $direction);
557
558   $main::lxdebug->leave_sub();
559
560   return $types;
561 }
562
563 sub get_basic_bin_info {
564   $main::lxdebug->enter_sub();
565
566   my $self     = shift;
567   my %params   = @_;
568
569   Common::check_params(\%params, qw(id));
570
571   my $myconfig = \%main::myconfig;
572   my $form     = $main::form;
573
574   my $dbh      = $params{dbh} || $form->get_standard_dbh();
575
576   my @ids      = 'ARRAY' eq ref $params{id} ? @{ $params{id} } : ($params{id});
577
578   my $query    =
579     qq|SELECT b.id AS bin_id, b.description AS bin_description,
580          w.id AS warehouse_id, w.description AS warehouse_description
581        FROM bin b
582        LEFT JOIN warehouse w ON (b.warehouse_id = w.id)
583        WHERE b.id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
584
585   my $result = selectall_hashref_query($form, $dbh, $query, map { conv_i($_) } @ids);
586
587   if ('' eq ref $params{id}) {
588     $result = $result->[0] || { };
589     $main::lxdebug->leave_sub();
590
591     return $result;
592   }
593
594   $main::lxdebug->leave_sub();
595
596   return map { $_->{bin_id} => $_ } @{ $result };
597 }
598
599
600 1;