+sub get_stock {
+  my ($self, %params) = @_;
+
+  return undef unless $self->id;
+
+  my $query = 'SELECT SUM(qty) FROM inventory WHERE parts_id = ?';
+  my @values = ($self->id);
+
+  if ( $params{bin_id} ) {
+    $query .= ' AND bin_id = ?';
+    push(@values, $params{bin_id});
+  }
+
+  if ( $params{warehouse_id} ) {
+    $query .= ' AND warehouse_id = ?';
+    push(@values, $params{warehouse_id});
+  }
+
+  if ( $params{shippingdate} ) {
+    die unless ref($params{shippingdate}) eq 'DateTime';
+    $query .= ' AND shippingdate <= ?';
+    push(@values, $params{shippingdate});
+  }
+
+  my ($stock) = selectrow_query($::form, $self->db->dbh, $query, @values);
+
+  return $stock || 0; # never return undef
+};
+
+
+# this is designed to ignore chargenumbers, expiration dates and just give a list of how much <-> where
+sub get_simple_stock {
+  my ($self, %params) = @_;
+
+  return [] unless $self->id;
+
+  my $query = <<'';
+    SELECT sum(qty), warehouse_id, bin_id FROM inventory WHERE parts_id = ?
+    GROUP BY warehouse_id, bin_id
+
+  my $stock_info = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, $self->id);
+  [ map { bless $_, 'SL::DB::Part::SimpleStock'} @$stock_info ];
+}
+# helper class to have bin/warehouse accessors in stock result
+{ package SL::DB::Part::SimpleStock;
+  sub warehouse { require SL::DB::Warehouse; SL::DB::Manager::Warehouse->find_by_or_create(id => $_[0]->{warehouse_id}) }
+  sub bin       { require SL::DB::Bin;       SL::DB::Manager::Bin      ->find_by_or_create(id => $_[0]->{bin_id}) }
+}
+
+sub get_simple_stock_sql {
+  my ($self, %params) = @_;
+
+  return [] unless $self->id;
+
+  my $query = <<SQL;
+     SELECT w.description                         AS warehouse_description,
+            b.description                         AS bin_description,
+            SUM(i.qty)                            AS qty,
+            SUM(i.qty * p.lastcost)               AS stock_value,
+            p.unit                                AS unit,
+            LEAD(w.description)           OVER pt AS wh_lead,            -- to detect warehouse changes for subtotals in template
+            SUM( SUM(i.qty) )             OVER pt AS run_qty,            -- running total of total qty
+            SUM( SUM(i.qty) )             OVER wh AS wh_run_qty,         -- running total of warehouse qty
+            SUM( SUM(i.qty * p.lastcost)) OVER pt AS run_stock_value,    -- running total of total stock_value
+            SUM( SUM(i.qty * p.lastcost)) OVER wh AS wh_run_stock_value  -- running total of warehouse stock_value
+       FROM inventory i
+            LEFT JOIN parts p     ON (p.id           = i.parts_id)
+            LEFT JOIN warehouse w ON (i.warehouse_id = w.id)
+            LEFT JOIN bin b       ON (i.bin_id       = b.id)
+      WHERE parts_id = ?
+   GROUP BY w.description, w.sortkey, b.description, p.unit, i.parts_id
+     HAVING SUM(qty) != 0
+     WINDOW pt AS (PARTITION BY i.parts_id    ORDER BY w.sortkey, b.description, p.unit),
+            wh AS (PARTITION by w.description ORDER BY w.sortkey, b.description, p.unit)
+   ORDER BY w.sortkey, b.description, p.unit
+SQL
+
+  my $stock_info = selectall_hashref_query($::form, $self->db->dbh, $query, $self->id);
+  return $stock_info;
+}
+
+sub get_mini_journal {
+  my ($self) = @_;
+
+  # inventory ids of the most recent 10 inventory trans_ids
+
+  # duplicate code copied from SL::Controller::Inventory mini_journal, except
+  # for the added filter on parts_id
+
+  my $parts_id = $self->id;
+  my $query = <<"SQL";
+with last_inventories as (
+   select id,
+          trans_id,
+          itime
+     from inventory
+    where parts_id = $parts_id
+ order by itime desc
+    limit 20
+),
+grouped_ids as (
+   select trans_id,
+          array_agg(id) as ids
+     from last_inventories
+ group by trans_id
+ order by max(itime)
+     desc limit 10
+)
+select unnest(ids)
+  from grouped_ids
+ limit 20  -- so the planner knows how many ids to expect, the cte is an optimisation fence
+SQL
+
+  my $objs  = SL::DB::Manager::Inventory->get_all(
+    query        => [ id => [ \"$query" ] ],                           # make emacs happy "
+    with_objects => [ 'parts', 'trans_type', 'bin', 'bin.warehouse' ], # prevent lazy loading in template
+    sort_by      => 'itime DESC',
+  );
+  # remember order of trans_ids from query, for ordering hash later
+  my @sorted_trans_ids = uniq map { $_->trans_id } @$objs;
+
+  # at most 2 of them belong to a transaction and the qty determines in or out.
+  my %transactions;
+  for (@$objs) {
+    $transactions{ $_->trans_id }{ $_->qty > 0 ? 'in' : 'out' } = $_;
+    $transactions{ $_->trans_id }{base} = $_;
+  }
+
+  # because the inventory transactions were built in a hash, we need to sort the
+  # hash by using the original sort order of the trans_ids
+  my @sorted = map { $transactions{$_} } @sorted_trans_ids;
+
+  return \@sorted;
+}
+
+sub clone_and_reset_deep {
+  my ($self) = @_;
+
+  my $clone = $self->clone_and_reset; # resets id and partnumber (primary key and unique constraint)
+  $clone->makemodels(   map { $_->clone_and_reset } @{$self->makemodels}   ) if @{$self->makemodels};
+  $clone->translations( map { $_->clone_and_reset } @{$self->translations} ) if @{$self->translations};
+
+  if ( $self->is_assortment ) {
+    # use clone rather than reset_and_clone because the unique constraint would also remove parts_id
+    $clone->assortment_items( map { $_->clone } @{$self->assortment_items} );
+    $_->assortment_id(undef) foreach @{ $clone->assortment_items }
+  };
+
+  if ( $self->is_assembly ) {
+    $clone->assemblies( map { $_->clone_and_reset } @{$self->assemblies});
+  };
+
+  if ( $self->prices ) {
+    $clone->prices( map { $_->clone } @{$self->prices}); # pricegroup_id gets reset here because it is part of a unique contraint
+    if ( $clone->prices ) {
+      foreach my $price ( @{$clone->prices} ) {
+        $price->id(undef);
+        $price->parts_id(undef);
+      };
+    };
+  };
+
+  return $clone;
+}
+
+sub item_diffs {
+  my ($self, $comparison_part) = @_;
+
+  die "item_diffs needs a part object" unless ref($comparison_part) eq 'SL::DB::Part';
+  die "part and comparison_part need to be of the same part_type" unless
+        ( $self->part_type eq 'assembly' or $self->part_type eq 'assortment' )
+    and ( $comparison_part->part_type eq 'assembly' or $comparison_part->part_type eq 'assortment' )
+    and $self->part_type eq $comparison_part->part_type;
+
+  # return [], [] if $self->items_checksum eq $comparison_part->items_checksum;
+  my @self_part_ids       = map { $_->parts_id } $self->items;
+  my @comparison_part_ids = map { $_->parts_id } $comparison_part->items;
+
+  my %orig       = map{ $_ => 1 } @self_part_ids;
+  my %comparison = map{ $_ => 1 } @comparison_part_ids;
+  my (@additions, @removals);
+  @additions = grep { !exists( $orig{$_}       ) } @comparison_part_ids if @comparison_part_ids;
+  @removals  = grep { !exists( $comparison{$_} ) } @self_part_ids       if @self_part_ids;
+
+  return \@additions, \@removals;
+};
+
+sub items_sellprice_sum {
+  my ($self, %params) = @_;
+
+  return unless $self->is_assortment or $self->is_assembly;
+  return unless $self->items;
+
+  if ($self->is_assembly) {
+    return sum map { $_->linetotal_sellprice          } @{$self->items};
+  } else {
+    return sum map { $_->linetotal_sellprice(%params) } grep { $_->charge } @{$self->items};
+  }
+}
+
+sub items_lastcost_sum {
+  my ($self) = @_;
+
+  return unless $self->is_assortment or $self->is_assembly;
+  return unless $self->items;
+  sum map { $_->linetotal_lastcost } @{$self->items};
+};
+
+sub items_weight_sum {
+  my ($self) = @_;
+
+  return unless $self->is_assembly;
+  return unless $self->items;
+  sum map { $_->linetotal_weight} @{$self->items};
+};
+