SL::ShopConnector::WooCommerce Kategorien per page
[kivitendo-erp.git] / SL / WH.pm
index 0072835..2899aaf 100644 (file)
--- a/SL/WH.pm
+++ b/SL/WH.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 #  Warehouse module
 
 package WH;
 
+use Carp qw(croak);
+
 use SL::AM;
 use SL::DBUtils;
+use SL::DB::Inventory;
 use SL::Form;
+use SL::Locale::String qw(t8);
 use SL::Util qw(trim);
 
 use warnings;
@@ -55,7 +60,6 @@ sub transfer {
   require SL::DB::TransferType;
   require SL::DB::Part;
   require SL::DB::Employee;
-  require SL::DB::Inventory;
 
   my $employee   = SL::DB::Manager::Employee->find_by(login => $::myconfig{login});
   my ($now)      = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT current_date|);
@@ -79,7 +83,8 @@ sub transfer {
   my $db = SL::DB::Inventory->new->db;
   $db->with_transaction(sub{
     while (my $transfer = shift @args) {
-      my ($trans_id) = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT nextval('id')|);
+      my $trans_id;
+      ($trans_id) = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT nextval('id')|) if $transfer->{qty};
 
       my $part          = $objectify->($transfer, 'parts',         'SL::DB::Part');
       my $unit          = $objectify->($transfer, 'unit',          'SL::DB::Unit',         name => $transfer->{unit});
@@ -97,13 +102,21 @@ sub transfer {
       $direction |= 1 if $src_bin;
       $direction |= 2 if $dst_bin;
 
-      my $transfer_type = $objectify->($transfer, 'transfer_type', 'SL::DB::TransferType', direction   => $directions[$direction],
-                                                                                           description => $transfer->{transfer_type});
+      my $transfer_type_id;
+      if ($transfer->{transfer_type_id}) {
+        $transfer_type_id = $transfer->{transfer_type_id};
+      } else {
+        my $transfer_type = $objectify->($transfer, 'transfer_type', 'SL::DB::TransferType', direction   => $directions[$direction],
+                                                                                             description => $transfer->{transfer_type});
+        $transfer_type_id = $transfer_type->id;
+      }
+
+      my $stocktaking_qty = $transfer->{stocktaking_qty};
 
       my %params = (
           part             => $part,
           employee         => $employee,
-          trans_type       => $transfer_type,
+          trans_type_id    => $transfer_type_id,
           project          => $project,
           trans_id         => $trans_id,
           shippingdate     => !$transfer->{shippingdate} || $transfer->{shippingdate} eq 'current_date'
@@ -112,13 +125,15 @@ sub transfer {
       );
 
       if ($unit) {
-        $qty = $unit->convert_to($qty, $part->unit_obj);
+        $qty             = $unit->convert_to($qty,             $part->unit_obj);
+        $stocktaking_qty = $unit->convert_to($stocktaking_qty, $part->unit_obj);
       }
 
       $params{chargenumber} ||= '';
 
-      if ($direction & 1) {
-        SL::DB::Inventory->new(
+      my @inventories;
+      if ($qty && $direction & 1) {
+        push @inventories, SL::DB::Inventory->new(
           %params,
           warehouse => $src_wh,
           bin       => $src_bin,
@@ -126,8 +141,8 @@ sub transfer {
         )->save;
       }
 
-      if ($direction & 2) {
-        SL::DB::Inventory->new(
+      if ($qty && $direction & 2) {
+        push @inventories, SL::DB::Inventory->new(
           %params,
           warehouse => $dst_wh->id,
           bin       => $dst_bin->id,
@@ -139,6 +154,30 @@ sub transfer {
         }
       }
 
+      # Record stocktaking if requested.
+      # This is only possible if transfer was a stock in or stock out,
+      # but not both (transfer).
+      if ($transfer->{record_stocktaking}) {
+        die 'Stocktaking can only be recorded for stock in or stock out, but not on a transfer.' if scalar @inventories > 1;
+
+        my $inventory_id;
+        $inventory_id = $inventories[0]->id if $inventories[0];
+
+        SL::DB::Stocktaking->new(
+          inventory_id => $inventory_id,
+          warehouse    => $src_wh  || $dst_wh,
+          bin          => $src_bin || $dst_bin,
+          parts_id     => $part->id,
+          employee_id  => $employee->id,
+          qty          => $stocktaking_qty,
+          comment      => $transfer->{comment},
+          cutoff_date  => $transfer->{stocktaking_cutoff_date},
+          chargenumber => $transfer->{chargenumber},
+          bestbefore   => $transfer->{bestbefore},
+        )->save;
+
+      }
+
       push @trans_ids, $trans_id;
     }
 
@@ -159,8 +198,6 @@ sub transfer_assembly {
   my %params   = @_;
   Common::check_params(\%params, qw(assembly_id dst_warehouse_id login qty unit dst_bin_id chargenumber bestbefore comment));
 
-#  my $maxcreate=WH->check_assembly_max_create(assembly_id =>$params{'assembly_id'}, dbh => $my_dbh);
-
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
   my $kannNichtFertigen ="";  # Falls leer dann erfolgreich
@@ -191,17 +228,20 @@ sub transfer_assembly {
 
     my $query = qq|SELECT assembly.parts_id, assembly.qty, parts.warehouse_id
                    FROM assembly INNER JOIN parts ON assembly.parts_id = parts.id
-                   WHERE assembly.id = ? AND (inventory_accno_id IS NOT NULL OR parts.assembly = TRUE)|;
+                   WHERE assembly.id = ? AND parts.part_type != 'service'|;
 
     my $sth_part_qty_assembly = prepare_execute_query($form, $dbh, $query, $params{assembly_id});
 
+    my @trans_ids;
+
     # Hier wird das prepared Statement für die Schleife über alle Lagerplätze vorbereitet
     my $transferPartSQL = qq|INSERT INTO inventory (parts_id, warehouse_id, bin_id, chargenumber, bestbefore, comment, employee_id, qty,
                              trans_id, trans_type_id, shippingdate)
-                             VALUES (?, ?, ?, ?, ?, ?, (SELECT id FROM employee WHERE login = ?), ?, nextval('id'),
+                             VALUES (?, ?, ?, ?, ?, ?, (SELECT id FROM employee WHERE login = ?), ?, ?,
                              (SELECT id FROM transfer_type WHERE direction = 'out' AND description = 'used'),
                              (SELECT current_date))|;
     my $sthTransferPartSQL   = prepare_query($form, $dbh, $transferPartSQL);
+    my $trans_id;
 
     # der return-string für die fehlermeldung inkl. welche waren zum fertigen noch fehlen
 
@@ -238,10 +278,11 @@ sub transfer_assembly {
         my $temppart_chargenumber = "";
         my $temppart_bestbefore   = localtime();
         my $temppart_qty          = $partsQTY * -1;
+        ($trans_id) = selectrow_query($form, $dbh, qq|SELECT nextval('id')| ) unless $trans_id;
 
         do_statement($form, $sthTransferPartSQL, $transferPartSQL, $currentPart_ID, $currentPart_WH_ID,
                        $temppart_bin_id, $temppart_chargenumber, $temppart_bestbefore, 'Verbraucht für ' .
-                       $self->get_part_description(parts_id => $params{assembly_id}), $params{login}, $temppart_qty);
+                       $self->get_part_description(parts_id => $params{assembly_id}), $params{login}, $temppart_qty, $trans_id);
         next;
       }
       # Überprüfen, ob diese Anzahl gefertigt werden kann
@@ -279,6 +320,7 @@ sub transfer_assembly {
         my $temppart_bestbefore   = conv_date($temphash_ref->{bestbefore});
         my $temppart_qty          = $temphash_ref->{sum};
 
+        ($trans_id) = selectrow_query($form, $dbh, qq|SELECT nextval('id')| ) unless $trans_id;
         if ($tmpPartsQTY > $temppart_qty) {  # wir haben noch mehr waren zum wegbuchen.
                                              # Wir buchen den kompletten Lagerplatzbestand und zählen die Hilfsvariable runter
           $tmpPartsQTY = $tmpPartsQTY - $temppart_qty;
@@ -286,9 +328,10 @@ sub transfer_assembly {
                                               # wenn * -1 als berechnung in der parameter-übergabe angegeben wird.
                                               # Dieser Wert IST und BLEIBT positiv!! Hilfe.
                                               # Liegt das daran, dass dieser Wert aus einem SQL-Statement stammt?
+          push @trans_ids, $trans_id;
           do_statement($form, $sthTransferPartSQL, $transferPartSQL, $currentPart_ID, $currentPart_WH_ID,
                        $temppart_bin_id, $temppart_chargenumber, $temppart_bestbefore, 'Verbraucht für ' .
-                       $self->get_part_description(parts_id => $params{assembly_id}), $params{login}, $temppart_qty);
+                       $self->get_part_description(parts_id => $params{assembly_id}), $params{login}, $temppart_qty, $trans_id);
 
           # hier ist noch ein fehler am besten mit definierten erzeugnissen debuggen 02/2009 jb
           # idee: ausbuch algorithmus mit rekursion lösen und an- und abschaltbar machen
@@ -298,7 +341,7 @@ sub transfer_assembly {
           $tmpPartsQTY *=-1;
           do_statement($form, $sthTransferPartSQL, $transferPartSQL, $currentPart_ID, $currentPart_WH_ID,
                        $temppart_bin_id, $temppart_chargenumber, $temppart_bestbefore, 'Verbraucht für ' .
-                       $self->get_part_description(parts_id => $params{assembly_id}), $params{login}, $tmpPartsQTY);
+                       $self->get_part_description(parts_id => $params{assembly_id}), $params{login}, $tmpPartsQTY, $trans_id);
           last; # beendet die schleife (springt zum letzten element)
         }
       }  # ende while SELECT SUM(qty), bin_id, chargenumber, bestbefore   FROM inventory  WHERE warehouse_id
@@ -308,22 +351,23 @@ sub transfer_assembly {
                                     # keine einzelteile definiert
         $kannNichtFertigen ="Für dieses Erzeugnis sind keine Einzelteile definiert.
                              Dementsprechend kann auch nichts hergestellt werden";
-   }
+    }
     # gibt die Fehlermeldung zurück. A.) Keine Teile definiert
     #                                B.) Artikel und Anzahl der fehlenden Teile/Dienstleistungen
-    if ($kannNichtFertigen) {
-      return 0;
-    }
+    die "<br><br>" . $kannNichtFertigen if ($kannNichtFertigen);
 
     # soweit alles gut. Jetzt noch die wirkliche Lagerbewegung für das Erzeugnis ausführen ...
+    ($trans_id) = selectrow_query($form, $dbh, qq|SELECT nextval('id')| ) unless $trans_id;
     my $transferAssemblySQL = qq|INSERT INTO inventory (parts_id, warehouse_id, bin_id, chargenumber, bestbefore,
                                                         comment, employee_id, qty, trans_id, trans_type_id, shippingdate)
-                                 VALUES (?, ?, ?, ?, ?, ?, (SELECT id FROM employee WHERE login = ?), ?, nextval('id'),
+                                 VALUES (?, ?, ?, ?, ?, ?, (SELECT id FROM employee WHERE login = ?), ?, ?,
                                  (SELECT id FROM transfer_type WHERE direction = 'in' AND description = 'assembled'),
                                  (select current_date))|;
     my $sthTransferAssemblySQL   = prepare_query($form, $dbh, $transferAssemblySQL);
     do_statement($form, $sthTransferAssemblySQL, $transferAssemblySQL, $params{assembly_id}, $params{dst_warehouse_id},
-                 $params{dst_bin_id}, $params{chargenumber}, conv_date($params{bestbefore}), $params{comment}, $params{login}, $params{qty});
+                 $params{dst_bin_id}, $params{chargenumber}, conv_date($params{bestbefore}), $params{comment}, $params{login}, $params{qty}, $trans_id);
+
+
     1;
   }) or do { return $kannNichtFertigen };
 
@@ -368,6 +412,11 @@ sub get_warehouse_journal {
     push @filter_vars, like($filter{description});
   }
 
+  if ($filter{classification_id}) {
+    push @filter_ary, "p.classification_id = ?";
+    push @filter_vars, $filter{classification_id};
+  }
+
   if ($filter{chargenumber}) {
     push @filter_ary, "i1.chargenumber ILIKE ?";
     push @filter_vars, like($filter{chargenumber});
@@ -414,10 +463,30 @@ sub get_warehouse_journal {
   my $sort_order = $form->{order};
 
   $sort_col      = $filter{sort}         unless $sort_col;
+  $sort_col      = 'shippingdate'        if     $sort_col eq 'date';
   $sort_order    = ($sort_col = 'shippingdate') unless $sort_col;
-  $sort_col      = 'shippingdate'               if     $sort_col eq 'date';
-  $sort_order    = $filter{order}        unless $sort_order;
-  my $sort_spec  = "${sort_col} " . ($sort_order ? " DESC" : " ASC");
+
+  my %orderspecs = (
+    'shippingdate'   => ['shippingdate', 'r_itime', 'r_parts_id'],
+    'bin_to'         => ['bin_to', 'r_itime', 'r_parts_id'],
+    'bin_from'       => ['bin_from', 'r_itime', 'r_parts_id'],
+    'warehouse_to'   => ['warehouse_to, r_itime, r_parts_id'],
+    'warehouse_from' => ['warehouse_from, r_itime, r_parts_id'],
+    'partnumber'     => ['partnumber'],
+    'partdescription'=> ['partdescription'],
+    'partunit'       => ['partunit, r_itime, r_parts_id'],
+    'qty'            => ['qty, r_itime, r_parts_id'],
+    'oe_id'          => ['oe_id'],
+    'comment'        => ['comment'],
+    'trans_type'     => ['trans_type'],
+    'employee'       => ['employee'],
+    'projectnumber'  => ['projectnumber'],
+    'chargenumber'   => ['chargenumber'],
+  );
+
+  $sort_order    = $filter{order}  unless $sort_order;
+  my $ASC = ($sort_order ? " DESC" : " ASC");
+  my $sort_spec  = join("$ASC , ", @{$orderspecs{$sort_col}}). " $ASC";
 
   my $where_clause = @filter_ary ? join(" AND ", @filter_ary) . " AND " : '';
 
@@ -426,6 +495,8 @@ sub get_warehouse_journal {
      "qty"                  => "ABS(SUM(i1.qty))",
      "partnumber"           => "p.partnumber",
      "partdescription"      => "p.description",
+     "classification_id"    => "p.classification_id",
+     "part_type"            => "p.part_type",
      "bindescription"       => "b.description",
      "chargenumber"         => "i1.chargenumber",
      "bestbefore"           => "i1.bestbefore",
@@ -438,6 +509,7 @@ sub get_warehouse_journal {
      "comment"              => "i1.comment",
      "trans_type"           => "tt.description",
      "trans_id"             => "i1.trans_id",
+     "id"                   => "i1.id",
      "oe_id"                => "COALESCE(i1.oe_id, i2.oe_id)",
      "invoice_id"           => "COALESCE(i1.invoice_id, i2.invoice_id)",
      "date"                 => "i1.shippingdate",
@@ -457,6 +529,10 @@ sub get_warehouse_journal {
      "warehouse_from"       => "'$filter{na}'",
      };
 
+  $form->{l_classification_id}  = 'Y';
+  $form->{l_trans_id}           = 'Y';
+  $form->{l_part_type}          = 'Y';
+  $form->{l_itime}              = 'Y';
   $form->{l_invoice_id} = $form->{l_oe_id} if $form->{l_oe_id};
 
   # build the select clauses.
@@ -467,31 +543,15 @@ sub get_warehouse_journal {
   }
 
   my $group_clause = join ", ", map { +/^l_/; "r_$'" }
-        ( grep( { !/qty$/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form), qw(l_parts_id l_partunit l_shippingdate) );
+        ( grep( { !/qty$/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form), qw(l_parts_id l_partunit l_shippingdate l_itime) );
 
   $where_clause = defined($where_clause) ? $where_clause : '';
 
   my $query =
-  qq|SELECT DISTINCT $select{trans}
-    FROM inventory i1
-    LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id
-    LEFT JOIN parts p ON i1.parts_id = p.id
-    LEFT JOIN bin b1 ON i1.bin_id = b1.id
-    LEFT JOIN bin b2 ON i2.bin_id = b2.id
-    LEFT JOIN warehouse w1 ON i1.warehouse_id = w1.id
-    LEFT JOIN warehouse w2 ON i2.warehouse_id = w2.id
-    LEFT JOIN transfer_type tt ON i1.trans_type_id = tt.id
-    LEFT JOIN project pr ON i1.project_id = pr.id
-    LEFT JOIN employee e ON i1.employee_id = e.id
-    WHERE $where_clause i2.qty = -i1.qty AND i2.qty > 0 AND
-          i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) = 2 )
-    GROUP BY $group_clause
-
-    UNION
-
+  qq|SELECT * FROM (
     SELECT DISTINCT $select{out}
     FROM inventory i1
-    LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id
+    LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id AND i1.id = i2.id
     LEFT JOIN parts p ON i1.parts_id = p.id
     LEFT JOIN bin b1 ON i1.bin_id = b1.id
     LEFT JOIN bin b2 ON i2.bin_id = b2.id
@@ -500,15 +560,15 @@ sub get_warehouse_journal {
     LEFT JOIN transfer_type tt ON i1.trans_type_id = tt.id
     LEFT JOIN project pr ON i1.project_id = pr.id
     LEFT JOIN employee e ON i1.employee_id = e.id
-    WHERE $where_clause i1.qty < 0 AND
-          i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) = 1 )
+    WHERE $where_clause i1.qty != 0 AND tt.direction = 'out' AND
+          i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) >= 1 )
     GROUP BY $group_clause
 
     UNION
 
     SELECT DISTINCT $select{in}
     FROM inventory i1
-    LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id
+    LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id AND i1.id = i2.id
     LEFT JOIN parts p ON i1.parts_id = p.id
     LEFT JOIN bin b1 ON i1.bin_id = b1.id
     LEFT JOIN bin b2 ON i2.bin_id = b2.id
@@ -517,29 +577,27 @@ sub get_warehouse_journal {
     LEFT JOIN transfer_type tt ON i1.trans_type_id = tt.id
     LEFT JOIN project pr ON i1.project_id = pr.id
     LEFT JOIN employee e ON i1.employee_id = e.id
-    WHERE $where_clause i1.qty > 0 AND
-          i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) = 1 )
+    WHERE $where_clause i1.qty != 0 AND tt.direction = 'in' AND
+          i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) >= 1 )
     GROUP BY $group_clause
-    ORDER BY r_${sort_spec}|;
+    ORDER BY r_${sort_spec}) AS lines WHERE r_qty != 0|;
 
-  my $sth = prepare_execute_query($form, $dbh, $query, @filter_vars, @filter_vars, @filter_vars);
+  my @all_vars = (@filter_vars,@filter_vars);
+
+  if ($filter{limit}) {
+    $query .= " LIMIT ?";
+    push @all_vars,$filter{limit};
+  }
+  if ($filter{offset}) {
+    $query .= " OFFSET ?";
+    push @all_vars, $filter{offset};
+  }
+
+  my $sth = prepare_execute_query($form, $dbh, $query, @all_vars);
 
   my ($h_oe_id, $q_oe_id);
   if ($form->{l_oe_id}) {
     $q_oe_id = <<SQL;
-      SELECT oe.id AS id,
-        CASE WHEN oe.quotation THEN oe.quonumber ELSE oe.ordnumber END AS number,
-        CASE
-          WHEN oe.customer_id IS NOT NULL AND     COALESCE(oe.quotation, FALSE) THEN 'sales_quotation'
-          WHEN oe.customer_id IS NOT NULL AND NOT COALESCE(oe.quotation, FALSE) THEN 'sales_order'
-          WHEN oe.customer_id IS     NULL AND     COALESCE(oe.quotation, FALSE) THEN 'request_quotation'
-          ELSE                                                                       'purchase_order'
-        END AS type
-      FROM oe
-      WHERE oe.id = ?
-
-      UNION
-
       SELECT dord.id AS id, dord.donumber AS number,
         CASE
           WHEN dord.customer_id IS NULL THEN 'purchase_delivery_order'
@@ -550,18 +608,6 @@ sub get_warehouse_journal {
 
       UNION
 
-      SELECT ar.id AS id, ar.invnumber AS number, 'sales_invoice' AS type
-      FROM ar
-      WHERE ar.id = ?
-
-      UNION
-
-      SELECT ap.id AS id, ap.invnumber AS number, 'purchase_invoice' AS type
-      FROM ap
-      WHERE ap.id = ?
-
-      UNION
-
       SELECT ar.id AS id, ar.invnumber AS number, 'sales_invoice' AS type
       FROM ar
       WHERE ar.id = (SELECT trans_id FROM invoice WHERE id = ?)
@@ -592,8 +638,7 @@ SQL
     }
 
     if ($h_oe_id && ($ref->{oe_id} || $ref->{invoice_id})) {
-      my $id = $ref->{oe_id} ? $ref->{oe_id} : $ref->{invoice_id};
-      do_statement($form, $h_oe_id, $q_oe_id, ($id) x 6);
+      do_statement($form, $h_oe_id, $q_oe_id, $ref->{oe_id}, ($ref->{invoice_id}) x 2);
       $ref->{oe_id_info} = $h_oe_id->fetchrow_hashref() || {};
     }
 
@@ -614,6 +659,7 @@ SQL
 #  - warehouse_id - will return matches with this warehouse_id only
 #  - partnumber   - will return only matches where the given string is a substring of the partnumber
 #  - partsid      - will return matches with this parts_id only
+#  - classification_id - will return matches with this parts with this classification only
 #  - description  - will return only matches where the given string is a substring of the description
 #  - chargenumber - will return only matches where the given string is a substring of the chargenumber
 #  - bestbefore   - will return only matches with this bestbefore date
@@ -665,6 +711,11 @@ sub get_warehouse_report {
     push @filter_vars, like($filter{partnumber});
   }
 
+  if ($filter{classification_id}) {
+    push @filter_ary, "p.classification_id = ?";
+    push @filter_vars, $filter{classification_id};
+  }
+
   if ($filter{description}) {
     push @filter_ary,  "p.description ILIKE ?";
     push @filter_vars, like($filter{description});
@@ -685,6 +736,11 @@ sub get_warehouse_report {
     push @filter_vars, trim($form->{bestbefore});
   }
 
+  if ($filter{classification_id}) {
+    push @filter_ary, "p.classification_id = ?";
+    push @filter_vars, $filter{classification_id};
+  }
+
   if ($filter{ean}) {
     push @filter_ary,  "p.ean ILIKE ?";
     push @filter_vars, like($filter{ean});
@@ -737,6 +793,8 @@ sub get_warehouse_report {
      "warehouseid"          => "i.warehouse_id",
      "partnumber"           => "p.partnumber",
      "partdescription"      => "p.description",
+     "classification_id"    => "p.classification_id",
+     "part_type"            => "p.part_type",
      "bindescription"       => "b.description",
      "binid"                => "b.id",
      "chargenumber"         => "i.chargenumber",
@@ -745,8 +803,13 @@ sub get_warehouse_report {
      "chargeid"             => "c.id",
      "warehousedescription" => "w.description",
      "partunit"             => "p.unit",
-     "stock_value"          => "p.lastcost / COALESCE(pfac.factor, 1)",
+     "stock_value"          => ($form->{stock_value_basis} // '') eq 'list_price' ? "p.listprice / COALESCE(pfac.factor, 1)" : "p.lastcost / COALESCE(pfac.factor, 1)",
+     "purchase_price"       => "p.lastcost",
+     "list_price"           => "p.listprice",
   );
+  $form->{l_classification_id}  = 'Y';
+  $form->{l_part_type}          = 'Y';
+
   my $select_clause = join ', ', map { +/^l_/; "$select_tokens{$'} AS $'" }
         ( grep( { !/qty/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
           qw(l_parts_id l_qty l_partunit) );
@@ -764,7 +827,7 @@ sub get_warehouse_report {
           qw(l_parts_id l_qty l_partunit) );
 
   my $query =
-    qq|SELECT $select_clause
+    qq|SELECT * FROM ( SELECT $select_clause
       FROM inventory i
       LEFT JOIN parts     p ON i.parts_id     = p.id
       LEFT JOIN bin       b ON i.bin_id       = b.id
@@ -772,9 +835,17 @@ sub get_warehouse_report {
       $joins
       WHERE $where_clause
       GROUP BY $group_clause
-      ORDER BY $sort_spec|;
+      ORDER BY $sort_spec ) AS lines WHERE qty<>0|;
 
-  my $sth = prepare_execute_query($form, $dbh, $query, @filter_vars);
+  if ($filter{limit}) {
+    $query .= " LIMIT ?";
+    push @filter_vars,$filter{limit};
+  }
+  if ($filter{offset}) {
+    $query .= " OFFSET ?";
+    push @filter_vars, $filter{offset};
+  }
+  my $sth = prepare_execute_query($form, $dbh, $query, @filter_vars );
 
   my (%non_empty_bins, @all_fields, @contents);
 
@@ -1052,6 +1123,31 @@ $main::lxdebug->enter_sub();
   return ($max_qty_parts, $error);
 }
 
+sub get_wh_and_bin_for_charge {
+  $main::lxdebug->enter_sub();
+
+  my $self     = shift;
+  my %params   = @_;
+  my %bin_qty;
+
+  croak t8('Need charge number!') unless $params{chargenumber};
+
+  my $inv_items = SL::DB::Manager::Inventory->get_all(where => [chargenumber => $params{chargenumber} ]);
+
+  croak t8("Invalid charge number: #1", $params{chargenumber}) unless (ref @{$inv_items}[0] eq 'SL::DB::Inventory');
+  # add all qty for one bin and add wh_id
+  ($bin_qty{$_->bin_id}{qty}, $bin_qty{$_->bin_id}{wh}) = ($bin_qty{$_->bin_id}{qty} + $_->qty, $_->warehouse_id) for @{ $inv_items };
+
+  while (my ($bin, $value) = each (%bin_qty)) {
+    if ($value->{qty} > 0) {
+      $main::lxdebug->leave_sub();
+      return ($value->{qty}, $value->{wh}, $bin, $params{chargenumber});
+    }
+  }
+
+  $main::lxdebug->leave_sub();
+  return undef;
+}
 1;
 
 __END__
@@ -1094,6 +1190,13 @@ transfer accepts more than one transaction parameter, each being a hash ref. If
 more than one is supplied, it is guaranteed, that all are processed in the same
 transaction.
 
+It is possible to record stocktakings within this transaction as well.
+This is useful if the transfer is the result of stocktaking (see also
+C<SL::Controller::Inventory>). To do so the parameters C<record_stocktaking>,
+C<stocktaking_qty> and C<stocktaking_cutoff_date> hava to be given.
+If stocktaking should be saved, then the transfer quantity can be zero. In this
+case no entry in inventory will be made, but only the stocktaking entry.
+
 Here is a full list of parameters. All "_id" parameters except oe and
 orderitems can be called without id with RDB objects as well.
 
@@ -1160,6 +1263,18 @@ An optional comment.
 
 An expiration date. Note that this is not by default used by C<warehouse_report>.
 
+=item record_stocktaking
+
+A boolean flag to indicate that a stocktaking entry should be saved.
+
+=item stocktaking_qty
+
+The quantity for the stocktaking entry.
+
+=item stocktaking_cutoff_date
+
+The cutoff date for the stocktaking entry.
+
 =back
 
 =head2 create_assembly \%PARAMS, [ \%PARAMS, ... ]
@@ -1168,7 +1283,7 @@ Creates an assembly if all defined items are available.
 
 Assembly item(s) will be stocked out and the assembly will be stocked in,
 taking into account the qty and units which can be defined for each
-assembly item seperately.
+assembly item separately.
 
 The calling params originate from C<transfer> but only parts_id with the
 attribute assembly are processed.
@@ -1186,6 +1301,16 @@ The typical params would be:
     'comment'          => $form->{comment}
   );
 
+
+=head2 get_wh_and_bin_for_charge C<$params{chargenumber}>
+
+Gets the current qty from the inventory entries with the mandatory chargenumber: C<$params{chargenumber}>.
+Croaks if the chargenumber is missing or no entry currently exists.
+If there is one bin and warehouse with a positive qty, this fields are returned:
+C<qty> C<warehouse_id>, C<bin_id>, C<chargenumber>.
+Otherwise returns undef.
+
+
 =head3 Prerequisites
 
 All of these prerequisites have to be trueish, otherwise the function will exit
@@ -1205,10 +1330,13 @@ unsuccessfully with a return value of undef.
 
   There has to be at least one data set in the table assembly referenced to this assembly_id.
 
-=item Assembly cannot be destroyed or disassembled
+=item Assembly can be disassembled
 
   Assemblies are like cakes. You cannot disassemble it. NEVER.
-  No negative nor zero qty's are valid inputs.
+  But if your assembly is a mechanical cake you may unscrew it.
+  Assemblies are created in one transaction therefore you can
+  safely rely on the trans_id in inventory to disassemble the
+  created assemblies (see action disassemble_assembly in wh.pl).
 
 =item The assembly item(s) have to be in the same warehouse
 
@@ -1241,7 +1369,7 @@ as the specific reason.
 The method is transaction safe, in case of errors not a single entry will be made
 in inventory.
 
-Two prerequisites can be changed with this global parameters
+Two prerequisites can be changed with these global parameters
 
 =over 2