Shop: Woocommerce Debug raus
[kivitendo-erp.git] / SL / Helper / ShippedQty.pm
index 2054513..2ac4e39 100644 (file)
@@ -3,21 +3,23 @@ package SL::Helper::ShippedQty;
 use strict;
 use parent qw(Rose::Object);
 
-use SL::AM;
+use Carp;
 use Scalar::Util qw(blessed);
-use SL::DBUtils qw(selectall_hashref_query selectall_as_map);
 use List::Util qw(min);
-use List::MoreUtils qw(any all);
+use List::MoreUtils qw(any all uniq);
 use List::UtilsBy qw(partition_by);
+use SL::AM;
+use SL::DBUtils qw(selectall_hashref_query selectall_as_map);
 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
+                                  services_deliverable) ],
 );
 
 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'
@@ -25,6 +27,7 @@ my $no_stock_item_links_query = <<'';
   ORDER BY oi.trans_id, oi.position
 
 # oi not item linked. takes about 250ms for 100k hits
+# obsolete since 3.5.6
 my $fill_up_oi_query = <<'';
   SELECT oi.id, oi.trans_id, oi.position, oi.parts_id, oi.description, oi.reqdate, oi.serialnumber, oi.qty, oi.unit
   FROM orderitems oi
@@ -32,6 +35,7 @@ my $fill_up_oi_query = <<'';
   ORDER BY oi.trans_id, oi.position
 
 # doi linked by record, but not by items; 250ms for 100k hits
+# obsolete since 3.5.6
 my $no_stock_fill_up_doi_query = <<'';
   SELECT doi.id, doi.delivery_order_id, doi.position, doi.parts_id, doi.description, doi.reqdate, doi.serialnumber, doi.qty, doi.unit
   FROM delivery_order_items doi
@@ -50,10 +54,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 +67,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
@@ -98,16 +106,16 @@ my %item_identity_fields = (
 sub calculate {
   my ($self, $data) = @_;
 
-  die 'Need exactly one argument, either id, object or arrayref of ids or objects.' unless 2 == @_;
-
-  return if !$data || ('ARRAY' eq ref $data && !@$data);
+  croak 'Need exactly one argument, either id, object or arrayref of ids or objects.' unless 2 == @_;
 
   $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 +130,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 +199,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;
       }
     }
   }
@@ -199,32 +211,36 @@ sub calculate_fill_up {
 sub write_to {
   my ($self, $objects) = @_;
 
-  die 'expecting array of objects' unless 'ARRAY' eq ref $objects;
+  croak 'expecting array of objects' unless 'ARRAY' eq ref $objects;
 
   my $shipped_qty = $self->shipped_qty;
 
   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}) {
-        $self->write_to($obj->{orderitems});
-        $obj->{delivered} = all { $_->{delivered} } @{ $obj->{orderitems} };
+      # load all orderitems unless not already loaded
+      $obj->orderitems unless (defined $obj->{orderitems});
+      $self->write_to($obj->{orderitems});
+      if ($self->services_deliverable) {
+        $obj->{delivered} = all { $_->{delivered} } grep { !$_->{optional} } @{ $obj->{orderitems} };
       } else {
-        # don't force a load on items. just compute by oe_id directly
-        $obj->{delivered} = $self->delivered->{$obj->id};
+        $obj->{delivered} = all { $_->{delivered} } grep { !$_->{optional} && !$_->part->is_service } @{ $obj->{orderitems} };
       }
     } else {
       die "unknown reference '@{[ ref $obj ]}' for @{[ __PACKAGE__ ]}::write_to";
     }
   }
+  $self;
 }
 
 sub write_to_objects {
   my ($self) = @_;
 
-  die 'Can only use write_to_objects, when calculate was called with objects. Use write_to instead.' unless $self->objects_or_ids;
+  return unless @{ $self->oe_ids };
+
+  croak '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);
 }
@@ -243,16 +259,33 @@ sub normalize_input {
   $self->objects_or_ids(!!blessed($data->[0]));
 
   if ($self->objects_or_ids) {
-    die 'unblessed object in data while expecting object' if any { !blessed($_) } @$data;
+    croak 'unblessed object in data while expecting object' if any { !blessed($_) } @$data;
     $self->objects($data);
   } else {
-    die 'object or reference in data while expecting ids' if any { ref($_) } @$data;
+    croak 'object or reference in data while expecting ids' if any { ref($_) } @$data;
+    croak 'ids need to be numbers'                          if any { ! ($_ * 1) } @$data;
     $self->oe_ids($data);
   }
 
   $self->shipped_qty({});
 }
 
+# some of the invocations never need to load all orderitems to copute their answers
+# delivered however needs oi_qty to be set for each orderitem to decide whether
+# delivered should be set or not.
+sub ensure_all_orderitems_for_orders {
+  my ($self) = @_;
+
+  return if $self->fill_up;
+
+  my $oi_query  = sprintf $fill_up_oi_query,   join (', ', ('?')x@{ $self->oe_ids });
+  my $oi  = selectall_hashref_query($::form, $self->dbh, $oi_query, @{ $self->oe_ids });
+  for (@$oi) {
+    $self->{oi_qty}{ $_->{id} } //= $_->{qty};
+    $self->{oi2oe}{ $_->{id} }  //= $_->{trans_id};
+  }
+}
+
 sub available_item_identity_fields {
   map { [ $_ => $item_identity_fields{$_} ] } @known_item_identity_fields;
 }
@@ -260,18 +293,24 @@ sub available_item_identity_fields {
 sub init_oe_ids {
   my ($self) = @_;
 
-  die 'oe_ids not initialized in id mode'            if !$self->objects_or_ids;
-  die 'objects not initialized before accessing ids' if $self->objects_or_ids && !defined $self->objects;
+  croak 'oe_ids not initialized in id mode'            if !$self->objects_or_ids;
+  croak 'objects not initialized before accessing ids' if $self->objects_or_ids && !defined $self->objects;
+  croak 'objects need to be Order or OrderItem'        if any  {  ref($_) !~ /^SL::DB::Order(?:Item)?$/ } @{ $self->objects };
 
-  [ map { $_->id } @{ $self->objects } ]
+  [ uniq map { ref($_) =~ /Item/ ? $_->trans_id : $_->id } @{ $self->objects } ]
 }
 
 sub init_dbh { SL::DB->client->dbh }
 
 sub init_oi2oe { {} }
 sub init_oi_qty { {} }
+sub init_matches { [] }
 sub init_delivered {
   my ($self) = @_;
+
+  # is needed in odyn
+  # $self->ensure_all_orderitems_for_orders;
+
   my $d = { };
   for (keys %{ $self->oi_qty }) {
     my $oe_id = $self->oi2oe->{$_};
@@ -285,6 +324,17 @@ sub init_require_stock_out    { $::instance_conf->get_shipped_qty_require_stock_
 sub init_item_identity_fields { [ grep $item_identity_fields{$_}, @{ $::instance_conf->get_shipped_qty_item_identity_fields } ] }
 sub init_fill_up              { $::instance_conf->get_shipped_qty_fill_up  }
 
+sub init_services_deliverable  {
+  my ($self) = @_;
+  if ($::form->{type} =~ m/^sales_/) {
+    $::instance_conf->get_sales_delivery_order_check_service;
+  } elsif ($::form->{type} =~ m/^purchase_/) {
+    $::instance_conf->get_purchase_delivery_order_check_service;
+  } else {
+    croak "wrong call, no customer or vendor object referenced";
+  }
+}
+
 1;
 
 __END__
@@ -303,15 +353,16 @@ 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);
   $helper->calculate(\@order_objects);
+  $helper->calculate($orderitem_object);
+  $helper->calculate(\@orderitem_objects);
   $helper->calculate($oe_id);
   $helper->calculate(\@oe_ids);
 
-  # if these are items set elivered and shipped_qty
+  # if these are items set delivered and shipped_qty
   # if these are orders, iterate through their items and set delivered on order
   $helper->write_to($objects);
 
@@ -319,10 +370,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
 
@@ -334,7 +388,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.
 
@@ -351,7 +405,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 *
 
@@ -372,7 +426,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 *
@@ -398,7 +452,9 @@ include a bulk mode to speed up multiple objects.
 
 =item C<new PARAMS>
 
-Creates a new helper object. PARAMS may include:
+Creates a new helper object, $::form->{type} is mandatory.
+
+PARAMS may include:
 
 =over 4
 
@@ -430,6 +486,15 @@ 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>
@@ -447,16 +512,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
 
@@ -469,20 +580,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.
@@ -645,14 +756,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
@@ -669,7 +780,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