ShippedQty: Übergabe von Strings als ids abfangen
[kivitendo-erp.git] / SL / Helper / ShippedQty.pm
index 1712ffb..af2a4f4 100644 (file)
@@ -12,12 +12,12 @@ use List::UtilsBy qw(partition_by);
 use SL::Locale::String qw(t8);
 
 use Rose::Object::MakeMethods::Generic (
-  'scalar'                => [ qw(objects objects_or_ids shipped_qty ) ],
-  'scalar --get_set_init' => [ qw(oe_ids dbh require_stock_out fill_up item_identity_fields oi2oe oi_qty delivered) ],
+  'scalar'                => [ qw(objects objects_or_ids shipped_qty keep_matches) ],
+  'scalar --get_set_init' => [ qw(oe_ids dbh require_stock_out fill_up item_identity_fields oi2oe oi_qty delivered matches) ],
 );
 
 my $no_stock_item_links_query = <<'';
-  SELECT oi.trans_id, oi.id AS oi_id, oi.qty AS oi_qty, oi.unit AS oi_unit, doi.qty AS doi_qty, doi.unit AS doi_unit
+  SELECT oi.trans_id, oi.id AS oi_id, oi.qty AS oi_qty, oi.unit AS oi_unit, doi.id AS doi_id, doi.qty AS doi_qty, doi.unit AS doi_unit
   FROM record_links rl
   INNER JOIN orderitems oi            ON oi.id = rl.from_id AND rl.from_table = 'orderitems'
   INNER JOIN delivery_order_items doi ON doi.id = rl.to_id AND rl.to_table = 'delivery_order_items'
@@ -50,10 +50,12 @@ my $no_stock_fill_up_doi_query = <<'';
       AND to_id = doi.id)
 
 my $stock_item_links_query = <<'';
-  SELECT oi.trans_id, oi.id AS oi_id, oi.qty AS oi_qty, oi.unit AS oi_unit, i.qty AS doi_qty, p.unit AS doi_unit
+  SELECT oi.trans_id, oi.id AS oi_id, oi.qty AS oi_qty, oi.unit AS oi_unit, doi.id AS doi_id,
+    (CASE WHEN doe.customer_id > 0 THEN -1 ELSE 1 END) * i.qty AS doi_qty, p.unit AS doi_unit
   FROM record_links rl
   INNER JOIN orderitems oi                   ON oi.id = rl.from_id AND rl.from_table = 'orderitems'
   INNER JOIN delivery_order_items doi        ON doi.id = rl.to_id AND rl.to_table = 'delivery_order_items'
+  INNER JOIN delivery_orders doe             ON doe.id = doi.delivery_order_id
   INNER JOIN delivery_order_items_stock dois ON dois.delivery_order_item_id = doi.id
   INNER JOIN inventory i                     ON dois.id = i.delivery_order_items_stock_id
   INNER JOIN parts p                         ON p.id = doi.parts_id
@@ -61,10 +63,12 @@ my $stock_item_links_query = <<'';
   ORDER BY oi.trans_id, oi.position
 
 my $stock_fill_up_doi_query = <<'';
-  SELECT doi.id, doi.delivery_order_id, doi.position, doi.parts_id, doi.description, doi.reqdate, doi.serialnumber, i.qty, i.unit
+  SELECT doi.id, doi.delivery_order_id, doi.position, doi.parts_id, doi.description, doi.reqdate, doi.serialnumber,
+    (CASE WHEN doe.customer_id > 0 THEN -1 ELSE 1 END) * i.qty, p.unit
   FROM delivery_order_items doi
   INNER JOIN parts p                         ON p.id = doi.parts_id
   INNER JOIN delivery_order_items_stock dois ON dois.delivery_order_item_id = doi.id
+  INNER JOIN delivery_orders doe             ON doe.id = doi.delivery_order_id
   INNER JOIN inventory i                     ON dois.id = i.delivery_order_items_stock_id
   WHERE doi.delivery_order_id IN (
     SELECT to_id
@@ -100,14 +104,14 @@ sub calculate {
 
   die 'Need exactly one argument, either id, object or arrayref of ids or objects.' unless 2 == @_;
 
-  return if !$data || ('ARRAY' eq ref $data && !@$data);
-
   $self->normalize_input($data);
 
-  return unless @{ $self->oe_ids };
+  return $self unless @{ $self->oe_ids };
 
   $self->calculate_item_links;
   $self->calculate_fill_up if $self->fill_up;
+
+  $self;
 }
 
 sub calculate_item_links {
@@ -122,10 +126,13 @@ sub calculate_item_links {
   my $data = selectall_hashref_query($::form, $self->dbh, $query, @oe_ids);
 
   for (@$data) {
+    my $qty = $_->{doi_qty} * AM->convert_unit($_->{doi_unit} => $_->{oi_unit});
     $self->shipped_qty->{$_->{oi_id}} //= 0;
-    $self->shipped_qty->{$_->{oi_id}} += $_->{doi_qty} * AM->convert_unit($_->{doi_unit} => $_->{oi_unit});
+    $self->shipped_qty->{$_->{oi_id}} += $qty;
     $self->oi2oe->{$_->{oi_id}}        = $_->{trans_id};
     $self->oi_qty->{$_->{oi_id}}       = $_->{oi_qty};
+
+    push @{ $self->matches }, [ $_->{oi_id}, $_->{doi_id}, $qty, 1 ] if $self->keep_matches;
   }
 }
 
@@ -188,6 +195,7 @@ sub calculate_fill_up {
 
         $self->shipped_qty->{$oi->{id}} += $min_qty;
         $doi->{qty}                     -= $min_qty / $factor;  # TODO: find a way to avoid float rounding
+        push @{ $self->matches }, [ $oi->{id}, $doi->{id}, $min_qty, 0 ] if $self->keep_matches;
       }
     }
   }
@@ -205,10 +213,10 @@ sub write_to {
 
   for my $obj (@$objects) {
     if ('SL::DB::OrderItem' eq ref $obj) {
-      $obj->{shipped_qty} = $shipped_qty->{$obj->id};
+      $obj->{shipped_qty} = $shipped_qty->{$obj->id} //= 0;
       $obj->{delivered}   = $shipped_qty->{$obj->id} == $obj->qty;
     } elsif ('SL::DB::Order' eq ref $obj) {
-      if (exists $obj->{orderitems}) {
+      if (defined $obj->{orderitems}) {
         $self->write_to($obj->{orderitems});
         $obj->{delivered} = all { $_->{delivered} } @{ $obj->{orderitems} };
       } else {
@@ -219,11 +227,14 @@ sub write_to {
       die "unknown reference '@{[ ref $obj ]}' for @{[ __PACKAGE__ ]}::write_to";
     }
   }
+  $self;
 }
 
 sub write_to_objects {
   my ($self) = @_;
 
+  return unless @{ $self->oe_ids };
+
   die 'Can only use write_to_objects, when calculate was called with objects. Use write_to instead.' unless $self->objects_or_ids;
 
   $self->write_to($self->objects);
@@ -247,6 +258,7 @@ sub normalize_input {
     $self->objects($data);
   } else {
     die 'object or reference in data while expecting ids' if any { ref($_) } @$data;
+    die 'ids need to be numbers'                          if any { ! ($_ * 1) } @$data;
     $self->oe_ids($data);
   }
 
@@ -271,6 +283,7 @@ sub init_dbh { SL::DB->client->dbh }
 
 sub init_oi2oe { {} }
 sub init_oi_qty { {} }
+sub init_matches { [] }
 sub init_delivered {
   my ($self) = @_;
   my $d = { };
@@ -304,7 +317,6 @@ SL::Helper::ShippedQty - Algorithmic module for calculating shipped qty
     fill_up              => 0,
     require_stock_out    => 0,
     item_identity_fields => [ qw(parts_id description reqdate serialnumber) ],
-    set_delivered        => 1,
   );
 
   $helper->calculate($order_object);
@@ -322,10 +334,13 @@ SL::Helper::ShippedQty - Algorithmic module for calculating shipped qty
   $helper->write_to_objects;
 
   # shipped_qtys by oi_id
-  my $shipped_qtys_by_oi_id = $helper->shipped_qtys;
+  my $shipped_qty = $helper->shipped_qty->{$oi->id};
 
   # delivered by oe_id
-  my $delivered_by_oe_id = $helper->delievered;
+  my $delivered = $helper->delievered->{$oi->id};
+
+  # calculate and write_to can be chained:
+  my $helper = SL::Helper::ShippedQty->new->calculate($orders)->write_to_objects;
 
 =head1 DESCRIPTION
 
@@ -337,7 +352,7 @@ loop over and over, so take advantage of batch processing when possible.
 
 =head1 MOTIVATION AND PROBLEMS
 
-The concept of shipped qty is sadly not as straight forward as it sounds on
+The concept of shipped qty is sadly not as straight forward as it sounds at
 first glance. Any correct implementation must in some way deal with the
 following problems.
 
@@ -354,7 +369,7 @@ inventory it will mean when the delivery order is saved.
 How to find the correct matching elements. After the changes
 to record item links it's natural to assume that each position is linked, but
 for various reasons this might not be the case. Positions that are not linked
-in database need to be matched by marching.
+in the database need to be matched by marching.
 
 =item *
 
@@ -375,7 +390,7 @@ be part of the identity of a position for finding shipped matches.
 
 =item *
 
-Certain delivery orders might not be eligable for qty calculations if delivery
+Certain delivery orders might not be eligible for qty calculations if delivery
 orders are used for other purposes.
 
 =item *
@@ -433,6 +448,14 @@ default is a client setting. Possible values include:
 
 =back
 
+=item * C<keep_matches>
+
+Boolean. If set to true the internal matchings of OrderItems and
+DeliveryOrderItems will be kept for later postprocessing, in case you need more
+than this modules provides.
+
+See C<matches> for the returned format.
+
 =back
 
 =item C<calculate OBJECTS>
@@ -450,16 +473,62 @@ No return value. All internal errors will throw an exception.
 
 =item C<write_to_objects>
 
-Save the C<shipped_qty> and C<delivered> state to the objects. If L</calculate>
-was called with objects, then C<write_to_objects> will use these.
+Save the C<shipped_qty> and C<delivered> state to the given objects. If
+L</calculate> was called with objects, then C<write_to_objects> will use these.
+
+C<shipped_qty> and C<delivered> will be directly infused into the objects
+without calling the accessor for delivered. If you want to save afterwards,
+you'll have to do that yourself.
+
+C<shipped_qty> is guaranteed to be coerced to a number. If no delivery_order
+was found it will be set to zero.
+
+C<delivered> is guaranteed only to be the correct boolean value, but not
+any specific value.
+
+Note: C<write_to> will avoid loading unnecessary objects. This means if it is
+called with an Order object that has not loaded its orderitems yet, only
+C<delivered> will be set in the Order object. A subsequent C<<
+$order->orderitems->[0]->{delivered} >> will return C<undef>, and C<<
+$order->orderitems->[0]->shipped_qty >> will invoke another implicit
+calculation.
 
 =item C<shipped_qty>
 
 Valid after L</calculate>. Returns a hasref with shipped qtys by orderitems id.
 
+Unlike the result of C</write_to>, entries in C<shipped_qty> may be C<undef> if
+linked elements were found.
+
 =item C<delivered>
 
-Valid after L</calculate>. Returns a hasref with delivered flag by order id.
+Valid after L</calculate>. Returns a hashref with a delivered flag by order id.
+
+=item C<matches>
+
+Valid after L</calculate> with C<with_matches> set. Returns an arrayref of
+individual matches. Each match is an arrayref with these fields:
+
+=over 4
+
+=item *
+
+The id of the OrderItem.
+
+=item *
+
+The id of the DeliveryOrderItem.
+
+=item *
+
+The qty that was matched between the two converted to the unit of the OrderItem.
+
+=item *
+
+A boolean flag indicating if this match was found with record_item links. If
+false, the match was made in the fill up stage.
+
+=back
 
 =back
 
@@ -472,20 +541,20 @@ with a delivery order and evaluates whether those are delivered or not. No
 detailed information is needed.
 
 This is to be integrated into fast delivered check on the orders. The calling
-convention for the delivery_order is not scope of this module.
+convention for the delivery_order is not part of the scope of this module.
 
 =head2 do_mode
 
 Originally used for printing delivery orders. Resolves for each position for
-much was originally ordered, and how much remains undelivered.
+how much was originally ordered, and how much remains undelivered.
 
-This one is likely to be dropped. The information makes only sense without
+This one is likely to be dropped. The information only makes sense without
 combined merge/split deliveries and is very fragile with unaccounted delivery
 orders.
 
 =head2 oe mode
 
-Same from order perspective. Used for transitions to delivery orders, where
+Same from the order perspective. Used for transitions to delivery orders, where
 delivered qtys should be removed from positions. Also used each time a record
 is rendered to show the shipped qtys. Also used to find orders that are not
 fully delivered.
@@ -648,14 +717,14 @@ this is the old get_shipped_qty algorithm by Martin for reference
 
 =head1 COMPLEXITY OBSERVATIONS
 
-Perl ops except sort are expected to be constant (relative to the op overhead).
+Perl ops except for sort are expected to be constant (relative to the op overhead).
 
 =head2 Record item links
 
 The query itself has indices available for all joins and filters and should
-scale with sublinear with number of affected orderitems.
+scale with sublinear with the number of affected orderitems.
 
-The rest of the code iterates through the result and call C<AM::convert_unit>,
+The rest of the code iterates through the result and calls C<AM::convert_unit>,
 which caches internally and is asymptotically constant.
 
 =head2 Fill up
@@ -672,7 +741,7 @@ Iterating through the values of the partitions scales with the number of
 elements in the multimap, and does not add additional complexity.
 
 The sort and parallel walk are O(nlogn) for the length of the subdivisions,
-whioch again makes square worst case, but much less than that in the general
+which again makes square worst case, but much less than that in the general
 case.
 
 =head3 Space requirements