Merge branch 'test' of ../kivitendo-erp_20220811
[kivitendo-erp.git] / SL / Helper / Inventory.pm
index 777801c..944a410 100644 (file)
@@ -13,7 +13,8 @@ use SL::Locale::String qw(t8);
 use SL::MoreCommon qw(listify);
 use SL::DBUtils qw(selectall_hashref_query selectrow_query);
 use SL::DB::TransferType;
-use SL::Helper::Number qw(_number _round_number);
+use SL::Helper::Number qw(_format_number _round_number);
+use SL::Helper::Inventory::Allocation;
 use SL::X;
 
 our @EXPORT_OK = qw(get_stock get_onhand allocate allocate_for_assembly produce_assembly check_constraints);
@@ -151,14 +152,8 @@ sub get_onhand {
 sub allocate {
   my (%params) = @_;
 
-  die SL::X::Inventory::Allocation->new(
-    error => 'allocate needs a part',
-    msg => t8("Method allocate needs the parameter 'part'"),
-  ) unless $params{part};
-  die SL::X::Inventory::Allocation->new(
-    error => 'allocate needs a qty',
-    msg => t8("Method allocate needs the parameter 'qty'"),
-  ) unless $params{qty};
+  croak('allocate needs a part') unless $params{part};
+  croak('allocate needs a qty')  unless $params{qty};
 
   my $part = $params{part};
   my $qty  = $params{qty};
@@ -202,8 +197,8 @@ sub allocate {
   }
   if ($rest_qty > 0) {
     die SL::X::Inventory::Allocation->new(
-      error => 'not enough to allocate',
-      msg => t8("can not allocate #1 units of #2, missing #3 units", _number(\%::myconfig, $qty), $part->displayable_name, _number(\%::myconfig, $rest_qty)),
+      code    => 'not enough to allocate',
+      message => t8("can not allocate #1 units of #2, missing #3 units", _format_number($qty), $part->displayable_name, _format_number($rest_qty)),
     );
   } else {
     if ($params{constraints}) {
@@ -218,12 +213,17 @@ sub allocate_for_assembly {
 
   my $part = $params{part} or Carp::croak('allocate needs a part');
   my $qty  = $params{qty}  or Carp::croak('allocate needs a qty');
+  my $wh   = $params{warehouse};
+  my $wh_strict       = $::instance_conf->get_produce_assembly_same_warehouse;
+  my $consume_service = $::instance_conf->get_produce_assembly_transfer_service;
 
-  Carp::croak('not an assembly') unless $part->is_assembly;
+  Carp::croak('not an assembly')       unless $part->is_assembly;
+  Carp::croak('No warehouse selected') if $wh_strict && !$wh;
 
   my %parts_to_allocate;
 
   for my $assembly ($part->assemblies) {
+    next if $assembly->part->type eq 'service' && !$consume_service;
     $parts_to_allocate{ $assembly->part->id } //= 0;
     $parts_to_allocate{ $assembly->part->id } += $assembly->qty * $qty;
   }
@@ -233,6 +233,15 @@ sub allocate_for_assembly {
   for my $part_id (keys %parts_to_allocate) {
     my $part = SL::DB::Part->load_cached($part_id);
     push @allocations, allocate(%params, part => $part, qty => $parts_to_allocate{$part_id});
+    if ($wh_strict) {
+      die SL::X::Inventory::Allocation->new(
+        code    => "wrong warehouse for part",
+        message => t8('Part #1 exists in warehouse #2, but not in warehouse #3 ',
+                        $part->partnumber . ' ' . $part->description,
+                        SL::DB::Manager::Warehouse->find_by(id => $allocations[-1]->{warehouse_id})->description,
+                        $wh->description),
+      ) unless $allocations[-1]->{warehouse_id} == $wh->id;
+    }
   }
 
   @allocations;
@@ -243,8 +252,8 @@ sub check_constraints {
   if ('CODE' eq ref $constraints) {
     if (!$constraints->(@$allocations)) {
       die SL::X::Inventory::Allocation->new(
-        error => 'allocation constraints failure',
-        msg => t8("Allocations didn't pass constraints"),
+        code    => 'allocation constraints failure',
+        message => t8("Allocations didn't pass constraints"),
       );
     }
   } else {
@@ -275,10 +284,10 @@ sub check_constraints {
         $err      .= ' '.t8('part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.',
               SL::DB::Part->load_cached($_->parts_id)->description,
               SL::DB::Bin->load_cached($_->bin_id)->full_description,
-              _number($_->qty), _number($needed), $_->chargenumber ? $_->chargenumber : '--') for @allocs;
+              _format_number($_->qty), _format_number($needed), $_->chargenumber ? $_->chargenumber : '--') for @allocs;
         die SL::X::Inventory::Allocation->new(
-          error => 'allocation constraints failure',
-          msg   => $err,
+          code    => 'allocation constraints failure',
+          message => $err,
         );
       }
     }
@@ -290,32 +299,31 @@ sub produce_assembly {
 
   my $part = $params{part} or Carp::croak('produce_assembly needs a part');
   my $qty  = $params{qty}  or Carp::croak('produce_assembly needs a qty');
+  my $bin  = $params{bin}  or Carp::croak("need target bin");
 
   my $allocations = $params{allocations};
+  my $strict_wh = $::instance_conf->get_produce_assembly_same_warehouse ? $bin->warehouse : undef;
   if ($params{auto_allocate}) {
     Carp::croak("produce_assembly: can't have both allocations and auto_allocate") if $params{allocations};
-    $allocations = [ allocate_for_assembly(part => $part, qty => $qty) ];
+    $allocations = [ allocate_for_assembly(part => $part, qty => $qty, warehouse => $strict_wh, chargenumber => $params{chargenumber}) ];
   } else {
     Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations};
     $allocations = $params{allocations};
   }
 
-  my $bin          = $params{bin} or Carp::croak("need target bin");
-  my $chargenumber = $params{chargenumber};
-  my $bestbefore   = $params{bestbefore};
+  my $chargenumber  = $params{chargenumber};
+  my $bestbefore    = $params{bestbefore};
   my $for_object_id = $params{for_object_id};
-  my $comment      = $params{comment} // '';
-
-  my $invoice               = $params{invoice};
-  my $project               = $params{project};
+  my $comment       = $params{comment} // '';
+  my $invoice       = $params{invoice};
+  my $project       = $params{project};
+  my $shippingdate  = $params{shippingsdate} // DateTime->now_local;
+  my $trans_id      = $params{trans_id};
 
-  my $shippingdate = $params{shippingsdate} // DateTime->now_local;
-
-  my $trans_id              = $params{trans_id};
   ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id;
 
   my $trans_type_out = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used');
-  my $trans_type_in  = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'assembled');
+  my $trans_type_in  = SL::DB::Manager::TransferType->find_by(direction => 'in',  description => 'assembled');
 
   # check whether allocations are sane
   if (!$params{no_check_allocations} && !$params{auto_allocate}) {
@@ -324,7 +332,10 @@ sub produce_assembly {
       $allocations_by_part{ $assembly->parts_id } -= $assembly->qty * $qty;
     }
 
-    die "allocations are insufficient for production" if any { $_ < 0 } values %allocations_by_part;
+    die SL::X::Inventory::Allocation->new(
+      code    => "allocations are insufficient for production",
+      message => t8('can not allocate enough resources for production'),
+    ) if any { $_ < 0 } values %allocations_by_part;
   }
 
   my @transfers;
@@ -336,6 +347,7 @@ sub produce_assembly {
       trans_type   => $trans_type_out,
       shippingdate => $shippingdate,
       employee     => SL::DB::Manager::Employee->current,
+      comment      => t8('Used for assembly #1 #2', $part->partnumber, $part->description),
     );
   }
 
@@ -370,42 +382,6 @@ sub default_show_bestbefore {
   $::instance_conf->get_show_bestbefore
 }
 
-package SL::Helper::Inventory::Allocation {
-  my @attributes = qw(parts_id qty bin_id warehouse_id chargenumber bestbefore comment for_object_id);
-  my %attributes = map { $_ => 1 } @attributes;
-  my %mapped_attributes = (
-    for_object_id => 'oe_id',
-  );
-
-  for my $name (@attributes) {
-    no strict 'refs';
-    *{"SL::Helper::Inventory::Allocation::$name"} = sub { $_[0]{$name} };
-  }
-
-  sub new {
-    my ($class, %params) = @_;
-
-    Carp::croak("missing attribute $_") for grep { !exists $params{$_}     } @attributes;
-    Carp::croak("unknown attribute $_") for grep { !exists $attributes{$_} } keys %params;
-    Carp::croak("$_ must be set")       for grep { !$params{$_} } qw(parts_id qty bin_id);
-    Carp::croak("$_ must be positive")  for grep { !($params{$_} > 0) } qw(parts_id qty bin_id);
-
-    bless { %params }, $class;
-  }
-
-  sub transfer_object {
-    my ($self, %params) = @_;
-
-    SL::DB::Inventory->new(
-      (map {
-        my $attr = $mapped_attributes{$_} // $_;
-        $attr => $self->{$attr}
-      } @attributes),
-      %params,
-    );
-  }
-}
-
 1;
 
 =encoding utf-8
@@ -423,9 +399,9 @@ SL::WH - Warehouse and Inventory API
   # stock, get "what's there" for a part with various conditions:
   my $qty = get_stock(part => $part);                              # how much is on stock?
   my $qty = get_stock(part => $part, date => $date);               # how much was on stock at a specific time?
-  my $qty = get_stock(part => $part, bin => $bin);                 # how is on stock in a specific bin?
-  my $qty = get_stock(part => $part, warehouse => $warehouse);     # how is on stock in a specific warehouse?
-  my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how is on stock of a specific chargenumber?
+  my $qty = get_stock(part => $part, bin => $bin);                 # how much is on stock in a specific bin?
+  my $qty = get_stock(part => $part, warehouse => $warehouse);     # how much is on stock in a specific warehouse?
+  my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how much is on stock of a specific chargenumber?
 
   # onhand, get "what's available" for a part with various conditions:
   my $qty = get_onhand(part => $part);                              # how much is available?
@@ -442,7 +418,7 @@ SL::WH - Warehouse and Inventory API
   );
 
   # allocate:
-  my @allocations, allocate(
+  my @allocations = allocate(
     part         => $part,          # part_id works too
     qty          => $qty,           # must be positive
     chargenumber => $chargenumber,  # optional, may be arrayref. if provided these charges will be used first
@@ -451,12 +427,12 @@ SL::WH - Warehouse and Inventory API
   );
 
   # shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate
-  my @allocations, allocate_for_assembly(
+  my @allocations = allocate_for_assembly(
     part         => $assembly,      # part_id works too
     qty          => $qty,           # must be positive
   );
 
-  # create allocation manually, bypassing checks, all of these need to be passed, even undefs
+  # create allocation manually, bypassing checks. all of these need to be passed, even undefs
   my $allocation = SL::Helper::Inventory::Allocation->new(
     part_id           => $part->id,
     qty               => 15,
@@ -478,15 +454,13 @@ SL::WH - Warehouse and Inventory API
     chargenumber => $chargenumber,  # optional
     bestbefore   => $datetime,      # optional
     comment      => $comment,       # optional
-
-    # links, all optional
   );
 
 =head1 DESCRIPTION
 
 New functions for the warehouse and inventory api.
 
-The WH api currently has three large shortcomings. It is very hard to just get
+The WH api currently has three large shortcomings: It is very hard to just get
 the current stock for an item, it's extremely complicated to use it to produce
 assemblies while ensuring that no stock ends up negative, and it's very hard to
 use it to get an overview over the actual contents of the inventory.
@@ -503,7 +477,7 @@ To get this cleaned up a bit this code introduces two concepts: stock and onhand
 there.
 
 =item * Onhand is what is available, which means things that are stocked,
-not expired and not reserved for other uses.
+not expired and not in any other way reserved for other uses.
 
 =back
 
@@ -526,7 +500,7 @@ C<produce_assembly> has been rewritten to only accept parameters about the
 target of the production, and requires allocations to complete the request. The
 allocations can be supplied manually, or can be generated automatically.
 C<produce_assembly> will check whether enough allocations are given to create
-the recipe, but will not check whether the allocations are backed. If the
+the assembly, but will not check whether the allocations are backed. If the
 allocations are not sufficient or if the auto-allocation fails an exception
 is returned. If you need to produce something that is not in the inventory, you
 can bypass those checks by creating the allocations yourself (see
@@ -800,13 +774,14 @@ insufficient allocations to process the request.
     the internal ordering of the hints is fixed and more complex preferentials
     are not supported.
   * bestbefore handling is untested
+  * interaction with config option "transfer_default_ignore_onhand" is
+    currently undefined (and implicitly ignores it)
 
 =head1 TODO
 
   * define and describe error classes
   * define wrapper classes for stock/onhand batch mode return values
   * handle extra arguments in produce: shippingdate, project
-  * clean up allocation helper class
   * document no_ check
   * tests
 
@@ -816,6 +791,6 @@ None yet :)
 
 =head1 AUTHOR
 
-Sven Schöling E<lt>sven.schoeling@opendynamic.deE<gt>
+Sven Schöling E<lt>sven.schoeling@googlemail.comE<gt>
 
 =cut