SL::DB::Part - Zugriff auf items und Summen überarbeitet
[kivitendo-erp.git] / SL / DB / Part.pm
index 5c795f1..5ea3b26 100644 (file)
@@ -4,21 +4,25 @@ use strict;
 
 use Carp;
 use List::MoreUtils qw(any);
 
 use Carp;
 use List::MoreUtils qw(any);
+use Rose::DB::Object::Helpers qw(as_tree);
 
 use SL::DBUtils;
 use SL::DB::MetaSetup::Part;
 use SL::DB::Manager::Part;
 use SL::DB::Chart;
 
 use SL::DBUtils;
 use SL::DB::MetaSetup::Part;
 use SL::DB::Manager::Part;
 use SL::DB::Chart;
+use SL::DB::Helper::AttrHTML;
 use SL::DB::Helper::TransNumberGenerator;
 use SL::DB::Helper::CustomVariables (
   module      => 'IC',
   cvars_alias => 1,
 );
 use SL::DB::Helper::TransNumberGenerator;
 use SL::DB::Helper::CustomVariables (
   module      => 'IC',
   cvars_alias => 1,
 );
+use List::Util qw(sum);
 
 __PACKAGE__->meta->add_relationships(
   assemblies                     => {
     type         => 'one to many',
     class        => 'SL::DB::Assembly',
 
 __PACKAGE__->meta->add_relationships(
   assemblies                     => {
     type         => 'one to many',
     class        => 'SL::DB::Assembly',
+    manager_args => { sort_by => 'position, oid' },
     column_map   => { id => 'id' },
   },
   prices         => {
     column_map   => { id => 'id' },
   },
   prices         => {
@@ -29,6 +33,7 @@ __PACKAGE__->meta->add_relationships(
   makemodels     => {
     type         => 'one to many',
     class        => 'SL::DB::MakeModel',
   makemodels     => {
     type         => 'one to many',
     class        => 'SL::DB::MakeModel',
+    manager_args => { sort_by => 'sortorder' },
     column_map   => { id => 'parts_id' },
   },
   translations   => {
     column_map   => { id => 'parts_id' },
   },
   translations   => {
@@ -36,10 +41,24 @@ __PACKAGE__->meta->add_relationships(
     class        => 'SL::DB::Translation',
     column_map   => { id => 'parts_id' },
   },
     class        => 'SL::DB::Translation',
     column_map   => { id => 'parts_id' },
   },
+  assortment_items => {
+    type         => 'one to many',
+    class        => 'SL::DB::AssortmentItem',
+    column_map   => { id => 'assortment_id' },
+  },
+  history_entries   => {
+    type            => 'one to many',
+    class           => 'SL::DB::History',
+    column_map      => { id => 'trans_id' },
+    query_args      => [ what_done => 'part' ],
+    manager_args    => { sort_by => 'itime' },
+  },
 );
 
 __PACKAGE__->meta->initialize;
 
 );
 
 __PACKAGE__->meta->initialize;
 
+__PACKAGE__->attr_html('notes');
+
 __PACKAGE__->before_save('_before_save_set_partnumber');
 
 sub _before_save_set_partnumber {
 __PACKAGE__->before_save('_before_save_set_partnumber');
 
 sub _before_save_set_partnumber {
@@ -49,55 +68,116 @@ sub _before_save_set_partnumber {
   return 1;
 }
 
   return 1;
 }
 
+sub items {
+  my ($self) = @_;
+
+  if ( $self->part_type eq 'assembly' ) {
+    return $self->assemblies;
+  } elsif ( $self->part_type eq 'assortment' ) {
+    return $self->assortment_items;
+  } else {
+    return undef;
+  }
+}
+
+sub items_checksum {
+  my ($self) = @_;
+
+  # for detecting if the items of an (orphaned) assembly or assortment have
+  # changed when saving
+
+  return join(' ', sort map { $_->part->id } @{$self->items});
+};
+
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+  push @errors, $::locale->text('The partnumber is missing.')     if $self->id and !$self->partnumber;
+  push @errors, $::locale->text('The unit is missing.')           unless $self->unit;
+  push @errors, $::locale->text('The buchungsgruppe is missing.') unless $self->buchungsgruppen_id or $self->buchungsgruppe;
+
+  unless ( $self->id ) {
+    push @errors, $::locale->text('The partnumber already exists.') if SL::DB::Manager::Part->get_all_count(where => [ partnumber => $self->partnumber ]);
+  };
+
+  if ($self->is_assortment && $self->orphaned && scalar @{$self->assortment_items} == 0) {
+    # when assortment isn't orphaned form doesn't contain any items
+    push @errors, $::locale->text('The assortment doesn\'t have any items.');
+  }
+
+  if ($self->is_assembly && scalar @{$self->assemblies} == 0) {
+    push @errors, $::locale->text('The assembly doesn\'t have any items.');
+  }
+
+  return @errors;
+}
+
 sub is_type {
   my $self = shift;
   my $type  = lc(shift || '');
 sub is_type {
   my $self = shift;
   my $type  = lc(shift || '');
-  die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
+  die 'invalid type' unless $type =~ /^(?:part|service|assembly|assortment)$/;
 
   return $self->type eq $type ? 1 : 0;
 }
 
 
   return $self->type eq $type ? 1 : 0;
 }
 
-sub is_part     { $_[0]->is_type('part') }
-sub is_assembly { $_[0]->is_type('assembly') }
-sub is_service  { $_[0]->is_type('service') }
+sub is_part       { $_[0]->part_type eq 'part'       }
+sub is_assembly   { $_[0]->part_type eq 'assembly'   }
+sub is_service    { $_[0]->part_type eq 'service'    }
+sub is_assortment { $_[0]->part_type eq 'assortment' }
 
 sub type {
 
 sub type {
-  my ($self, $type) = @_;
-  if (@_ > 1) {
-    die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
-    $self->assembly(          $type eq 'assembly' ? 1 : 0);
-    $self->inventory_accno_id($type ne 'service'  ? 1 : undef);
-  }
-
-  return 'assembly' if $self->assembly;
-  return 'part'     if $self->inventory_accno_id;
-  return 'service';
+  return $_[0]->part_type;
+  # my ($self, $type) = @_;
+  # if (@_ > 1) {
+  #   die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
+  #   $self->assembly(          $type eq 'assembly' ? 1 : 0);
+  #   $self->inventory_accno_id($type ne 'service'  ? 1 : undef);
+  # }
+
+  # return 'assembly' if $self->assembly;
+  # return 'part'     if $self->inventory_accno_id;
+  # return 'service';
 }
 
 sub new_part {
   my ($class, %params) = @_;
 }
 
 sub new_part {
   my ($class, %params) = @_;
-  $class->new(%params, type => 'part');
+  $class->new(%params, part_type => 'part');
 }
 
 sub new_assembly {
   my ($class, %params) = @_;
 }
 
 sub new_assembly {
   my ($class, %params) = @_;
-  $class->new(%params, type => 'assembly');
+  $class->new(%params, part_type => 'assembly');
 }
 
 sub new_service {
   my ($class, %params) = @_;
 }
 
 sub new_service {
   my ($class, %params) = @_;
-  $class->new(%params, type => 'service');
+  $class->new(%params, part_type => 'service');
+}
+
+sub new_assortment {
+  my ($class, %params) = @_;
+  $class->new(%params, part_type => 'assortment');
 }
 
 }
 
+sub last_modification {
+  my ($self) = @_;
+  return $self->mtime or $self->itime;
+};
+
 sub orphaned {
   my ($self) = @_;
   die 'not an accessor' if @_ > 1;
 
 sub orphaned {
   my ($self) = @_;
   die 'not an accessor' if @_ > 1;
 
+  return 1 unless $self->id;
+
   my @relations = qw(
     SL::DB::InvoiceItem
     SL::DB::OrderItem
   my @relations = qw(
     SL::DB::InvoiceItem
     SL::DB::OrderItem
+    SL::DB::DeliveryOrderItem
     SL::DB::Inventory
     SL::DB::Inventory
-    SL::DB::RMAItem
+    SL::DB::Assembly
+    SL::DB::AssortmentItem
   );
 
   for my $class (@relations) {
   );
 
   for my $class (@relations) {
@@ -141,21 +221,19 @@ sub get_taxkey {
   my $date     = $params{date} || DateTime->today_local;
   my $is_sales = !!$params{is_sales};
   my $taxzone  = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
   my $date     = $params{date} || DateTime->today_local;
   my $is_sales = !!$params{is_sales};
   my $taxzone  = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
+  my $tk_info  = $::request->cache('get_taxkey');
 
 
-  $self->{__partpriv_taxkey_information} ||= { };
-  my $tk_info = $self->{__partpriv_taxkey_information};
-
-  $tk_info->{$taxzone}              ||= { };
-  $tk_info->{$taxzone}->{$is_sales} ||= { };
+  $tk_info->{$self->id}                                      //= {};
+  $tk_info->{$self->id}->{$taxzone}                          //= { };
+  my $cache = $tk_info->{$self->id}->{$taxzone}->{$is_sales} //= { };
 
 
-  if (!exists $tk_info->{$taxzone}->{$is_sales}->{$date}) {
-    $tk_info->{$taxzone}->{$is_sales}->{$date} =
+  if (!exists $cache->{$date}) {
+    $cache->{$date} =
       $self->get_chart(type => $is_sales ? 'income' : 'expense', taxzone => $taxzone)
       $self->get_chart(type => $is_sales ? 'income' : 'expense', taxzone => $taxzone)
-      ->load
       ->get_active_taxkey($date);
   }
 
       ->get_active_taxkey($date);
   }
 
-  return $tk_info->{$taxzone}->{$is_sales}->{$date};
+  return $cache->{$date};
 }
 
 sub get_chart {
 }
 
 sub get_chart {
@@ -164,22 +242,130 @@ sub get_chart {
   my $type    = (any { $_ eq $params{type} } qw(income expense inventory)) ? $params{type} : croak("Invalid 'type' parameter '$params{type}'");
   my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
 
   my $type    = (any { $_ eq $params{type} } qw(income expense inventory)) ? $params{type} : croak("Invalid 'type' parameter '$params{type}'");
   my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
 
-  $self->{__partpriv_get_chart_id} ||= { };
-  my $charts = $self->{__partpriv_get_chart_id};
+  my $charts     = $::request->cache('get_chart_id/by_part_id_and_taxzone')->{$self->id} //= {};
+  my $all_charts = $::request->cache('get_chart_id/by_id');
 
   $charts->{$taxzone} ||= { };
 
   if (!exists $charts->{$taxzone}->{$type}) {
 
   $charts->{$taxzone} ||= { };
 
   if (!exists $charts->{$taxzone}->{$type}) {
-    my $bugru    = $self->buchungsgruppe;
-    my $chart_id = ($type eq 'inventory') ? ($self->inventory_accno_id ? $bugru->inventory_accno_id : undef)
-                 :                          $bugru->call_sub("${type}_accno_id_${taxzone}");
-
-    $charts->{$taxzone}->{$type} = $chart_id ? SL::DB::Chart->new(id => $chart_id)->load : undef;
+    require SL::DB::Buchungsgruppe;
+    my $bugru    = SL::DB::Buchungsgruppe->load_cached($self->buchungsgruppen_id);
+    my $chart_id = ($type eq 'inventory') ? ($self->is_part ? $bugru->inventory_accno_id : undef)
+                 :                          $bugru->call_sub("${type}_accno_id", $taxzone);
+
+    if ($chart_id) {
+      my $chart                    = $all_charts->{$chart_id} // SL::DB::Chart->load_cached($chart_id)->load;
+      $all_charts->{$chart_id}     = $chart;
+      $charts->{$taxzone}->{$type} = $chart;
+    }
   }
 
   return $charts->{$taxzone}->{$type};
 }
 
   }
 
   return $charts->{$taxzone}->{$type};
 }
 
+# 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 displayable_name {
+  join ' ', grep $_, map $_[0]->$_, qw(partnumber description);
+}
+
+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 assortment_lastcost_sum {
+  my ($self) = @_;
+
+  return unless $self->is_assortment;
+  sum map { $_->linetotal_lastcost } @{$self->assortment_items};
+};
+
 1;
 
 __END__
 1;
 
 __END__
@@ -210,13 +396,15 @@ flavours called:
 
 =item Assembly - a collection of both parts and services
 
 
 =item Assembly - a collection of both parts and services
 
+=item Assortment - a collection of items (parts or assemblies)
+
 =back
 
 These types are sadly represented by data inside the class and cannot be
 migrated into a flag. To work around this, each C<Part> object knows what type
 =back
 
 These types are sadly represented by data inside the class and cannot be
 migrated into a flag. To work around this, each C<Part> object knows what type
-it currently is. Since the type ist data driven, there ist no explicit setting
+it currently is. Since the type is data driven, there ist no explicit setting
 method for it, but you can construct them explicitly with C<new_part>,
 method for it, but you can construct them explicitly with C<new_part>,
-C<new_service>, and C<new_assembly>. A Buchungsgruppe should be supplied in this
+C<new_service>, C<new_assembly> and C<new_assortment>. A Buchungsgruppe should be supplied in this
 case, but it will use the default Buchungsgruppe if you don't.
 
 Matching these there are assorted helper methods dealing with types,
 case, but it will use the default Buchungsgruppe if you don't.
 
 Matching these there are assorted helper methods dealing with types,
@@ -234,7 +422,7 @@ L</is_type> and others.
 =item C<new_assembly %PARAMS>
 
 Will set the appropriate data fields so that the resulting instance will be of
 =item C<new_assembly %PARAMS>
 
 Will set the appropriate data fields so that the resulting instance will be of
-tthe requested type. Since part of the distinction are accounting targets,
+the requested type. Since accounting targets are part of the distinction,
 providing a C<Buchungsgruppe> is recommended. If none is given the constructor
 will load a default one and set the accounting targets from it.
 
 providing a C<Buchungsgruppe> is recommended. If none is given the constructor
 will load a default one and set the accounting targets from it.
 
@@ -289,7 +477,7 @@ This function looks up the income (for trueish values of
 C<$params{is_sales}>) or expense (for falsish values of
 C<$params{is_sales}>) account for the current part. It uses the part's
 associated buchungsgruppe and uses the fields belonging to the tax
 C<$params{is_sales}>) or expense (for falsish values of
 C<$params{is_sales}>) account for the current part. It uses the part's
 associated buchungsgruppe and uses the fields belonging to the tax
-zone given by C<$params{taxzone}> (range 0..3).
+zone given by C<$params{taxzone}>.
 
 The information retrieved by the function is cached.
 
 
 The information retrieved by the function is cached.
 
@@ -301,22 +489,37 @@ C<$params{type}> and tax zone C<$params{taxzone}>
 the three key words C<income>, C<expense> and C<inventory>.
 
 This function uses the part's associated buchungsgruppe and uses the
 the three key words C<income>, C<expense> and C<inventory>.
 
 This function uses the part's associated buchungsgruppe and uses the
-fields belonging to the tax zone given by C<$params{taxzone}> (range
-0..3).
+fields belonging to the tax zone given by C<$params{taxzone}>.
 
 The information retrieved by the function is cached.
 
 =item C<orphaned>
 
 
 The information retrieved by the function is cached.
 
 =item C<orphaned>
 
-Checks if this articke is used in orders, invoices, delivery orders or
+Checks if this article is used in orders, invoices, delivery orders or
 assemblies.
 
 =item C<buchungsgruppe BUCHUNGSGRUPPE>
 
 assemblies.
 
 =item C<buchungsgruppe BUCHUNGSGRUPPE>
 
-Used to set the accounting informations from a L<SL:DB::Buchungsgruppe> object.
+Used to set the accounting information from a L<SL:DB::Buchungsgruppe> object.
 Please note, that this is a write only accessor, the original Buchungsgruppe can
 not be retrieved from an article once set.
 
 Please note, that this is a write only accessor, the original Buchungsgruppe can
 not be retrieved from an article once set.
 
+=item C<assembly_sellprice_sum>
+
+Non-recursive sellprice sum of all the assembly item sellprices.
+
+=item C<assortment_sellprice_sum>
+
+Non-recursive sellprice sum of all the assortment item sellprices.
+
+=item C<assembly_lastcost_sum>
+
+Non-recursive lastcost sum of all the assembly item lastcosts.
+
+=item C<assortment_lastcost_sum>
+
+Non-recursive lastcost sum of all the assortment item lastcosts.
+
 =back
 
 =head1 AUTHORS
 =back
 
 =head1 AUTHORS