1 #====================================================================
4 # Based on SQL-Ledger Version 2.1.9
5 # Web http://www.lx-office.org
7 #=====================================================================
8 # SQL-Ledger Accounting
9 # Copyright (C) 1999-2003
11 # Author: Dieter Simader
12 # Email: dsimader@sql-ledger.org
13 # Web: http://www.sql-ledger.org
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.
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., 51 Franklin Street, Fifth Floor, Boston,
30 #======================================================================
34 #======================================================================
39 use List::MoreUtils qw(any);
43 use SL::DB::Inventory;
45 use SL::Locale::String qw(t8);
46 use SL::Util qw(trim);
52 $::lxdebug->enter_sub;
54 my ($self, @args) = @_;
57 $::lxdebug->leave_sub;
61 require SL::DB::TransferType;
63 require SL::DB::Employee;
65 my $employee = SL::DB::Manager::Employee->current;
66 my ($now) = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT current_date|);
67 my @directions = (undef, qw(out in transfer));
70 my ($transfer, $field, $class, @find_by) = @_;
72 @find_by = (description => $transfer->{$field}) unless @find_by;
74 if ($transfer->{$field} || $transfer->{"${field}_id"}) {
75 return ref $transfer->{$field} && $transfer->{$field}->isa($class) ? $transfer->{$field}
76 : $transfer->{$field} ? $class->_get_manager_class->find_by(@find_by)
77 : $class->_get_manager_class->find_by(id => $transfer->{"${field}_id"});
84 my $db = SL::DB::Inventory->new->db;
85 $db->with_transaction(sub{
86 while (my $transfer = shift @args) {
88 ($trans_id) = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT nextval('id')|) if $transfer->{qty};
90 my $part = $objectify->($transfer, 'parts', 'SL::DB::Part');
91 my $unit = $objectify->($transfer, 'unit', 'SL::DB::Unit', name => $transfer->{unit});
92 my $qty = $transfer->{qty};
93 my $src_bin = $objectify->($transfer, 'src_bin', 'SL::DB::Bin');
94 my $dst_bin = $objectify->($transfer, 'dst_bin', 'SL::DB::Bin');
95 my $src_wh = $objectify->($transfer, 'src_warehouse', 'SL::DB::Warehouse');
96 my $dst_wh = $objectify->($transfer, 'dst_warehouse', 'SL::DB::Warehouse');
97 my $project = $objectify->($transfer, 'project', 'SL::DB::Project');
99 $src_wh ||= $src_bin->warehouse if $src_bin;
100 $dst_wh ||= $dst_bin->warehouse if $dst_bin;
102 my $direction = 0; # bit mask
103 $direction |= 1 if $src_bin;
104 $direction |= 2 if $dst_bin;
106 my $transfer_type_id;
107 if ($transfer->{transfer_type_id}) {
108 $transfer_type_id = $transfer->{transfer_type_id};
110 my $transfer_type = $objectify->($transfer, 'transfer_type', 'SL::DB::TransferType', direction => $directions[$direction],
111 description => $transfer->{transfer_type});
112 $transfer_type_id = $transfer_type->id;
115 my $stocktaking_qty = $transfer->{stocktaking_qty};
119 employee => $employee,
120 trans_type_id => $transfer_type_id,
122 trans_id => $trans_id,
123 shippingdate => !$transfer->{shippingdate} || $transfer->{shippingdate} eq 'current_date'
124 ? $now : $transfer->{shippingdate},
125 map { $_ => $transfer->{$_} } qw(chargenumber bestbefore oe_id delivery_order_items_stock_id invoice_id comment),
129 $qty = $unit->convert_to($qty, $part->unit_obj);
130 $stocktaking_qty = $unit->convert_to($stocktaking_qty, $part->unit_obj);
133 $params{chargenumber} ||= '';
136 if ($qty && $direction & 1) {
137 push @inventories, SL::DB::Inventory->new(
139 warehouse => $src_wh,
145 if ($qty && $direction & 2) {
146 push @inventories, SL::DB::Inventory->new(
148 warehouse => $dst_wh->id,
152 # Standardlagerplatz in Stammdaten gleich mitverschieben
153 if (defined($transfer->{change_default_bin})){
154 $part->update_attributes(warehouse_id => $dst_wh->id, bin_id => $dst_bin->id);
158 # Record stocktaking if requested.
159 # This is only possible if transfer was a stock in or stock out,
160 # but not both (transfer).
161 if ($transfer->{record_stocktaking}) {
162 die 'Stocktaking can only be recorded for stock in or stock out, but not on a transfer.' if scalar @inventories > 1;
165 $inventory_id = $inventories[0]->id if $inventories[0];
167 SL::DB::Stocktaking->new(
168 inventory_id => $inventory_id,
169 warehouse => $src_wh || $dst_wh,
170 bin => $src_bin || $dst_bin,
171 parts_id => $part->id,
172 employee_id => $employee->id,
173 qty => $stocktaking_qty,
174 comment => $transfer->{comment},
175 cutoff_date => $transfer->{stocktaking_cutoff_date},
176 chargenumber => $transfer->{chargenumber},
177 bestbefore => $transfer->{bestbefore},
182 push @trans_ids, $trans_id;
187 $::form->error("Warehouse transfer error: " . join("\n", (split(/\n/, $db->error))[0..2]));
190 $::lxdebug->leave_sub;
195 sub get_warehouse_journal {
196 $main::lxdebug->enter_sub();
201 my $myconfig = \%main::myconfig;
202 my $form = $main::form;
204 my $all_units = AM->retrieve_units($myconfig, $form);
206 # connect to database
207 my $dbh = $form->get_standard_dbh($myconfig);
210 my (@filter_ary, @filter_vars, $joins, %select_tokens, %select);
212 if ($filter{warehouse_id}) {
213 push @filter_ary, "w1.id = ? OR w2.id = ?";
214 push @filter_vars, $filter{warehouse_id}, $filter{warehouse_id};
217 if ($filter{bin_id}) {
218 push @filter_ary, "b1.id = ? OR b2.id = ?";
219 push @filter_vars, $filter{bin_id}, $filter{bin_id};
222 if ($filter{partnumber}) {
223 push @filter_ary, "p.partnumber ILIKE ?";
224 push @filter_vars, like($filter{partnumber});
227 if ($filter{description}) {
228 push @filter_ary, "(p.description ILIKE ?)";
229 push @filter_vars, like($filter{description});
232 if ($filter{classification_id}) {
233 push @filter_ary, "p.classification_id = ?";
234 push @filter_vars, $filter{classification_id};
237 if ($filter{chargenumber}) {
238 push @filter_ary, "i1.chargenumber ILIKE ?";
239 push @filter_vars, like($filter{chargenumber});
242 if (trim($form->{bestbefore})) {
243 push @filter_ary, "?::DATE = i1.bestbefore::DATE";
244 push @filter_vars, trim($form->{bestbefore});
247 if (trim($form->{fromdate})) {
248 push @filter_ary, "? <= i1.shippingdate";
249 push @filter_vars, trim($form->{fromdate});
252 if (trim($form->{todate})) {
253 push @filter_ary, "? >= i1.shippingdate";
254 push @filter_vars, trim($form->{todate});
257 if ($form->{l_employee}) {
261 # prepare qty comparison for later filtering
262 my ($f_qty_op, $f_qty, $f_qty_base_unit);
263 if ($filter{qty_op} && defined($filter{qty}) && $filter{qty_unit} && $all_units->{$filter{qty_unit}}) {
264 $f_qty_op = $filter{qty_op};
265 $f_qty = $filter{qty} * $all_units->{$filter{qty_unit}}->{factor};
266 $f_qty_base_unit = $all_units->{$filter{qty_unit}}->{base_unit};
269 map { $_ = "(${_})"; } @filter_ary;
271 # if of a property number or description is requested,
272 # automatically check the matching id too.
273 map { $form->{"l_${_}id"} = "Y" if ($form->{"l_${_}"} || $form->{"l_${_}number"}); } qw(warehouse bin);
275 # customize shown entry for not available fields.
276 $filter{na} = '-' unless $filter{na};
278 # make order, search in $filter and $form
279 my $sort_col = $form->{sort};
280 my $sort_order = $form->{order};
282 $sort_col = $filter{sort} unless $sort_col;
283 $sort_col = 'shippingdate' if $sort_col eq 'date';
284 $sort_order = ($sort_col = 'shippingdate') unless $sort_col;
287 'shippingdate' => ['shippingdate', 'r_itime', 'r_parts_id'],
288 'bin_to' => ['bin_to', 'r_itime', 'r_parts_id'],
289 'bin_from' => ['bin_from', 'r_itime', 'r_parts_id'],
290 'warehouse_to' => ['warehouse_to, r_itime, r_parts_id'],
291 'warehouse_from' => ['warehouse_from, r_itime, r_parts_id'],
292 'partnumber' => ['partnumber'],
293 'partdescription'=> ['partdescription'],
294 'partunit' => ['partunit, r_itime, r_parts_id'],
295 'qty' => ['qty, r_itime, r_parts_id'],
296 'oe_id' => ['oe_id'],
297 'comment' => ['comment'],
298 'trans_type' => ['trans_type'],
299 'employee' => ['employee'],
300 'projectnumber' => ['projectnumber'],
301 'chargenumber' => ['chargenumber'],
304 $sort_order = $filter{order} unless $sort_order;
305 my $ASC = ($sort_order ? " DESC" : " ASC");
306 my $sort_spec = join("$ASC , ", @{$orderspecs{$sort_col}}). " $ASC";
308 my $where_clause = @filter_ary ? join(" AND ", @filter_ary) . " AND " : '';
310 my ($cvar_where, @cvar_values) = CVar->build_filter_query(
312 trans_id_field => 'p.id',
318 $where_clause .= qq| ($cvar_where) AND |;
319 push @filter_vars, @cvar_values;
322 $select_tokens{'trans'} = {
323 "parts_id" => "i1.parts_id",
324 "qty" => "ABS(SUM(i1.qty))",
325 "partnumber" => "p.partnumber",
326 "partdescription" => "p.description",
327 "classification_id" => "p.classification_id",
328 "part_type" => "p.part_type",
329 "bin" => "b.description",
330 "chargenumber" => "i1.chargenumber",
331 "bestbefore" => "i1.bestbefore",
332 "warehouse" => "w.description",
333 "partunit" => "p.unit",
334 "bin_from" => "b1.description",
335 "bin_to" => "b2.description",
336 "warehouse_from" => "w1.description",
337 "warehouse_to" => "w2.description",
338 "comment" => "i1.comment",
339 "trans_type" => "tt.description",
340 "trans_id" => "i1.trans_id",
342 "oe_id" => "COALESCE(i1.oe_id, i2.oe_id)",
343 "invoice_id" => "COALESCE(i1.invoice_id, i2.invoice_id)",
344 "date" => "i1.shippingdate",
345 "itime" => "i1.itime",
346 "shippingdate" => "i1.shippingdate",
347 "employee" => "e.name",
348 "projectnumber" => "COALESCE(pr.projectnumber, '$filter{na}')",
351 $select_tokens{'out'} = {
352 "bin_to" => "'$filter{na}'",
353 "warehouse_to" => "'$filter{na}'",
356 $select_tokens{'in'} = {
357 "bin_from" => "'$filter{na}'",
358 "warehouse_from" => "'$filter{na}'",
361 $form->{l_classification_id} = 'Y';
362 $form->{l_trans_id} = 'Y';
363 $form->{l_part_type} = 'Y';
364 $form->{l_itime} = 'Y';
365 $form->{l_invoice_id} = $form->{l_oe_id} if $form->{l_oe_id};
367 # build the select clauses.
368 # take all the requested ones from the first hash and overwrite them from the out/in hashes if present.
369 for my $i ('trans', 'out', 'in') {
370 $select{$i} = join ', ', map { +/^l_/; ($select_tokens{$i}{"$'"} || $select_tokens{'trans'}{"$'"}) . " AS r_$'" }
371 ( grep( { !/qty$/ and !/^l_cvar/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form), qw(l_parts_id l_qty l_partunit l_shippingdate) );
374 my $group_clause = join ", ", map { +/^l_/; "r_$'" }
375 ( grep( { !/qty$/ and !/^l_cvar/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form), qw(l_parts_id l_partunit l_shippingdate l_itime) );
377 $where_clause = defined($where_clause) ? $where_clause : '';
381 SELECT DISTINCT $select{out}
383 LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id AND i1.id = i2.id
384 LEFT JOIN parts p ON i1.parts_id = p.id
385 LEFT JOIN bin b1 ON i1.bin_id = b1.id
386 LEFT JOIN bin b2 ON i2.bin_id = b2.id
387 LEFT JOIN warehouse w1 ON i1.warehouse_id = w1.id
388 LEFT JOIN warehouse w2 ON i2.warehouse_id = w2.id
389 LEFT JOIN transfer_type tt ON i1.trans_type_id = tt.id
390 LEFT JOIN project pr ON i1.project_id = pr.id
391 LEFT JOIN employee e ON i1.employee_id = e.id
392 WHERE $where_clause i1.qty != 0 AND tt.direction = 'out' AND
393 i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) >= 1 )
394 GROUP BY $group_clause
398 SELECT DISTINCT $select{in}
400 LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id AND i1.id = i2.id
401 LEFT JOIN parts p ON i1.parts_id = p.id
402 LEFT JOIN bin b1 ON i1.bin_id = b1.id
403 LEFT JOIN bin b2 ON i2.bin_id = b2.id
404 LEFT JOIN warehouse w1 ON i1.warehouse_id = w1.id
405 LEFT JOIN warehouse w2 ON i2.warehouse_id = w2.id
406 LEFT JOIN transfer_type tt ON i1.trans_type_id = tt.id
407 LEFT JOIN project pr ON i1.project_id = pr.id
408 LEFT JOIN employee e ON i1.employee_id = e.id
409 WHERE $where_clause i1.qty != 0 AND tt.direction = 'in' AND
410 i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) >= 1 )
411 GROUP BY $group_clause
412 ORDER BY r_${sort_spec}) AS lines WHERE r_qty != 0|;
414 my @all_vars = (@filter_vars,@filter_vars);
416 if ($filter{limit}) {
417 $query .= " LIMIT ?";
418 push @all_vars,$filter{limit};
420 if ($filter{offset}) {
421 $query .= " OFFSET ?";
422 push @all_vars, $filter{offset};
425 my $sth = prepare_execute_query($form, $dbh, $query, @all_vars);
427 my ($h_oe_id, $q_oe_id);
428 if ($form->{l_oe_id}) {
430 SELECT dord.id AS id, dord.donumber AS number,
431 dord.record_type::text AS type
432 FROM delivery_orders dord
437 SELECT ar.id AS id, ar.invnumber AS number, 'sales_invoice' AS type
439 WHERE ar.id = (SELECT trans_id FROM invoice WHERE id = ?)
443 SELECT ap.id AS id, ap.invnumber AS number, 'purchase_invoice' AS type
445 WHERE ap.id = (SELECT trans_id FROM invoice WHERE id = ?)
447 $h_oe_id = prepare_query($form, $dbh, $q_oe_id);
451 while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
452 map { /^r_/; $ref->{"$'"} = $ref->{$_} } keys %$ref;
453 my $qty = $ref->{"qty"} * 1;
455 next unless ($qty > 0);
458 my $part_unit = $all_units->{$ref->{"partunit"}};
459 next unless ($part_unit && ($part_unit->{"base_unit"} eq $f_qty_base_unit));
460 $qty *= $part_unit->{"factor"};
461 next if (('=' eq $f_qty_op) && ($qty != $f_qty));
462 next if (('>=' eq $f_qty_op) && ($qty < $f_qty));
463 next if (('<=' eq $f_qty_op) && ($qty > $f_qty));
466 if ($h_oe_id && ($ref->{oe_id} || $ref->{invoice_id})) {
467 do_statement($form, $h_oe_id, $q_oe_id, $ref->{oe_id}, ($ref->{invoice_id}) x 2);
468 $ref->{oe_id_info} = $h_oe_id->fetchrow_hashref() || {};
471 push @contents, $ref;
475 $h_oe_id->finish() if $h_oe_id;
477 $main::lxdebug->leave_sub();
483 # This sub is the primary function to retrieve information about items in warehouses.
484 # $filter is a hashref and supports the following keys:
485 # - warehouse_id - will return matches with this warehouse_id only
486 # - partnumber - will return only matches where the given string is a substring of the partnumber
487 # - partsid - will return matches with this parts_id only
488 # - classification_id - will return matches with this parts with this classification only
489 # - description - will return only matches where the given string is a substring of the description
490 # - chargenumber - will return only matches where the given string is a substring of the chargenumber
491 # - bestbefore - will return only matches with this bestbefore date
492 # - ean - will return only matches where the given string is a substring of the ean as stored in the table parts (article)
493 # - charge_ids - must be an arrayref. will return contents with these ids only
494 # - expires_in - will only return matches that expire within the given number of days
495 # will also add a column named 'has_expired' containing if the match has already expired or not
496 # - hazardous - will return matches with the flag hazardous only
497 # - oil - will return matches with the flag oil only
498 # - qty, qty_op - quantity filter (more info to come)
499 # - sort, order_by - sorting (more to come)
500 # - reservation - will provide an extra column containing the amount reserved of this match
501 # note: reservation flag turns off warehouse_* or bin_* information. both together don't make sense, since reserved info is stored separately
503 sub get_warehouse_report {
504 $main::lxdebug->enter_sub();
509 my $myconfig = \%main::myconfig;
510 my $form = $main::form;
512 my $all_units = AM->retrieve_units($myconfig, $form);
514 # connect to database
515 my $dbh = $form->get_standard_dbh($myconfig);
518 my (@filter_ary, @filter_vars, @wh_bin_filter_ary, @wh_bin_filter_vars);
520 delete $form->{include_empty_bins} unless ($form->{l_warehouse} || $form->{l_bin});
522 if ($filter{warehouse_id}) {
523 push @wh_bin_filter_ary, "w.id = ?";
524 push @wh_bin_filter_vars, $filter{warehouse_id};
527 if ($filter{bin_id}) {
528 push @wh_bin_filter_ary, "b.id = ?";
529 push @wh_bin_filter_vars, $filter{bin_id};
532 push @filter_ary, @wh_bin_filter_ary;
533 push @filter_vars, @wh_bin_filter_vars;
535 if ($filter{partnumber}) {
536 push @filter_ary, "p.partnumber ILIKE ?";
537 push @filter_vars, like($filter{partnumber});
540 if ($filter{classification_id}) {
541 push @filter_ary, "p.classification_id = ?";
542 push @filter_vars, $filter{classification_id};
545 if ($filter{description}) {
546 push @filter_ary, "p.description ILIKE ?";
547 push @filter_vars, like($filter{description});
550 if ($filter{partsid}) {
551 push @filter_ary, "p.id = ?";
552 push @filter_vars, $filter{partsid};
555 if ($filter{partsgroup_id}) {
556 push @filter_ary, "p.partsgroup_id = ?";
557 push @filter_vars, $filter{partsgroup_id};
560 if ($filter{chargenumber}) {
561 push @filter_ary, "i.chargenumber ILIKE ?";
562 push @filter_vars, like($filter{chargenumber});
565 if (trim($form->{bestbefore})) {
566 push @filter_ary, "?::DATE = i.bestbefore::DATE";
567 push @filter_vars, trim($form->{bestbefore});
570 if ($filter{classification_id}) {
571 push @filter_ary, "p.classification_id = ?";
572 push @filter_vars, $filter{classification_id};
576 push @filter_ary, "p.ean ILIKE ?";
577 push @filter_vars, like($filter{ean});
580 if (trim($filter{date})) {
581 push @filter_ary, "i.shippingdate <= ?";
582 push @filter_vars, trim($filter{date});
584 if (!$filter{include_invalid_warehouses}){
585 push @filter_ary, "NOT (w.invalid)";
588 # prepare qty comparison for later filtering
589 my ($f_qty_op, $f_qty, $f_qty_base_unit);
591 if ($filter{qty_op} && defined $filter{qty} && $filter{qty_unit} && $all_units->{$filter{qty_unit}}) {
592 $f_qty_op = $filter{qty_op};
593 $f_qty = $filter{qty} * $all_units->{$filter{qty_unit}}->{factor};
594 $f_qty_base_unit = $all_units->{$filter{qty_unit}}->{base_unit};
597 map { $_ = "(${_})"; } @filter_ary;
599 # if of a property number or description is requested,
600 # automatically check the matching id too.
601 map { $form->{"l_${_}id"} = "Y" if ($form->{"l_${_}"} || $form->{"l_${_}number"}); } qw(warehouse bin);
603 # make order, search in $filter and $form
604 my $sort_col = $form->{sort};
605 my $sort_order = $form->{order};
607 $sort_col = $filter{sort} unless $sort_col;
608 # falls $sort_col gar nicht in dem Bericht aufgenommen werden soll,
609 # führt ein entsprechenes order by $sort_col zu einem SQL-Fehler
610 # entsprechend parts_id als default lassen, wenn $sort_col UND l_$sort_col
611 # vorhanden sind (bpsw. l_partnumber = 'Y', für in Bericht aufnehmen).
612 # S.a. Bug 1597 jb 12.5.2011
613 $sort_col = "parts_id" unless ($sort_col && $form->{"l_$sort_col"});
614 $sort_order = $filter{order} unless $sort_order;
615 $sort_col =~ s/ASC|DESC//; # kill stuff left in from previous queries
616 my $orderby = $sort_col;
617 my $sort_spec = "${sort_col} " . ($sort_order ? " DESC" : " ASC");
619 my $where_clause = join " AND ", ("1=1", @filter_ary);
621 my %select_tokens = (
622 "parts_id" => "i.parts_id",
623 "qty" => "SUM(i.qty)",
624 "warehouseid" => "i.warehouse_id",
625 "partnumber" => "p.partnumber",
626 "partdescription" => "p.description",
627 "classification_id" => "p.classification_id",
628 "part_type" => "p.part_type",
629 "bin" => "b.description",
631 "chargenumber" => "i.chargenumber",
632 "bestbefore" => "i.bestbefore",
634 "chargeid" => "c.id",
635 "warehouse" => "w.description",
636 "partunit" => "p.unit",
637 "stock_value" => ($form->{stock_value_basis} // '') eq 'list_price' ? "p.listprice / COALESCE(pfac.factor, 1)" : "p.lastcost / COALESCE(pfac.factor, 1)",
638 "purchase_price" => "p.lastcost",
639 "list_price" => "p.listprice",
640 "price_factor" => ($form->{l_purchase_price} || $form->{l_list_price}) ? "pfac.description" : undef,
642 $form->{l_classification_id} = 'Y';
643 $form->{l_part_type} = 'Y';
644 $form->{l_price_factor} = 'Y' if $form->{l_purchase_price} || $form->{l_list_price};
646 my $select_clause = join ', ', map { +/^l_/; "$select_tokens{$'} AS $'" }
647 ( grep( { !/qty/ and !/^l_cvar/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
648 qw(l_parts_id l_qty l_partunit) );
650 my $group_clause = join ", ", map { +/^l_/; "$'" }
651 ( grep( { !/qty/ and !/^l_cvar/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
652 qw(l_parts_id l_partunit) );
654 my @join_values = ();
656 "stock_value" => "LEFT JOIN price_factors pfac ON (p.price_factor_id = pfac.id)",
658 $join_tokens{price_factor} = "LEFT JOIN price_factors pfac ON (p.price_factor_id = pfac.id)" if !$form->{l_stock_value};
660 my $joins = join ' ', grep { $_ } map { +/^l_/; $join_tokens{"$'"} }
661 ( grep( { !/qty/ and !/^l_cvar/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
662 qw(l_parts_id l_qty l_partunit) );
664 # add cvar for sorting
665 if (($form->{sort} // '') =~ /^cvar_/) {
666 my $sort_name = $form->{sort};
667 my $cvar_name = $sort_name;
668 $cvar_name =~ s/^cvar_//;
669 my $cvar_configs = CVar->get_configs('module' => 'IC');
670 my @allowed_cvar_names =
672 grep {$_->{type} =~ m/text|textfield|htmlfield/}
674 unless (any {$sort_name eq 'cvar_' . $_} @allowed_cvar_names) {
675 die "unsupported sort on cvar field";
678 $select_clause .= ", cvar_fields.$sort_name";
679 $group_clause .= ", cvar_fields.$sort_name";
682 SELECT text_value as $sort_name, trans_id
683 FROM custom_variable_configs cvar_cfg
684 LEFT JOIN custom_variables cvar
685 ON (cvar_cfg.module = 'IC' AND cvar_cfg.name = ?
686 AND cvar_cfg.id = cvar.config_id)
687 ) cvar_fields ON (cvar_fields.trans_id = p.id)
689 push @join_values, $cvar_name
691 @filter_vars = (@join_values, @filter_vars);
693 my ($cvar_where, @cvar_values) = CVar->build_filter_query(
695 trans_id_field => 'p.id',
701 $where_clause .= qq| AND ($cvar_where)|;
702 push @filter_vars, @cvar_values;
706 qq|SELECT * FROM ( SELECT $select_clause
708 LEFT JOIN parts p ON i.parts_id = p.id
709 LEFT JOIN bin b ON i.bin_id = b.id
710 LEFT JOIN warehouse w ON i.warehouse_id = w.id
713 GROUP BY $group_clause
714 ORDER BY $sort_spec ) AS lines WHERE qty<>0|;
716 if ($filter{limit}) {
717 $query .= " LIMIT ?";
718 push @filter_vars,$filter{limit};
720 if ($filter{offset}) {
721 $query .= " OFFSET ?";
722 push @filter_vars, $filter{offset};
724 my $sth = prepare_execute_query($form, $dbh, $query, @filter_vars );
726 my (%non_empty_bins, @all_fields, @contents);
728 while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
730 my $qty = $ref->{qty};
732 next unless ($qty != 0);
735 my $part_unit = $all_units->{$ref->{partunit}};
736 next if (!$part_unit || ($part_unit->{base_unit} ne $f_qty_base_unit));
737 $qty *= $part_unit->{factor};
738 next if (('=' eq $f_qty_op) && ($qty != $f_qty));
739 next if (('>=' eq $f_qty_op) && ($qty < $f_qty));
740 next if (('<=' eq $f_qty_op) && ($qty > $f_qty));
743 if ($form->{include_empty_bins}) {
744 $non_empty_bins{$ref->{binid}} = 1;
745 @all_fields = keys %{ $ref } unless (@all_fields);
748 $ref->{stock_value} = ($ref->{stock_value} || 0) * $ref->{qty};
750 push @contents, $ref;
755 if ($form->{include_empty_bins}) {
758 w.id AS warehouseid, w.description AS warehouse,
759 b.id AS binid, b.description AS bin
761 LEFT JOIN warehouse w ON (b.warehouse_id = w.id)|;
763 @filter_ary = @wh_bin_filter_ary;
764 @filter_vars = @wh_bin_filter_vars;
766 my @non_empty_bin_ids = keys %non_empty_bins;
767 if (@non_empty_bin_ids) {
768 push @filter_ary, qq|NOT b.id IN (| . join(', ', map { '?' } @non_empty_bin_ids) . qq|)|;
769 push @filter_vars, @non_empty_bin_ids;
772 $query .= qq| WHERE | . join(' AND ', map { "($_)" } @filter_ary) if (@filter_ary);
774 $sth = prepare_execute_query($form, $dbh, $query, @filter_vars);
776 while (my $ref = $sth->fetchrow_hashref()) {
777 map { $ref->{$_} ||= "" } @all_fields;
778 push @contents, $ref;
782 if (grep { $orderby eq $_ } qw(bin warehouse)) {
783 @contents = sort { ($a->{$orderby} cmp $b->{$orderby}) * (($form->{order}) ? 1 : -1) } @contents;
787 $main::lxdebug->leave_sub();
793 $main::lxdebug->enter_sub();
795 my ($self, $qty_op) = @_;
797 if (!$qty_op || ($qty_op eq "dontcare")) {
798 $main::lxdebug->leave_sub();
802 if ($qty_op eq "atleast") {
804 } elsif ($qty_op eq "atmost") {
810 $main::lxdebug->leave_sub();
815 sub retrieve_transfer_types {
816 $main::lxdebug->enter_sub();
819 my $direction = shift;
821 my $myconfig = \%main::myconfig;
822 my $form = $main::form;
824 my $dbh = $form->get_standard_dbh($myconfig);
826 my $types = selectall_hashref_query($form, $dbh, qq|SELECT * FROM transfer_type WHERE direction = ? ORDER BY sortkey|, $direction);
828 $main::lxdebug->leave_sub();
833 sub get_basic_bin_info {
834 $main::lxdebug->enter_sub();
839 Common::check_params(\%params, qw(id));
841 my $myconfig = \%main::myconfig;
842 my $form = $main::form;
844 my $dbh = $params{dbh} || $form->get_standard_dbh();
846 my @ids = 'ARRAY' eq ref $params{id} ? @{ $params{id} } : ($params{id});
849 qq|SELECT b.id AS bin_id, b.description AS bin_description,
850 w.id AS warehouse_id, w.description AS warehouse_description
852 LEFT JOIN warehouse w ON (b.warehouse_id = w.id)
853 WHERE b.id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
855 my $result = selectall_hashref_query($form, $dbh, $query, map { conv_i($_) } @ids);
857 if ('' eq ref $params{id}) {
858 $result = $result->[0] || { };
859 $main::lxdebug->leave_sub();
864 $main::lxdebug->leave_sub();
866 return map { $_->{bin_id} => $_ } @{ $result };
869 sub get_basic_warehouse_info {
870 $main::lxdebug->enter_sub();
875 Common::check_params(\%params, qw(id));
877 my $myconfig = \%main::myconfig;
878 my $form = $main::form;
880 my $dbh = $params{dbh} || $form->get_standard_dbh();
882 my @ids = 'ARRAY' eq ref $params{id} ? @{ $params{id} } : ($params{id});
885 qq|SELECT w.id AS warehouse_id, w.description AS warehouse_description
887 WHERE w.id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
889 my $result = selectall_hashref_query($form, $dbh, $query, map { conv_i($_) } @ids);
891 if ('' eq ref $params{id}) {
892 $result = $result->[0] || { };
893 $main::lxdebug->leave_sub();
898 $main::lxdebug->leave_sub();
900 return map { $_->{warehouse_id} => $_ } @{ $result };
903 # Eingabe: Teilenummer, Lagernummer (warehouse)
904 # Ausgabe: Die maximale Anzahl der Teile in diesem Lager
906 sub get_max_qty_parts {
907 $main::lxdebug->enter_sub();
912 Common::check_params(\%params, qw(parts_id warehouse_id)); #die brauchen wir
914 my $myconfig = \%main::myconfig;
915 my $form = $main::form;
917 my $dbh = $params{dbh} || $form->get_standard_dbh();
919 my $query = qq| SELECT SUM(qty), bin_id, chargenumber, bestbefore FROM inventory where parts_id = ? AND warehouse_id = ? GROUP BY bin_id, chargenumber, bestbefore|;
920 my $sth_QTY = prepare_execute_query($form, $dbh, $query, ,$params{parts_id}, $params{warehouse_id}); #info: aufruf an DBUtils.pm
923 my $max_qty_parts = 0; #Initialisierung mit 0
924 while (my $ref = $sth_QTY->fetchrow_hashref()) { # wir laufen über alle Haltbarkeiten, chargen und Lagerorte (s.a. SQL-Query oben)
925 $max_qty_parts += $ref->{sum};
928 $main::lxdebug->leave_sub();
930 return $max_qty_parts;
934 # Eingabe: Teilenummer, Lagernummer (warehouse)
935 # Ausgabe: Die Beschreibung der Ware bzw. Erzeugnis
937 sub get_part_description {
938 $main::lxdebug->enter_sub();
943 Common::check_params(\%params, qw(parts_id)); #die brauchen wir
945 my $myconfig = \%main::myconfig;
946 my $form = $main::form;
948 my $dbh = $params{dbh} || $form->get_standard_dbh();
950 my $query = qq| SELECT partnumber, description FROM parts where id = ? |;
952 my $sth = prepare_execute_query($form, $dbh, $query, ,$params{parts_id}); #info: aufruf zu DBUtils.pm
954 my $ref = $sth->fetchrow_hashref();
955 my $part_description = $ref->{partnumber} . " " . $ref->{description};
957 $main::lxdebug->leave_sub();
959 return $part_description;
962 # Eingabe: Teilenummer, Lagerplatz_Id (bin_id)
963 # Ausgabe: Die maximale Anzahl der Teile in diesem Lagerplatz
964 # Bzw. Fehler, falls Chargen oder bestbefore
965 # bei eingelagerten Teilen definiert sind.
967 sub get_max_qty_parts_bin {
968 $main::lxdebug->enter_sub();
973 Common::check_params(\%params, qw(parts_id bin_id)); #die brauchen wir
975 my $myconfig = \%main::myconfig;
976 my $form = $main::form;
978 my $dbh = $params{dbh} || $form->get_standard_dbh();
980 my $query = qq| SELECT SUM(qty), chargenumber, bestbefore FROM inventory where parts_id = ?
981 AND bin_id = ? GROUP BY chargenumber, bestbefore|;
983 my $sth_QTY = prepare_execute_query($form, $dbh, $query, ,$params{parts_id}, $params{bin_id}); #info: aufruf an DBUtils.pm
985 my $max_qty_parts = 0; #Initialisierung mit 0
986 # falls derselbe artikel mehrmals eingelagert ist
987 # chargennummer, muss entsprechend händisch agiert werden
990 while (my $ref = $sth_QTY->fetchrow_hashref()) { # wir laufen über alle Haltbarkeiten und Chargen(s.a. SQL-Query oben)
991 $max_qty_parts += $ref->{sum};
993 if (($ref->{chargenumber} || $ref->{bestbefore}) && $ref->{sum} != 0){
997 $main::lxdebug->leave_sub();
999 return ($max_qty_parts, $error);
1002 sub get_wh_and_bin_for_charge {
1003 $main::lxdebug->enter_sub();
1009 croak t8('Need charge number!') unless $params{chargenumber};
1011 my $inv_items = SL::DB::Manager::Inventory->get_all(where => [chargenumber => $params{chargenumber} ]);
1013 croak t8("Invalid charge number: #1", $params{chargenumber}) unless (ref @{$inv_items}[0] eq 'SL::DB::Inventory');
1014 # add all qty for one bin and add wh_id
1015 ($bin_qty{$_->bin_id}{qty}, $bin_qty{$_->bin_id}{wh}) = ($bin_qty{$_->bin_id}{qty} + $_->qty, $_->warehouse_id) for @{ $inv_items };
1017 while (my ($bin, $value) = each (%bin_qty)) {
1018 if ($value->{qty} > 0) {
1019 $main::lxdebug->leave_sub();
1020 return ($value->{qty}, $value->{wh}, $bin, $params{chargenumber});
1024 $main::lxdebug->leave_sub();
1033 SL::WH - Warehouse backend
1038 WH->transfer(\%params);
1042 Backend for kivitendo warehousing functions.
1046 =head2 transfer \%PARAMS, [ \%PARAMS, ... ]
1048 This is the main function to manipulate warehouse contents. A typical transfer
1049 is called like this:
1054 transfer_type => 'transfer',
1055 src_warehouse_id => 12,
1057 dst_warehouse_id => 25,
1061 It will generate an entry in inventory representing the transfer. Note that
1062 parts_id, qty, and transfer_type are mandatory. Depending on the transfer_type
1063 a destination or a src is mandatory.
1065 transfer accepts more than one transaction parameter, each being a hash ref. If
1066 more than one is supplied, it is guaranteed, that all are processed in the same
1069 It is possible to record stocktakings within this transaction as well.
1070 This is useful if the transfer is the result of stocktaking (see also
1071 C<SL::Controller::Inventory>). To do so the parameters C<record_stocktaking>,
1072 C<stocktaking_qty> and C<stocktaking_cutoff_date> hava to be given.
1073 If stocktaking should be saved, then the transfer quantity can be zero. In this
1074 case no entry in inventory will be made, but only the stocktaking entry.
1076 Here is a full list of parameters. All "_id" parameters except oe and
1077 orderitems can be called without id with RDB objects as well.
1083 The id of the article transferred. Does not check if the article is a service.
1088 Quantity of the transaction. Mandatory.
1092 Unit of the transaction. Optional.
1096 =item transfer_type_id
1098 The type of transaction. The first version is a string describing the
1099 transaction (the types 'transfer' 'in' 'out' and a few others are present on
1100 every system), the id is the hard id of a transfer_type from the database.
1102 Depending of the direction of the transfer_type, source and/or destination must
1105 One of transfer_type or transfer_type_id is mandatory.
1107 =item src_warehouse_id
1111 Warehouse and bin from which to transfer. Mandatory in transfer and out
1112 directions. Ignored in in directions.
1114 =item dst_warehouse_id
1118 Warehouse and bin to which to transfer. Mandatory in transfer and in
1119 directions. Ignored in out directions.
1123 If given, the transfer will transfer only articles with this chargenumber.
1128 Reference to an orderitem for which this transfer happened. Optional
1132 Reference to an order for which this transfer happened. Optional
1136 An optional comment.
1140 An expiration date. Note that this is not by default used by C<warehouse_report>.
1142 =item record_stocktaking
1144 A boolean flag to indicate that a stocktaking entry should be saved.
1146 =item stocktaking_qty
1148 The quantity for the stocktaking entry.
1150 =item stocktaking_cutoff_date
1152 The cutoff date for the stocktaking entry.
1156 =head2 create_assembly \%PARAMS, [ \%PARAMS, ... ]
1158 Creates an assembly if all defined items are available.
1160 Assembly item(s) will be stocked out and the assembly will be stocked in,
1161 taking into account the qty and units which can be defined for each
1162 assembly item separately.
1164 The calling params originate from C<transfer> but only parts_id with the
1165 attribute assembly are processed.
1167 The typical params would be:
1170 'login' => $::myconfig{login},
1171 'dst_warehouse_id' => $form->{warehouse_id},
1172 'dst_bin_id' => $form->{bin_id},
1173 'chargenumber' => $form->{chargenumber},
1174 'bestbefore' => $form->{bestbefore},
1175 'assembly_id' => $form->{parts_id},
1176 'qty' => $form->{qty},
1177 'comment' => $form->{comment}
1181 =head2 get_wh_and_bin_for_charge C<$params{chargenumber}>
1183 Gets the current qty from the inventory entries with the mandatory chargenumber: C<$params{chargenumber}>.
1184 Croaks if the chargenumber is missing or no entry currently exists.
1185 If there is one bin and warehouse with a positive qty, this fields are returned:
1186 C<qty> C<warehouse_id>, C<bin_id>, C<chargenumber>.
1187 Otherwise returns undef.
1190 =head3 Prerequisites
1192 All of these prerequisites have to be trueish, otherwise the function will exit
1193 unsuccessfully with a return value of undef.
1197 =item Mandantory params
1199 assembly_id, qty, login, dst_warehouse_id and dst_bin_id are mandatory.
1201 =item Subset named 'Assembly' of data set 'Part'
1203 assembly_id has to be an id in the table parts with the valid subset assembly.
1205 =item Assembly is composed of assembly item(s)
1207 There has to be at least one data set in the table assembly referenced to this assembly_id.
1209 =item Assembly can be disassembled
1211 Assemblies are like cakes. You cannot disassemble it. NEVER.
1212 But if your assembly is a mechanical cake you may unscrew it.
1213 Assemblies are created in one transaction therefore you can
1214 safely rely on the trans_id in inventory to disassemble the
1215 created assemblies (see action disassemble_assembly in wh.pl).
1217 =item The assembly item(s) have to be in the same warehouse
1219 inventory.warehouse_id equals dst_warehouse_id (client configurable).
1221 =item The assembly item(s) have to be in stock with the qty needed
1223 I can only make a cake by receipt if I have ALL ingredients and
1224 in the needed stock amount.
1225 The qty of stocked in assembly item(s) has to fit into the
1226 number of the qty of the assemblies, which are going to be created (client configurable).
1228 =item assembly item(s) with the parts set 'service' are ignored
1230 The subset 'Services' of part will not transferred for assembly item(s).
1234 Client configurable prerequisites can be changed with different
1235 prerequisites as described in client_config (s.a. next chapter).
1238 =head2 default creation of assembly
1240 The valid state of the assembly item(s) used for the assembly process are
1241 'out' for the general direction and 'used' as the specific reason.
1242 The valid state of the assembly is 'in' for the direction and 'assembled'
1243 as the specific reason.
1245 The method is transaction safe, in case of errors not a single entry will be made