Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision bae8f672

Von Sven Schöling vor mehr als 7 Jahren hinzugefügt

  • ID bae8f672cb7a0c7062b0570d3c91e7f9ac756f1f
  • Vorgänger 963330cc
  • Nachfolger f559550f

ShippedQty Helper

Unterschiede anzeigen:

SL/Helper/ShippedQty.pm
1
package SL::Helper::ShippedQty;
2

  
3
use strict;
4
use parent qw(Rose::Object);
5

  
6
use SL::AM;
7
use Scalar::Util qw(blessed);
8
use SL::DBUtils qw(selectall_hashref_query selectall_as_map);
9
use List::Util qw(min);
10
use List::MoreUtils qw(any all);
11
use List::UtilsBy qw(partition_by);
12

  
13
use Rose::Object::MakeMethods::Generic (
14
  'scalar'                => [ qw(objects objects_or_ids shipped_qty ) ],
15
  'scalar --get_set_init' => [ qw(oe_ids dbh require_stock_out fill_up item_identity_fields oi2oe oi_qty delivered) ],
16
);
17

  
18
my $no_stock_item_links_query = <<'';
19
  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
20
  FROM record_links rl
21
  INNER JOIN orderitems oi            ON oi.id = rl.from_id AND rl.from_table = 'orderitems'
22
  INNER JOIN delivery_order_items doi ON doi.id = rl.to_id AND rl.to_table = 'delivery_order_items'
23
  WHERE oi.trans_id IN (%s)
24
  ORDER BY oi.trans_id, oi.position
25

  
26
# oi not item linked. takes about 250ms for 100k hits
27
my $fill_up_oi_query = <<'';
28
  SELECT oi.id, oi.trans_id, oi.position, oi.parts_id, oi.description, oi.reqdate, oi.serialnumber, oi.qty, oi.unit
29
  FROM orderitems oi
30
  WHERE oi.trans_id IN (%s)
31
  ORDER BY oi.trans_id, oi.position
32

  
33
# doi linked by record, but not by items; 250ms for 100k hits
34
my $no_stock_fill_up_doi_query = <<'';
35
  SELECT doi.id, doi.delivery_order_id, doi.position, doi.parts_id, doi.description, doi.reqdate, doi.serialnumber, doi.qty, doi.unit
36
  FROM delivery_order_items doi
37
  WHERE doi.delivery_order_id IN (
38
    SELECT to_id
39
    FROM record_links
40
    WHERE from_id IN (%s)
41
      AND from_table = 'oe'
42
      AND to_table = 'delivery_orders'
43
      AND to_id = doi.delivery_order_id)
44
   AND NOT EXISTS (
45
    SELECT NULL
46
    FROM record_links
47
    WHERE from_table = 'orderitems'
48
      AND to_table = 'delivery_order_items'
49
      AND to_id = doi.id)
50

  
51
my $stock_item_links_query = <<'';
52
  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
53
  FROM record_links rl
54
  INNER JOIN orderitems oi                   ON oi.id = rl.from_id AND rl.from_table = 'orderitems'
55
  INNER JOIN delivery_order_items doi        ON doi.id = rl.to_id AND rl.to_table = 'delivery_order_items'
56
  INNER JOIN delivery_order_items_stock dois ON dois.delivery_order_item_id = doi.id
57
  INNER JOIN inventory i                     ON dois.id = i.delivery_order_items_stock_id
58
  INNER JOIN parts p                         ON p.id = doi.parts_id
59
  WHERE oi.trans_id IN (%s)
60
  ORDER BY oi.trans_id, oi.position
61

  
62
my $stock_fill_up_doi_query = <<'';
63
  SELECT doi.id, doi.delivery_order_id, doi.position, doi.parts_id, doi.description, doi.reqdate, doi.serialnumber, i.qty, i.unit
64
  FROM delivery_order_items doi
65
  INNER JOIN parts p                         ON p.id = doi.parts_id
66
  INNER JOIN delivery_order_items_stock dois ON dois.delivery_order_item_id = doi.id
67
  INNER JOIN inventory i                     ON dois.id = i.delivery_order_items_stock_id
68
  WHERE doi.delivery_order_id IN (
69
    SELECT to_id
70
    FROM record_links
71
    WHERE from_id IN (%s)
72
      AND from_table = 'oe'
73
      AND to_table = 'delivery_orders'
74
      AND to_id = doi.delivery_order_id)
75
   AND NOT EXISTS (
76
    SELECT NULL
77
    FROM record_links
78
    WHERE from_table = 'orderitems'
79
      AND to_table = 'delivery_order_items'
80
      AND to_id = doi.id)
81

  
82
my $oe_do_record_links = <<'';
83
  SELECT from_id, to_id
84
  FROM record_links
85
  WHERE from_id IN (%s)
86
    AND from_table = 'oe'
87
    AND to_table = 'delivery_orders'
88

  
89
sub calculate {
90
  my ($self, $data) = @_;
91

  
92
  die 'Need exactly one argument, either id, object or arrayref of ids or objects.' unless 2 == @_;
93

  
94
  return if !$data || ('ARRAY' eq ref $data && !@$data);
95

  
96
  $self->normalize_input($data);
97

  
98
  return unless @{ $self->oe_ids };
99

  
100
  $self->calculate_item_links;
101
  $self->calculate_fill_up if $self->fill_up;
102
}
103

  
104
sub calculate_item_links {
105
  my ($self) = @_;
106

  
107
  my @oe_ids = @{ $self->oe_ids };
108

  
109
  my $item_links_query = $self->require_stock_out ? $stock_item_links_query : $no_stock_item_links_query;
110

  
111
  my $query = sprintf $item_links_query, join (', ', ('?')x @oe_ids);
112

  
113
  my $data = selectall_hashref_query($::form, $self->dbh, $query, @oe_ids);
114

  
115
  for (@$data) {
116
    $self->shipped_qty->{$_->{oi_id}} //= 0;
117
    $self->shipped_qty->{$_->{oi_id}} += $_->{doi_qty} * AM->convert_unit($_->{doi_unit} => $_->{oi_unit});
118
    $self->oi2oe->{$_->{oi_id}}        = $_->{trans_id};
119
    $self->oi_qty->{$_->{oi_id}}       = $_->{oi_qty};
120
  }
121
}
122

  
123
sub _intersect {
124
  my ($a1, $a2) = @_;
125
  my %seen;
126
  grep { $seen{$_}++ } @$a1, @$a2;
127
}
128

  
129
sub calculate_fill_up {
130
  my ($self) = @_;
131

  
132
  my @oe_ids = @{ $self->oe_ids };
133

  
134
  my $fill_up_doi_query = $self->require_stock_out ? $stock_fill_up_doi_query : $no_stock_fill_up_doi_query;
135

  
136
  my $oi_query  = sprintf $fill_up_oi_query,   join (', ', ('?')x@oe_ids);
137
  my $doi_query = sprintf $fill_up_doi_query,  join (', ', ('?')x@oe_ids);
138
  my $rl_query  = sprintf $oe_do_record_links, join (', ', ('?')x@oe_ids);
139

  
140
  my $oi  = selectall_hashref_query($::form, $self->dbh, $oi_query,  @oe_ids);
141

  
142
  return unless @$oi;
143

  
144
  my $doi = selectall_hashref_query($::form, $self->dbh, $doi_query, @oe_ids);
145
  my $rl  = selectall_hashref_query($::form, $self->dbh, $rl_query,  @oe_ids);
146

  
147
  my %oi_by_identity  = partition_by { $self->item_identity($_) } @$oi;
148
  my %doi_by_id       = partition_by { $_->{delivery_order_id} } @$doi;
149
  my %doi_by_trans_id;
150
  push @{ $doi_by_trans_id{$_->{from_id}} //= [] }, @{ $doi_by_id{$_->{to_id}} }
151
    for grep { exists $doi_by_id{$_->{to_id}} } @$rl;
152

  
153
  my %doi_by_identity = partition_by { $self->item_identity($_) } @$doi;
154

  
155
  for my $match (sort keys %oi_by_identity) {
156
    next unless exists $doi_by_identity{$match};
157

  
158
    my %oi_by_oe = partition_by { $_->{trans_id} } @{ $oi_by_identity{$match} };
159
    for my $trans_id (sort { $a <=> $b } keys %oi_by_oe) {
160
      next unless my @sorted_doi = _intersect($doi_by_identity{$match}, $doi_by_trans_id{$trans_id});
161

  
162
      # sorting should be quite fast here, because there are usually only a handful of matches
163
      next unless my @sorted_oi  = sort { $a->{position} <=> $b->{position} } @{ $oi_by_oe{$trans_id} };
164

  
165
      # parallel walk through sorted oi/doi entries
166
      my $oi_i = my $doi_i = 0;
167
      my ($oi, $doi) = ($sorted_oi[$oi_i], $sorted_doi[$doi_i]);
168
      while ($oi_i < @sorted_oi && $doi_i < @sorted_doi) {
169
        $oi =  $sorted_oi[++$oi_i],   next if $oi->{qty} <= $self->shipped_qty->{$oi->{id}};
170
        $doi = $sorted_doi[++$doi_i], next if 0 == $doi->{qty};
171

  
172
        my $factor  = AM->convert_unit($doi->{unit} => $oi->{unit});
173
        my $min_qty = min($oi->{qty} - $self->shipped_qty->{$oi->{id}}, $doi->{qty} * $factor);
174

  
175
        # min_qty should never be 0 now. the first part triggers the first next,
176
        # the second triggers the second next and factor must not be 0
177
        # but it would lead to an infinite loop, so catch that.
178
        die 'panic! invalid shipping quantity' unless $min_qty;
179

  
180
        $self->shipped_qty->{$oi->{id}} += $min_qty;
181
        $doi->{qty}                     -= $min_qty / $factor;  # TODO: find a way to avoid float rounding
182
      }
183
    }
184
  }
185

  
186
  $self->oi2oe->{$_->{id}}  = $_->{trans_id} for @$oi;
187
  $self->oi_qty->{$_->{id}} = $_->{qty}      for @$oi;
188
}
189

  
190
sub write_to {
191
  my ($self, $objects) = @_;
192

  
193
  die 'expecting array of objects' unless 'ARRAY' eq ref $objects;
194

  
195
  my $shipped_qty = $self->shipped_qty;
196

  
197
  for my $obj (@$objects) {
198
    if ('SL::DB::OrderItem' eq ref $obj) {
199
      $obj->{shipped_qty} = $shipped_qty->{$obj->id};
200
      $obj->{delivered}   = $shipped_qty->{$obj->id} == $obj->qty;
201
    } elsif ('SL::DB::Order' eq ref $obj) {
202
      if (exists $obj->{orderitems}) {
203
        $self->write_to($obj->{orderitems});
204
        $obj->{delivered} = all { $_->{delivered} } @{ $obj->{orderitems} };
205
      } else {
206
        # don't force a load on items. just compute by oe_id directly
207
        $obj->{delivered} = $self->delivered->{$obj->id};
208
      }
209
    } else {
210
      die "unknown reference '@{[ ref $obj ]}' for @{[ __PACKAGE__ ]}::write_to";
211
    }
212
  }
213
}
214

  
215
sub write_to_objects {
216
  my ($self) = @_;
217

  
218
  die 'Can only use write_to_objects, when calculate was called with objects. Use write_to instead.' unless $self->objects_or_ids;
219

  
220
  $self->write_to($self->objects);
221
}
222

  
223
sub item_identity {
224
  my ($self, $row) = @_;
225

  
226
  join $;, map $row->{$_}, @{ $self->item_identity_fields };
227
}
228

  
229
sub normalize_input {
230
  my ($self, $data) = @_;
231

  
232
  $data = [$data] if 'ARRAY' ne ref $data;
233

  
234
  $self->objects_or_ids(!!blessed($data->[0]));
235

  
236
  if ($self->objects_or_ids) {
237
    die 'unblessed object in data while expecting object' if any { !blessed($_) } @$data;
238
    $self->objects($data);
239
  } else {
240
    die 'object or reference in data while expecting ids' if any { ref($_) } @$data;
241
    $self->oe_ids($data);
242
  }
243

  
244
  $self->shipped_qty({});
245
}
246

  
247
sub init_oe_ids {
248
  my ($self) = @_;
249

  
250
  die 'oe_ids not initialized in id mode'            if !$self->objects_or_ids;
251
  die 'objects not initialized before accessing ids' if $self->objects_or_ids && !defined $self->objects;
252

  
253
  [ map { $_->id } @{ $self->objects } ]
254
}
255

  
256
sub init_dbh { SL::DB->client->dbh }
257

  
258
sub init_oi2oe { {} }
259
sub init_oi_qty { {} }
260
sub init_delivered {
261
  my ($self) = @_;
262
  my $d = { };
263
  for (keys %{ $self->oi_qty }) {
264
    my $oe_id = $self->oi2oe->{$_};
265
    $d->{$oe_id} //= 1;
266
    $d->{$oe_id} &&= $self->shipped_qty->{$_} == $self->oi_qty->{$_};
267
  }
268
  $d;
269
}
270

  
271
sub init_require_stock_out { 0 }
272
sub init_item_identity_fields { [ qw(parts_id description reqdate serialnumber) ] }
273
sub init_fill_up { 1 }
274

  
275
1;
276

  
277
__END__
278

  
279
=encoding utf-8
280

  
281
=head1 NAME
282

  
283
SL::Helper::ShippedQty - Algorithmic module for calculating shipped qty
284

  
285
=head1 SYNOPSIS
286

  
287
  use SL::Helper::ShippedQty;
288

  
289
  my $helper = SL::Helper::ShippedQty->new(
290
    fill_up              => 0,
291
    require_stock_out    => 0,
292
    item_identity_fields => [ qw(parts_id description reqdate serialnumber) ],
293
    set_delivered        => 1,
294
  );
295

  
296
  $helper->calculate($order_object);
297
  $helper->calculate(\@order_objects);
298
  $helper->calculate($oe_id);
299
  $helper->calculate(\@oe_ids);
300

  
301
  # if these are items set elivered and shipped_qty
302
  # if these are orders, iterate through their items and set delivered on order
303
  $helper->write_to($objects);
304

  
305
  # if calculate was called with objects, you can use this shortcut:
306
  $helper->write_to_objects;
307

  
308
  # shipped_qtys by oi_id
309
  my $shipped_qtys_by_oi_id = $helper->shipped_qtys;
310

  
311
  # delivered by oe_id
312
  my $delivered_by_oe_id = $helper->delievered;
313

  
314
=head1 DESCRIPTION
315

  
316
This module encapsulates the algorithm needed to compute the shipped qty for
317
orderitems (hopefully) correctly and efficiently for several use cases.
318

  
319
While this is used in object accessors, it can not be fast when called in a
320
loop over and over, so take advantage of batch processing when possible.
321

  
322
=head1 MOTIVATION AND PROBLEMS
323

  
324
The concept of shipped qty is sadly not as straight forward as it sounds on
325
first glance. Any correct implementation must in some way deal with the
326
following problems.
327

  
328
=over 4
329

  
330
=item *
331

  
332
When is an order shipped? For users that use the inventory it
333
will mean when a delivery order is stocked out. For those not using the
334
inventory it will mean when the delivery order is saved.
335

  
336
=item *
337

  
338
How to find the correct matching elements. After the changes
339
to record item links it's natural to assume that each position is linked, but
340
for various reasons this might not be the case. Positions that are not linked
341
in database need to be matched by marching.
342

  
343
=item *
344

  
345
Double links need to be accounted for (these can stem from buggy code).
346

  
347
=item *
348

  
349
orderitems and oe entries may link to many of their counterparts in
350
delivery_orders. delivery_orders my be created from multiple orders. The
351
only constant is that a single entry in delivery_order_items has at most one
352
link from an orderitem.
353

  
354
=item *
355

  
356
For the fill up case the identity of positions is not clear. The naive approach
357
is just the same part, but description, charge number, reqdate and qty can all
358
be part of the identity of a position for finding shipped matches.
359

  
360
=item *
361

  
362
Certain delivery orders might not be eligable for qty calculations if delivery
363
orders are used for other purposes.
364

  
365
=item *
366

  
367
Units need to be handled correctly
368

  
369
=item *
370

  
371
Negative positions must be taken into account. A negative delivery order is
372
assumed to be a RMA of sorts, but a negative order is not as straight forward.
373

  
374
=item *
375

  
376
Must be able to work with plain ids and Rose objects, and absolutely must
377
include a bulk mode to speed up multiple objects.
378

  
379
=back
380

  
381

  
382
=head1 FUNCTIONS
383

  
384
=over 4
385

  
386
=item C<new PARAMS>
387

  
388
Creates a new helper object. PARAMS may include:
389

  
390
=over 4
391

  
392
=item * C<require_stock_out>
393

  
394
Boolean. If set, delivery orders must be stocked out to be considered
395
delivered. The default is a client setting.
396

  
397
=item * C<fill_up>
398

  
399
Boolean. If set, unlinked delivery order items will be used to fill up
400
undelivered order items. Not needed in newer installations. The default is a
401
client setting.
402

  
403
=item * C<item_identity_fields ARRAY>
404

  
405
If set, the fields are used to compute the identity of matching positions. The
406
default is a client setting. Possible values include:
407

  
408
=over 4
409

  
410
=item * C<parts_id>
411

  
412
=item * C<description>
413

  
414
=item * C<reqdate>
415

  
416
=item * C<serialnumber>
417

  
418
=back
419

  
420
=back
421

  
422
=item C<calculate OBJECTS>
423

  
424
=item C<calculate IDS>
425

  
426
Do the main work. There must be a single argument: Either an id or an
427
C<SL::DB::Order> object, or an arrayref of one of these types.
428

  
429
Mixing ids and objects will generate an exception.
430

  
431
No return value. All internal errors will throw an exception.
432

  
433
=item C<write_to OBJECTS>
434

  
435
=item C<write_to_objects>
436

  
437
Save the C<shipped_qty> and C<delivered> state to the objects. If L</calculate>
438
was called with objects, then C<write_to_objects> will use these.
439

  
440
=item C<shipped_qty>
441

  
442
Valid after L</calculate>. Returns a hasref with shipped qtys by orderitems id.
443

  
444
=item C<delivered>
445

  
446
Valid after L</calculate>. Returns a hasref with delivered flag by order id.
447

  
448
=back
449

  
450
=head1 REPLACED FUNCTIONALITY
451

  
452
=head2 delivered mode
453

  
454
Originally used in mark_orders_if_delivered. Searches for orders associated
455
with a delivery order and evaluates whether those are delivered or not. No
456
detailed information is needed.
457

  
458
This is to be integrated into fast delivered check on the orders. The calling
459
convention for the delivery_order is not scope of this module.
460

  
461
=head2 do_mode
462

  
463
Originally used for printing delivery orders. Resolves for each position for
464
much was originally ordered, and how much remains undelivered.
465

  
466
This one is likely to be dropped. The information makes only sense without
467
combined merge/split deliveries and is very fragile with unaccounted delivery
468
orders.
469

  
470
=head2 oe mode
471

  
472
Same from order perspective. Used for transitions to delivery orders, where
473
delivered qtys should be removed from positions. Also used each time a record
474
is rendered to show the shipped qtys. Also used to find orders that are not
475
fully delivered.
476

  
477
Acceptable shortcuts would be the concepts fully shipped (for the order) and
478
providing already loaded objects.
479

  
480
=head2 Replaces the following functions
481

  
482
C<DO::get_shipped_qty>
483

  
484
C<SL::Controller::DeliveryPlan::calc_qtys>
485

  
486
C<SL::DB::OrderItem::shipped_qty>
487

  
488
C<SL::DB::OrderItem::delivered_qty>
489

  
490
=head1 OLD ALGORITHM
491

  
492
this is the old get_shipped_qty algorithm by Martin for reference
493

  
494
    in: oe_id, do_id, doctype, delivered flag
495

  
496
    not needed with better signatures
497
     if do_id:
498
       load oe->do links for this id,
499
       set oe_ids from those
500
     fi
501
     if oe_id:
502
       set oe_ids to this
503

  
504
    return if no oe_ids;
505

  
506
  2 load all orderitems for these oe_ids
507
    for orderitem:
508
      nomalize qty
509
      set undelivered := qty
510
    end
511

  
512
    create tuple: [ position => qty_ordered, qty_not_delivered, orderitem.id ]
513

  
514
  1 load all oe->do links for these oe_ids
515

  
516
    if no links:
517
      return all tuples so far
518
    fi
519

  
520
  4 create dictionary for orderitems from [2] by id
521

  
522
  3 load all delivery_order_items for do_ids from [1], with recorditem_links from orderitems
523
      - optionally with doctype filter (identity filter)
524

  
525
    # first pass for record_item_links
526
    for dois:
527
      normalize qty
528
      if link from orderitem exists and orderitem is in dictionary [4]
529
        reduce qty_notdelivered in orderitem by doi.qty
530
        keep link to do entry in orderitem
531
    end
532

  
533
    # second pass fill up
534
    for dois:
535
      ignroe if from link exists or qty == 0
536

  
537
      for orderitems from [2]:
538
        next if notdelivered_qty == 0
539
        if doi.parts_id == orderitem.parts_id:
540
          if oi.notdelivered_qty < 0:
541
            doi :+= -oi.notdelivered_qty,
542
            oi.notdelivered_qty := 0
543
          else:
544
            fi doi.qty < oi.notdelivered_qty:
545
              doi.qty := 0
546
              oi.notdelivered_qty :-= doi.qty
547
            else:
548
              doi.qty :-= oi.notdelivered_qty
549
              oi.notdelivered_qty := 0
550
            fi
551
            keep link to oi in doi
552
          fi
553
        fi
554
        last wenn doi.qty <= 0
555
      end
556
    end
557

  
558
    # post process for return
559

  
560
    if oe_id:
561
      copy notdelivered from oe to ship{position}{notdelivered}
562
    if !oe_id and do_id and delivered:
563
      ship.{oi.trans_id}.delivered := oi.notdelivered_qty <= 0
564
    if !oe_id and do_id and !delivered:
565
      for all doi:
566
        ignore if do.id != doi.delivery_order_id
567
        if oi in doi verlinkt und position bekannt:
568
          addiere oi.qty               zu doi.ordered_qty
569
          addiere oi.notdelievered_qty zu doi.notdelivered_qty
570
        fi
571
      end
572
    fi
573

  
574
=head1 NEW ALGORITHM
575

  
576
  in: orders, parameters
577

  
578
  normalize orders to ids
579

  
580
  # handle record_item links
581
  retrieve record_links entries with inner joins on orderitems, delivery_orderitems and stock/inventory if requested
582
  for all record_links:
583
    initialize shipped_qty for this doi to 0 if not yet seen
584
    convert doi.qty to oi.unit
585
    add normalized doi.qty to shipped_qty
586
  end
587

  
588
  # handle fill up
589
  abort if fill up is not requested
590

  
591
  retrieve all orderitems matching the given order ids
592
  retrieve all doi with a link to the given order ids but without item link (and optionally with stock/inventory)
593
  retrieve all record_links between orders and delivery_orders                  (1)
594

  
595
  abort when no dois were found
596

  
597
  create a partition of the delivery order items by do_id                       (2)
598
  create empty mapping for delivery order items by order_id                     (3)
599
  for all record_links from [1]:
600
    add all matching doi from (2) to (3)
601
  end
602

  
603
  create a partition of the orderitems by item identity                         (4)
604
  create a partition of the delivery order items by item identity               (5)
605

  
606
  for each identity in (4):
607
    skip if no matching entries in (5)
608

  
609
    create partition of all orderitems for this identity by order id            (6)
610
    for each sorted order id in [6]:
611
      look up matching delivery order items by identity from [5]                (7)
612
      look up matching delivery order items by order id from [3]                (8)
613
      create stable sorted intersection between [7] and [8]                     (9)
614

  
615
      sort the orderitems from (6) by position                                 (10)
616

  
617
      parallel walk through [9] and [10]:
618
        missing qty :=  oi.qty - shipped_qty[oi]
619

  
620

  
621
        next orderitem           if missing_qty <= 0
622
        next delivery order item if doi.qty == 0
623

  
624
        min_qty := minimum(missing_qty, [doi.qty converted to oi.unit]
625

  
626
        # transfer min_qty from doi.qty to shipped[qty]:
627
        shipped_qty[oi] += min_qty
628
        doi.qty         -= [min_qty converted to doi.unit]
629
      end
630
    end
631
  end
632

  
633
=head1 COMPLEXITY OBSERVATIONS
634

  
635
Perl ops except sort are expected to be constant (relative to the op overhead).
636

  
637
=head2 Record item links
638

  
639
The query itself has indices available for all joins and filters and should
640
scale with sublinear with number of affected orderitems.
641

  
642
The rest of the code iterates through the result and call C<AM::convert_unit>,
643
which caches internally and is asymptotically constant.
644

  
645
=head2 Fill up
646

  
647
C<partition_by> and C<intersect> both scale linearly. The first two scale with
648
input size, but use existing indices. The delivery order items query scales
649
with the nested loop anti join of the "NOT EXISTS" subquery, which takes most
650
of the time. For large databases omitting the order id filter may be faster.
651

  
652
Three partitions after that scale linearly. Building the doi_by_oe_id
653
multimap is O(n²) worst case, but will be linear for most real life data.
654

  
655
Iterating through the values of the partitions scales with the number of
656
elements in the multimap, and does not add additional complexity.
657

  
658
The sort and parallel walk are O(nlogn) for the length of the subdivisions,
659
whioch again makes square worst case, but much less than that in the general
660
case.
661

  
662
=head3 Space requirements
663

  
664
In the current form the results of the 4 queries get fetched, and 4 of them are
665
held in memory at the same time. Three persistent structures are held:
666
C<shipped_qty>, C<oi2oe>, and C<oi_qty> - all hashes with one entry for each
667
orderitem. C<delivered> is calculated on demand and is a hash with an entry for
668
each order id of input.
669

  
670
Temporary structures are partitions of the orderitems, of which again the fill
671
up multi map between order id and delivery order items is potentially the
672
largest with square requierment worst case.
673

  
674

  
675
=head1 TODO
676

  
677
  * delivery order identity
678
  * test stocked
679
  * rewrite to avoid division
680
  * rewrite to avoid selectall for really large queries (no problem for up to 100k)
681
  * calling mode or return to flag delivery_orders as delivered?
682
  * add localized field white list
683
  * reduce worst case square space requirement to linear
684

  
685
=head1 BUGS
686

  
687
None yet, but there are most likely a lot in code this funky.
688

  
689
=head1 AUTHOR
690

  
691
Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
692

  
693
=cut

Auch abrufbar als: Unified diff