Inventory: Stückbehandlung und dispotype no_stock entfernt
[kivitendo-erp.git] / SL / Helper / Inventory.pm
1 package SL::Helper::Inventory;
2
3 use strict;
4 use Carp;
5 use DateTime;
6 use Exporter qw(import);
7 use List::Util qw(min sum);
8 use List::UtilsBy qw(sort_by);
9 use List::MoreUtils qw(any);
10 use POSIX qw(ceil);
11
12 use SL::Locale::String qw(t8);
13 use SL::MoreCommon qw(listify);
14 use SL::DBUtils qw(selectall_hashref_query selectrow_query);
15 use SL::DB::TransferType;
16 use SL::Helper::Number qw(_number _round_number);
17 use SL::X;
18
19 our @EXPORT_OK = qw(get_stock get_onhand allocate allocate_for_assembly produce_assembly check_constraints);
20 our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
21
22 sub _get_stock_onhand {
23   my (%params) = @_;
24
25   my $onhand_mode = !!$params{onhand};
26
27   my @selects = (
28     'SUM(qty) AS qty',
29     'MIN(EXTRACT(epoch FROM inventory.itime)) AS itime',
30   );
31   my @values;
32   my @where;
33   my @groups;
34
35   if ($params{part}) {
36     my @ids = map { ref $_ ? $_->id : $_ } listify($params{part});
37     push @where, sprintf "parts_id IN (%s)", join ', ', ("?") x @ids;
38     push @values, @ids;
39   }
40
41   if ($params{bin}) {
42     my @ids = map { ref $_ ? $_->id : $_ } listify($params{bin});
43     push @where, sprintf "bin_id IN (%s)", join ', ', ("?") x @ids;
44     push @values, @ids;
45   }
46
47   if ($params{warehouse}) {
48     my @ids = map { ref $_ ? $_->id : $_ } listify($params{warehouse});
49     push @where, sprintf "warehouse.id IN (%s)", join ', ', ("?") x @ids;
50     push @values, @ids;
51   }
52
53   if ($params{chargenumber}) {
54     my @ids = listify($params{chargenumber});
55     push @where, sprintf "chargenumber IN (%s)", join ', ', ("?") x @ids;
56     push @values, @ids;
57   }
58
59   if ($params{date}) {
60     Carp::croak("not DateTime ".$params{date}) unless ref($params{date}) eq 'DateTime';
61     push @where, sprintf "shippingdate <= ?";
62     push @values, $params{date};
63   }
64
65   if (!$params{bestbefore} && $onhand_mode && default_show_bestbefore()) {
66     $params{bestbefore} = DateTime->now_local;
67   }
68
69   if ($params{bestbefore}) {
70     Carp::croak("not DateTime ".$params{date}) unless ref($params{bestbefore}) eq 'DateTime';
71     push @where, sprintf "(bestbefore IS NULL OR bestbefore >= ?)";
72     push @values, $params{bestbefore};
73   }
74
75   # by
76   my %allowed_by = (
77     part          => [ qw(parts_id) ],
78     bin           => [ qw(bin_id inventory.warehouse_id)],
79     warehouse     => [ qw(inventory.warehouse_id) ],
80     chargenumber  => [ qw(chargenumber) ],
81     bestbefore    => [ qw(bestbefore) ],
82     for_allocate  => [ qw(parts_id bin_id inventory.warehouse_id chargenumber bestbefore) ],
83   );
84
85   if ($params{by}) {
86     for (listify($params{by})) {
87       my $selects = $allowed_by{$_} or Carp::croak("unknown option for by: $_");
88       push @selects, @$selects;
89       push @groups,  @$selects;
90     }
91   }
92
93   my $select   = join ',', @selects;
94   my $where    = @where  ? 'WHERE ' . join ' AND ', @where : '';
95   my $group_by = @groups ? 'GROUP BY ' . join ', ', @groups : '';
96
97   my $query = <<"";
98     SELECT $select FROM inventory
99     LEFT JOIN bin ON bin_id = bin.id
100     LEFT JOIN warehouse ON bin.warehouse_id = warehouse.id
101     $where
102     $group_by
103
104   if ($onhand_mode) {
105     $query .= ' HAVING SUM(qty) > 0';
106   }
107
108   my $results = selectall_hashref_query($::form, SL::DB->client->dbh, $query, @values);
109
110   my %with_objects = (
111     part         => 'SL::DB::Manager::Part',
112     bin          => 'SL::DB::Manager::Bin',
113     warehouse    => 'SL::DB::Manager::Warehouse',
114   );
115
116   my %slots = (
117     part      =>  'parts_id',
118     bin       =>  'bin_id',
119     warehouse =>  'warehouse_id',
120   );
121
122   if ($params{by} && $params{with_objects}) {
123     for my $with_object (listify($params{with_objects})) {
124       Carp::croak("unknown with_object $with_object") if !exists $with_objects{$with_object};
125
126       my $manager = $with_objects{$with_object};
127       my $slot = $slots{$with_object};
128       next if !(my @ids = map { $_->{$slot} } @$results);
129       my $objects = $manager->get_all(query => [ id => \@ids ]);
130       my %objects_by_id = map { $_->id => $_ } @$objects;
131
132       $_->{$with_object} = $objects_by_id{$_->{$slot}} for @$results;
133     }
134   }
135
136   if ($params{by}) {
137     return $results;
138   } else {
139     return $results->[0]{qty};
140   }
141 }
142
143 sub get_stock {
144   _get_stock_onhand(@_, onhand => 0);
145 }
146
147 sub get_onhand {
148   _get_stock_onhand(@_, onhand => 1);
149 }
150
151 sub allocate {
152   my (%params) = @_;
153
154   die SL::X::Inventory::Allocation->new(
155     error => 'allocate needs a part',
156     msg => t8("Method allocate needs the parameter 'part'"),
157   ) unless $params{part};
158   die SL::X::Inventory::Allocation->new(
159     error => 'allocate needs a qty',
160     msg => t8("Method allocate needs the parameter 'qty'"),
161   ) unless $params{qty};
162
163   my $part = $params{part};
164   my $qty  = $params{qty};
165
166   return () if $qty <= 0;
167
168   my $results = get_stock(part => $part, by => 'for_allocate');
169   my %bin_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{bin});
170   my %wh_whitelist  = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{warehouse});
171   my %chargenumbers = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{chargenumber});
172
173   # sort results so that chargenumbers are matched first, then wanted bins, then wanted warehouses
174   my @sorted_results = sort {
175        exists $chargenumbers{$b->{chargenumber}}  <=> exists $chargenumbers{$a->{chargenumber}} # then prefer wanted chargenumbers
176     || exists $bin_whitelist{$b->{bin_id}}        <=> exists $bin_whitelist{$a->{bin_id}}       # then prefer wanted bins
177     || exists $wh_whitelist{$b->{warehouse_id}}   <=> exists $wh_whitelist{$a->{warehouse_id}}  # then prefer wanted bins
178     || $a->{itime}                                <=> $b->{itime}                               # and finally prefer earlier charges
179   } @$results;
180   my @allocations;
181   my $rest_qty = $qty;
182
183   for my $chunk (@sorted_results) {
184     my $qty = min($chunk->{qty}, $rest_qty);
185
186     # since allocate operates on stock, this also ensures that no negative stock results are used
187     if ($qty > 0) {
188       push @allocations, SL::Helper::Inventory::Allocation->new(
189         parts_id          => $chunk->{parts_id},
190         qty               => $qty,
191         comment           => $params{comment},
192         bin_id            => $chunk->{bin_id},
193         warehouse_id      => $chunk->{warehouse_id},
194         chargenumber      => $chunk->{chargenumber},
195         bestbefore        => $chunk->{bestbefore},
196         for_object_id     => undef,
197       );
198       $rest_qty -=  _round_number($qty, 5);
199     }
200     $rest_qty = _round_number($rest_qty, 5);
201     last if $rest_qty == 0;
202   }
203   if ($rest_qty > 0) {
204     die SL::X::Inventory::Allocation->new(
205       error => 'not enough to allocate',
206       msg => t8("can not allocate #1 units of #2, missing #3 units", _number(\%::myconfig, $qty), $part->displayable_name, _number(\%::myconfig, $rest_qty)),
207     );
208   } else {
209     if ($params{constraints}) {
210       check_constraints($params{constraints},\@allocations);
211     }
212     return @allocations;
213   }
214 }
215
216 sub allocate_for_assembly {
217   my (%params) = @_;
218
219   my $part = $params{part} or Carp::croak('allocate needs a part');
220   my $qty  = $params{qty}  or Carp::croak('allocate needs a qty');
221
222   Carp::croak('not an assembly') unless $part->is_assembly;
223
224   my %parts_to_allocate;
225
226   for my $assembly ($part->assemblies) {
227     $parts_to_allocate{ $assembly->part->id } //= 0;
228     $parts_to_allocate{ $assembly->part->id } += $assembly->qty * $qty;
229   }
230
231   my @allocations;
232
233   for my $part_id (keys %parts_to_allocate) {
234     my $part = SL::DB::Part->load_cached($part_id);
235     push @allocations, allocate(%params, part => $part, qty => $parts_to_allocate{$part_id});
236   }
237
238   @allocations;
239 }
240
241 sub check_constraints {
242   my ($constraints, $allocations) = @_;
243   if ('CODE' eq ref $constraints) {
244     if (!$constraints->(@$allocations)) {
245       die SL::X::Inventory::Allocation->new(
246         error => 'allocation constraints failure',
247         msg => t8("Allocations didn't pass constraints"),
248       );
249     }
250   } else {
251     croak 'constraints needs to be a hashref' unless 'HASH' eq ref $constraints;
252
253     my %supported_constraints = (
254       bin_id       => 'bin_id',
255       warehouse_id => 'warehouse_id',
256       chargenumber => 'chargenumber',
257     );
258
259     for (keys %$constraints ) {
260       croak "unsupported constraint '$_'" unless $supported_constraints{$_};
261       next unless defined $constraints->{$_};
262
263       my %whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($constraints->{$_});
264       my $accessor = $supported_constraints{$_};
265
266       if (any { !$whitelist{$_->$accessor} } @$allocations) {
267         my %error_constraints = (
268           bin_id         => t8('Bins'),
269           warehouse_id   => t8('Warehouses'),
270           chargenumber   => t8('Chargenumbers'),
271         );
272         my @allocs = grep { $whitelist{$_->$accessor} } @$allocations;
273         my $needed = sum map { $_->qty } grep { !$whitelist{$_->$accessor} } @$allocations;
274         my $err    = t8("Cannot allocate parts.");
275         $err      .= ' '.t8('part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.',
276               SL::DB::Part->load_cached($_->parts_id)->description,
277               SL::DB::Bin->load_cached($_->bin_id)->full_description,
278               _number($_->qty), _number($needed), $_->chargenumber ? $_->chargenumber : '--') for @allocs;
279         die SL::X::Inventory::Allocation->new(
280           error => 'allocation constraints failure',
281           msg   => $err,
282         );
283       }
284     }
285   }
286 }
287
288 sub produce_assembly {
289   my (%params) = @_;
290
291   my $part = $params{part} or Carp::croak('produce_assembly needs a part');
292   my $qty  = $params{qty}  or Carp::croak('produce_assembly needs a qty');
293
294   my $allocations = $params{allocations};
295   if ($params{auto_allocate}) {
296     Carp::croak("produce_assembly: can't have both allocations and auto_allocate") if $params{allocations};
297     $allocations = [ allocate_for_assembly(part => $part, qty => $qty) ];
298   } else {
299     Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations};
300     $allocations = $params{allocations};
301   }
302
303   my $bin          = $params{bin} or Carp::croak("need target bin");
304   my $chargenumber = $params{chargenumber};
305   my $bestbefore   = $params{bestbefore};
306   my $for_object_id = $params{for_object_id};
307   my $comment      = $params{comment} // '';
308
309   my $invoice               = $params{invoice};
310   my $project               = $params{project};
311
312   my $shippingdate = $params{shippingsdate} // DateTime->now_local;
313
314   my $trans_id              = $params{trans_id};
315   ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id;
316
317   my $trans_type_out = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used');
318   my $trans_type_in  = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'assembled');
319
320   # check whether allocations are sane
321   if (!$params{no_check_allocations} && !$params{auto_allocate}) {
322     my %allocations_by_part = map { $_->parts_id  => $_->qty } @$allocations;
323     for my $assembly ($part->assemblies) {
324       $allocations_by_part{ $assembly->parts_id } -= $assembly->qty * $qty;
325     }
326
327     die "allocations are insufficient for production" if any { $_ < 0 } values %allocations_by_part;
328   }
329
330   my @transfers;
331   for my $allocation (@$allocations) {
332     my $oe_id = delete $allocation->{for_object_id};
333     push @transfers, SL::DB::Inventory->new(
334       trans_id     => $trans_id,
335       %$allocation,
336       qty          => -$allocation->qty,
337       trans_type   => $trans_type_out,
338       shippingdate => $shippingdate,
339       employee     => SL::DB::Manager::Employee->current,
340       oe_id        => $allocation->for_object_id,
341     );
342   }
343
344   push @transfers, SL::DB::Inventory->new(
345     trans_id          => $trans_id,
346     trans_type        => $trans_type_in,
347     part              => $part,
348     qty               => $qty,
349     bin               => $bin,
350     warehouse         => $bin->warehouse_id,
351     chargenumber      => $chargenumber,
352     bestbefore        => $bestbefore,
353     shippingdate      => $shippingdate,
354     project           => $project,
355     invoice           => $invoice,
356     comment           => $comment,
357     employee          => SL::DB::Manager::Employee->current,
358     oe_id             => $for_object_id,
359   );
360
361   SL::DB->client->with_transaction(sub {
362     $_->save for @transfers;
363     1;
364   }) or do {
365     die SL::DB->client->error;
366   };
367
368   @transfers;
369 }
370
371 sub default_show_bestbefore {
372   $::instance_conf->get_show_bestbefore
373 }
374
375 package SL::Helper::Inventory::Allocation {
376   my @attributes = qw(parts_id qty bin_id warehouse_id chargenumber bestbefore comment for_object_id);
377   my %attributes = map { $_ => 1 } @attributes;
378
379   for my $name (@attributes) {
380     no strict 'refs';
381     *{"SL::Helper::Inventory::Allocation::$name"} = sub { $_[0]{$name} };
382   }
383
384   sub new {
385     my ($class, %params) = @_;
386
387     Carp::croak("missing attribute $_") for grep { !exists $params{$_}     } @attributes;
388     Carp::croak("unknown attribute $_") for grep { !exists $attributes{$_} } keys %params;
389     Carp::croak("$_ must be set")       for grep { !$params{$_} } qw(parts_id qty bin_id);
390     Carp::croak("$_ must be positive")  for grep { !($params{$_} > 0) } qw(parts_id qty bin_id);
391
392     bless { %params }, $class;
393   }
394 }
395
396 1;
397
398 =encoding utf-8
399
400 =head1 NAME
401
402 SL::WH - Warehouse and Inventory API
403
404 =head1 SYNOPSIS
405
406   # See description for an intro to the concepts used here.
407
408   use SL::Helper::Inventory qw(:ALL);
409
410   # stock, get "what's there" for a part with various conditions:
411   my $qty = get_stock(part => $part);                              # how much is on stock?
412   my $qty = get_stock(part => $part, date => $date);               # how much was on stock at a specific time?
413   my $qty = get_stock(part => $part, bin => $bin);                 # how is on stock in a specific bin?
414   my $qty = get_stock(part => $part, warehouse => $warehouse);     # how is on stock in a specific warehouse?
415   my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how is on stock of a specific chargenumber?
416
417   # onhand, get "what's available" for a part with various conditions:
418   my $qty = get_onhand(part => $part);                              # how much is available?
419   my $qty = get_onhand(part => $part, date => $date);               # how much was available at a specific time?
420   my $qty = get_onhand(part => $part, bin => $bin);                 # how much is available in a specific bin?
421   my $qty = get_onhand(part => $part, warehouse => $warehouse);     # how much is available in a specific warehouse?
422   my $qty = get_onhand(part => $part, chargenumber => $chargenumber); # how much is availbale of a specific chargenumber?
423
424   # onhand batch mode:
425   my $data = get_onhand(
426     warehouse    => $warehouse,
427     by           => [ qw(bin part chargenumber) ],
428     with_objects => [ qw(bin part) ],
429   );
430
431   # allocate:
432   my @allocations, allocate(
433     part         => $part,          # part_id works too
434     qty          => $qty,           # must be positive
435     chargenumber => $chargenumber,  # optional, may be arrayref. if provided these charges will be used first
436     bestbefore   => $datetime,      # optional, defaults to today. items with bestbefore prior to that date wont be used
437     bin          => $bin,           # optional, may be arrayref. if provided
438   );
439
440   # shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate
441   my @allocations, allocate_for_assembly(
442     part         => $assembly,      # part_id works too
443     qty          => $qty,           # must be positive
444   );
445
446   # create allocation manually, bypassing checks, all of these need to be passed, even undefs
447   my $allocation = SL::Helper::Inventory::Allocation->new(
448     part_id           => $part->id,
449     qty               => 15,
450     bin_id            => $bin_obj->id,
451     warehouse_id      => $bin_obj->warehouse_id,
452     chargenumber      => '1823772365',
453     bestbefore        => undef,
454     for_object_id     => $order->id,
455   );
456
457   # produce_assembly:
458   produce_assembly(
459     part         => $part,           # target assembly
460     qty          => $qty,            # qty
461     allocations  => \@allocations,   # allocations to use. alternatively use "auto_allocate => 1,"
462
463     # where to put it
464     bin          => $bin,           # needed unless a global standard target is configured
465     chargenumber => $chargenumber,  # optional
466     bestbefore   => $datetime,      # optional
467     comment      => $comment,       # optional
468
469     # links, all optional
470   );
471
472 =head1 DESCRIPTION
473
474 New functions for the warehouse and inventory api.
475
476 The WH api currently has three large shortcomings. It is very hard to just get
477 the current stock for an item, it's extremely complicated to use it to produce
478 assemblies while ensuring that no stock ends up negative, and it's very hard to
479 use it to get an overview over the actual contents of the inventory.
480
481 The first problem has spawned several dozen small functions in the program that
482 try to implement that, and those usually miss some details. They may ignore
483 bestbefore times, comments, ignore negative quantities etc.
484
485 To get this cleaned up a bit this code introduces two concepts: stock and onhand.
486
487 =over 4
488
489 =item * Stock is defined as the actual contents of the inventory, everything that is
490 there.
491
492 =item * Onhand is what is available, which means things that are stocked,
493 not expired and not reserved for other uses.
494
495 =back
496
497 The two new functions C<get_stock> and C<get_onhand> encapsulate these principles and
498 allow simple access with some optional filters for chargenumbers or warehouses.
499 Both of them have a batch mode that can be used to get these information to
500 supllement smiple reports.
501
502 To address the safe assembly creation a new function has been added.
503 C<allocate> will try to find the requested quantity of a part in the inventory
504 and will return allocations of it which can then be used to create the
505 assembly. Allocation will happen with the C<onhand> semantics defined above,
506 meaning that by default no expired goods will be used. The caller can supply
507 hints of what shold be used and in those cases chargenumbers will be used up as
508 much as possible first. C<allocate> will always try to fulfil the request even
509 beyond those. Should the required amount not be stocked, allocate will throw an
510 exception.
511
512 C<produce_assembly> has been rewritten to only accept parameters about the
513 target of the production, and requires allocations to complete the request. The
514 allocations can be supplied manually, or can be generated automatically.
515 C<produce_assembly> will check whether enough allocations are given to create
516 the recipe, but will not check whether the allocations are backed. If the
517 allocations are not sufficient or if the auto-allocation fails an exception
518 is returned. If you need to produce something that is not in the inventory, you
519 can bypass those checks by creating the allocations yourself (see
520 L</"ALLOCATION DATA STRUCTURE">).
521
522 Note: this is only intended to cover the scenarios described above. For other cases:
523
524 =over 4
525
526 =item *
527
528 If you need actual inventory objects because of record links or something like
529 that load them directly. And strongly consider redesigning that, because it's
530 really fragile.
531
532 =item *
533
534 You need weight or accounting information you're on your own. The inventory api
535 only concerns itself with the raw quantities.
536
537 =item *
538
539 If you need the first stock date of parts, or anything related to a specific
540 transfer type or direction, this is not covered yet.
541
542 =back
543
544 =head1 FUNCTIONS
545
546 =over 4
547
548 =item * get_stock PARAMS
549
550 Returns for single parts how much actually exists in the inventory.
551
552 Options:
553
554 =over 4
555
556 =item * part
557
558 The part. Must be present without C<by>. May be arrayref with C<by>. Can be object or id.
559
560 =item * bin
561
562 If given, will only return stock on these bins. Optional. May be array, May be object or id.
563
564 =item * warehouse
565
566 If given, will only return stock on these warehouses. Optional. May be array, May be object or id.
567
568 =item * date
569
570 If given, will return stock as it were on this timestamp. Optional. Must be L<DateTime> object.
571
572 =item * chargenumber
573
574 If given, will only show stock with this chargenumber. Optional. May be array.
575
576 =item * by
577
578 See L</"STOCK/ONHAND REPORT MODE">
579
580 =item * with_objects
581
582 See L</"STOCK/ONHAND REPORT MODE">
583
584 =back
585
586 Will return a single qty normally, see L</"STOCK/ONHAND REPORT MODE"> for batch
587 mode when C<by> is given.
588
589 =item * get_onhand PARAMS
590
591 Returns for single parts how much is available in the inventory. That excludes
592 stock with expired bestbefore.
593
594 It takes the same options as L</get_stock>.
595
596 =over 4
597
598 =item * bestbefore
599
600 If given, will only return stock with a bestbefore at or after the given date.
601 Optional. Must be L<DateTime> object.
602
603 =back
604
605 =item * allocate PARAMS
606
607 Accepted parameters:
608
609 =over 4
610
611 =item * part
612
613 =item * qty
614
615 =item * bin
616
617 Bin object. Optional.
618
619 =item * warehouse
620
621 Warehouse object. Optional.
622
623 =item * chargenumber
624
625 Optional.
626
627 =item * bestbefore
628
629 Datetime. Optional.
630
631 =back
632
633 Tries to allocate the required quantity using what is currently onhand. If
634 given any of C<bin>, C<warehouse>, C<chargenumber>
635
636 =item * allocate_for_assembly PARAMS
637
638 Shortcut to allocate everything for an assembly. Takes the same arguments. Will
639 compute the required amount for each assembly part and allocate all of them.
640
641 =item * produce_assembly
642
643
644 =back
645
646 =head1 STOCK/ONHAND REPORT MODE
647
648 If the special option C<by> is given with an arrayref, the result will instead
649 be an arrayref of partitioned stocks by those fields. Valid partitions are:
650
651 =over 4
652
653 =item * part
654
655 If this is given, part is optional in the parameters
656
657 =item * bin
658
659 =item * warehouse
660
661 =item * chargenumber
662
663 =item * bestbefore
664
665 =back
666
667 Note: If you want to use the returned data to create allocations you I<need> to
668 enable all of these. To make this easier a special shortcut exists
669
670 In this mode, C<with_objects> can be used to load C<warehouse>, C<bin>,
671 C<parts>  objects in one go, just like with Rose. They
672 need to be present in C<by> before that though.
673
674 =head1 ALLOCATION ALGORITHM
675
676 When calling allocate, the current onhand (== available stock) of the item will
677 be used to decide which bins/chargenumbers/bestbefore can be used.
678
679 In general allocate will try to make the request happen, and will use the
680 provided charges up first, and then tap everything else. If you need to only
681 I<exactly> use the provided charges, you'll need to craft the allocations
682 yourself. See L</"ALLOCATION DATA STRUCTURE"> for that.
683
684 If C<chargenumber> is given, those will be used up next.
685
686 After that normal quantities will be used.
687
688 These are tiebreakers and expected to rarely matter in reality. If you need
689 finegrained control over which allocation is used, you may want to get the
690 onhands yourself and select the appropriate ones.
691
692 Only quantities with C<bestbefore> unset or after the given date will be
693 considered. If more than one charge is eligible, the earlier C<bestbefore>
694 will be used.
695
696 Allocations do NOT have an internal memory and can't react to other allocations
697 of the same part earlier. Never double allocate the same part within a
698 transaction.
699
700 =head1 ALLOCATION DATA STRUCTURE
701
702 Allocations are instances of the helper class C<SL::Helper::Inventory::Allocation>. They require
703 each of the following attributes to be set at creation time:
704
705 =over 4
706
707 =item * parts_id
708
709 =item * qty
710
711 =item * bin_id
712
713 =item * warehouse_id
714
715 =item * chargenumber
716
717 =item * bestbefore
718
719 =item * for_object_id
720
721 If set the allocations will be marked as allocated for the given object.
722 If these allocations are later used to produce an assembly, the resulting
723 consuming transactions will be marked as belonging to the given object.
724 The object may be an order, productionorder or other objects
725
726 =back
727
728 C<chargenumber>, C<bestbefore> and C<for_object_id> may be C<undef> (but must
729 still be present at creation time). Instances are considered immutable.
730
731
732 =head1 CONSTRAINTS
733
734   # whitelist constraints
735   ->allocate(
736     ...
737     constraints => {
738       bin_id       => \@allowed_bins,
739       chargenumber => \@allowed_chargenumbers,
740     }
741   );
742
743   # custom constraints
744   ->allocate(
745     constraints => sub {
746       # only allow chargenumbers with specific format
747       all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_
748
749       &&
750       # and must all have a bestbefore date
751       all { $_->bestbefore } @_;
752     }
753   )
754
755 C<allocation> is "best effort" in nature. It will take the C<bin>,
756 C<chargenumber> etc hints from the parameters, but will try it's bvest to
757 fulfil the request anyway and only bail out if it is absolutely not possible.
758
759 Sometimes you need to restrict allocations though. For this you can pass
760 additional constraints to C<allocate>. A constraint serves as a whitelist.
761 Every allocation must fulfil every constraint by having that attribute be one
762 of the given values.
763
764 In case even that is not enough, you may supply a custom check by passing a
765 function that will be given the allocation objects.
766
767 Note that both whitelists and constraints do not influence the order of
768 allocations, which is done purely from the initial parameters. They only serve
769 to reject allocations made in good faith which do fulfil required assertions.
770
771 =head1 ERROR HANDLING
772
773 C<allocate> and C<produce_assembly> will throw exceptions if the request can
774 not be completed. The usual reason will be insufficient onhand to allocate, or
775 insufficient allocations to process the request.
776
777 =head1 KNOWN PROBLEMS
778
779   * It's not currently possible to identify allocations between requests, for
780     example for presenting the user possible allocations and then actually using
781     them on the next request.
782   * It's not currently possible to give C<allocate> prior constraints.
783     Currently all constraints are treated as hints (and will be preferred) but
784     the internal ordering of the hints is fixed and more complex preferentials
785     are not supported.
786   * bestbefore handling is untested
787
788 =head1 TODO
789
790   * define and describe error classes
791   * define wrapper classes for stock/onhand batch mode return values
792   * handle extra arguments in produce: shippingdate, project
793   * clean up allocation helper class
794   * document no_ check
795   * tests
796
797 =head1 BUGS
798
799 None yet :)
800
801 =head1 AUTHOR
802
803 Sven Schöling E<lt>sven.schoeling@opendynamic.deE<gt>
804
805 =cut