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