cc7601f6d550ff972331165a3c8546b53aa60880
[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   my $bin  = $params{bin}  or Carp::croak("need target bin");
289
290   my $allocations = $params{allocations};
291   if ($params{auto_allocate}) {
292     Carp::croak("produce_assembly: can't have both allocations and auto_allocate") if $params{allocations};
293     $allocations = [ allocate_for_assembly(part => $part, qty => $qty) ];
294   } else {
295     Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations};
296     $allocations = $params{allocations};
297   }
298
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   my $invoice       = $params{invoice};
304   my $project       = $params{project};
305   my $shippingdate  = $params{shippingsdate} // DateTime->now_local;
306   my $trans_id      = $params{trans_id};
307
308   ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id;
309
310   my $trans_type_out = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used');
311   my $trans_type_in  = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'assembled');
312
313   # check whether allocations are sane
314   if (!$params{no_check_allocations} && !$params{auto_allocate}) {
315     my %allocations_by_part = map { $_->parts_id  => $_->qty } @$allocations;
316     for my $assembly ($part->assemblies) {
317       $allocations_by_part{ $assembly->parts_id } -= $assembly->qty * $qty;
318     }
319
320     die SL::X::Inventory::Allocation->new(
321       code    => "allocations are insufficient for production",
322       message => t8('can not allocate enough resources for production'),
323     ) if any { $_ < 0 } values %allocations_by_part;
324   }
325
326   my @transfers;
327   for my $allocation (@$allocations) {
328     my $oe_id = delete $allocation->{for_object_id};
329     push @transfers, $allocation->transfer_object(
330       trans_id     => $trans_id,
331       qty          => -$allocation->qty,
332       trans_type   => $trans_type_out,
333       shippingdate => $shippingdate,
334       employee     => SL::DB::Manager::Employee->current,
335     );
336   }
337
338   push @transfers, SL::DB::Inventory->new(
339     trans_id          => $trans_id,
340     trans_type        => $trans_type_in,
341     part              => $part,
342     qty               => $qty,
343     bin               => $bin,
344     warehouse         => $bin->warehouse_id,
345     chargenumber      => $chargenumber,
346     bestbefore        => $bestbefore,
347     shippingdate      => $shippingdate,
348     project           => $project,
349     invoice           => $invoice,
350     comment           => $comment,
351     employee          => SL::DB::Manager::Employee->current,
352     oe_id             => $for_object_id,
353   );
354
355   SL::DB->client->with_transaction(sub {
356     $_->save for @transfers;
357     1;
358   }) or do {
359     die SL::DB->client->error;
360   };
361
362   @transfers;
363 }
364
365 sub default_show_bestbefore {
366   $::instance_conf->get_show_bestbefore
367 }
368
369 1;
370
371 =encoding utf-8
372
373 =head1 NAME
374
375 SL::WH - Warehouse and Inventory API
376
377 =head1 SYNOPSIS
378
379   # See description for an intro to the concepts used here.
380
381   use SL::Helper::Inventory qw(:ALL);
382
383   # stock, get "what's there" for a part with various conditions:
384   my $qty = get_stock(part => $part);                              # how much is on stock?
385   my $qty = get_stock(part => $part, date => $date);               # how much was on stock at a specific time?
386   my $qty = get_stock(part => $part, bin => $bin);                 # how much is on stock in a specific bin?
387   my $qty = get_stock(part => $part, warehouse => $warehouse);     # how much is on stock in a specific warehouse?
388   my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how much is on stock of a specific chargenumber?
389
390   # onhand, get "what's available" for a part with various conditions:
391   my $qty = get_onhand(part => $part);                              # how much is available?
392   my $qty = get_onhand(part => $part, date => $date);               # how much was available at a specific time?
393   my $qty = get_onhand(part => $part, bin => $bin);                 # how much is available in a specific bin?
394   my $qty = get_onhand(part => $part, warehouse => $warehouse);     # how much is available in a specific warehouse?
395   my $qty = get_onhand(part => $part, chargenumber => $chargenumber); # how much is availbale of a specific chargenumber?
396
397   # onhand batch mode:
398   my $data = get_onhand(
399     warehouse    => $warehouse,
400     by           => [ qw(bin part chargenumber) ],
401     with_objects => [ qw(bin part) ],
402   );
403
404   # allocate:
405   my @allocations = allocate(
406     part         => $part,          # part_id works too
407     qty          => $qty,           # must be positive
408     chargenumber => $chargenumber,  # optional, may be arrayref. if provided these charges will be used first
409     bestbefore   => $datetime,      # optional, defaults to today. items with bestbefore prior to that date wont be used
410     bin          => $bin,           # optional, may be arrayref. if provided
411   );
412
413   # shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate
414   my @allocations = allocate_for_assembly(
415     part         => $assembly,      # part_id works too
416     qty          => $qty,           # must be positive
417   );
418
419   # create allocation manually, bypassing checks. all of these need to be passed, even undefs
420   my $allocation = SL::Helper::Inventory::Allocation->new(
421     part_id           => $part->id,
422     qty               => 15,
423     bin_id            => $bin_obj->id,
424     warehouse_id      => $bin_obj->warehouse_id,
425     chargenumber      => '1823772365',
426     bestbefore        => undef,
427     for_object_id     => $order->id,
428   );
429
430   # produce_assembly:
431   produce_assembly(
432     part         => $part,           # target assembly
433     qty          => $qty,            # qty
434     allocations  => \@allocations,   # allocations to use. alternatively use "auto_allocate => 1,"
435
436     # where to put it
437     bin          => $bin,           # needed unless a global standard target is configured
438     chargenumber => $chargenumber,  # optional
439     bestbefore   => $datetime,      # optional
440     comment      => $comment,       # optional
441   );
442
443 =head1 DESCRIPTION
444
445 New functions for the warehouse and inventory api.
446
447 The WH api currently has three large shortcomings: It is very hard to just get
448 the current stock for an item, it's extremely complicated to use it to produce
449 assemblies while ensuring that no stock ends up negative, and it's very hard to
450 use it to get an overview over the actual contents of the inventory.
451
452 The first problem has spawned several dozen small functions in the program that
453 try to implement that, and those usually miss some details. They may ignore
454 bestbefore times, comments, ignore negative quantities etc.
455
456 To get this cleaned up a bit this code introduces two concepts: stock and onhand.
457
458 =over 4
459
460 =item * Stock is defined as the actual contents of the inventory, everything that is
461 there.
462
463 =item * Onhand is what is available, which means things that are stocked,
464 not expired and not in any other way reserved for other uses.
465
466 =back
467
468 The two new functions C<get_stock> and C<get_onhand> encapsulate these principles and
469 allow simple access with some optional filters for chargenumbers or warehouses.
470 Both of them have a batch mode that can be used to get these information to
471 supplement simple reports.
472
473 To address the safe assembly creation a new function has been added.
474 C<allocate> will try to find the requested quantity of a part in the inventory
475 and will return allocations of it which can then be used to create the
476 assembly. Allocation will happen with the C<onhand> semantics defined above,
477 meaning that by default no expired goods will be used. The caller can supply
478 hints of what shold be used and in those cases chargenumbers will be used up as
479 much as possible first. C<allocate> will always try to fulfil the request even
480 beyond those. Should the required amount not be stocked, allocate will throw an
481 exception.
482
483 C<produce_assembly> has been rewritten to only accept parameters about the
484 target of the production, and requires allocations to complete the request. The
485 allocations can be supplied manually, or can be generated automatically.
486 C<produce_assembly> will check whether enough allocations are given to create
487 the assembly, but will not check whether the allocations are backed. If the
488 allocations are not sufficient or if the auto-allocation fails an exception
489 is returned. If you need to produce something that is not in the inventory, you
490 can bypass those checks by creating the allocations yourself (see
491 L</"ALLOCATION DATA STRUCTURE">).
492
493 Note: this is only intended to cover the scenarios described above. For other cases:
494
495 =over 4
496
497 =item *
498
499 If you need actual inventory objects because of record links or something like
500 that load them directly. And strongly consider redesigning that, because it's
501 really fragile.
502
503 =item *
504
505 You need weight or accounting information you're on your own. The inventory api
506 only concerns itself with the raw quantities.
507
508 =item *
509
510 If you need the first stock date of parts, or anything related to a specific
511 transfer type or direction, this is not covered yet.
512
513 =back
514
515 =head1 FUNCTIONS
516
517 =over 4
518
519 =item * get_stock PARAMS
520
521 Returns for single parts how much actually exists in the inventory.
522
523 Options:
524
525 =over 4
526
527 =item * part
528
529 The part. Must be present without C<by>. May be arrayref with C<by>. Can be object or id.
530
531 =item * bin
532
533 If given, will only return stock on these bins. Optional. May be array, May be object or id.
534
535 =item * warehouse
536
537 If given, will only return stock on these warehouses. Optional. May be array, May be object or id.
538
539 =item * date
540
541 If given, will return stock as it were on this timestamp. Optional. Must be L<DateTime> object.
542
543 =item * chargenumber
544
545 If given, will only show stock with this chargenumber. Optional. May be array.
546
547 =item * by
548
549 See L</"STOCK/ONHAND REPORT MODE">
550
551 =item * with_objects
552
553 See L</"STOCK/ONHAND REPORT MODE">
554
555 =back
556
557 Will return a single qty normally, see L</"STOCK/ONHAND REPORT MODE"> for batch
558 mode when C<by> is given.
559
560 =item * get_onhand PARAMS
561
562 Returns for single parts how much is available in the inventory. That excludes
563 stock with expired bestbefore.
564
565 It takes the same options as L</get_stock>.
566
567 =over 4
568
569 =item * bestbefore
570
571 If given, will only return stock with a bestbefore at or after the given date.
572 Optional. Must be L<DateTime> object.
573
574 =back
575
576 =item * allocate PARAMS
577
578 Accepted parameters:
579
580 =over 4
581
582 =item * part
583
584 =item * qty
585
586 =item * bin
587
588 Bin object. Optional.
589
590 =item * warehouse
591
592 Warehouse object. Optional.
593
594 =item * chargenumber
595
596 Optional.
597
598 =item * bestbefore
599
600 Datetime. Optional.
601
602 =back
603
604 Tries to allocate the required quantity using what is currently onhand. If
605 given any of C<bin>, C<warehouse>, C<chargenumber>
606
607 =item * allocate_for_assembly PARAMS
608
609 Shortcut to allocate everything for an assembly. Takes the same arguments. Will
610 compute the required amount for each assembly part and allocate all of them.
611
612 =item * produce_assembly
613
614
615 =back
616
617 =head1 STOCK/ONHAND REPORT MODE
618
619 If the special option C<by> is given with an arrayref, the result will instead
620 be an arrayref of partitioned stocks by those fields. Valid partitions are:
621
622 =over 4
623
624 =item * part
625
626 If this is given, part is optional in the parameters
627
628 =item * bin
629
630 =item * warehouse
631
632 =item * chargenumber
633
634 =item * bestbefore
635
636 =back
637
638 Note: If you want to use the returned data to create allocations you I<need> to
639 enable all of these. To make this easier a special shortcut exists
640
641 In this mode, C<with_objects> can be used to load C<warehouse>, C<bin>,
642 C<parts>  objects in one go, just like with Rose. They
643 need to be present in C<by> before that though.
644
645 =head1 ALLOCATION ALGORITHM
646
647 When calling allocate, the current onhand (== available stock) of the item will
648 be used to decide which bins/chargenumbers/bestbefore can be used.
649
650 In general allocate will try to make the request happen, and will use the
651 provided charges up first, and then tap everything else. If you need to only
652 I<exactly> use the provided charges, you'll need to craft the allocations
653 yourself. See L</"ALLOCATION DATA STRUCTURE"> for that.
654
655 If C<chargenumber> is given, those will be used up next.
656
657 After that normal quantities will be used.
658
659 These are tiebreakers and expected to rarely matter in reality. If you need
660 finegrained control over which allocation is used, you may want to get the
661 onhands yourself and select the appropriate ones.
662
663 Only quantities with C<bestbefore> unset or after the given date will be
664 considered. If more than one charge is eligible, the earlier C<bestbefore>
665 will be used.
666
667 Allocations do NOT have an internal memory and can't react to other allocations
668 of the same part earlier. Never double allocate the same part within a
669 transaction.
670
671 =head1 ALLOCATION DATA STRUCTURE
672
673 Allocations are instances of the helper class C<SL::Helper::Inventory::Allocation>. They require
674 each of the following attributes to be set at creation time:
675
676 =over 4
677
678 =item * parts_id
679
680 =item * qty
681
682 =item * bin_id
683
684 =item * warehouse_id
685
686 =item * chargenumber
687
688 =item * bestbefore
689
690 =item * for_object_id
691
692 If set the allocations will be marked as allocated for the given object.
693 If these allocations are later used to produce an assembly, the resulting
694 consuming transactions will be marked as belonging to the given object.
695 The object may be an order, productionorder or other objects
696
697 =back
698
699 C<chargenumber>, C<bestbefore> and C<for_object_id> and C<comment> may be
700 C<undef> (but must still be present at creation time). Instances are considered
701 immutable.
702
703 Allocations also provide the method C<transfer_object> which will create a new
704 C<SL::DB::Inventory> bject with all the playload.
705
706 =head1 CONSTRAINTS
707
708   # whitelist constraints
709   ->allocate(
710     ...
711     constraints => {
712       bin_id       => \@allowed_bins,
713       chargenumber => \@allowed_chargenumbers,
714     }
715   );
716
717   # custom constraints
718   ->allocate(
719     constraints => sub {
720       # only allow chargenumbers with specific format
721       all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_
722
723       &&
724       # and must all have a bestbefore date
725       all { $_->bestbefore } @_;
726     }
727   )
728
729 C<allocation> is "best effort" in nature. It will take the C<bin>,
730 C<chargenumber> etc hints from the parameters, but will try it's bvest to
731 fulfil the request anyway and only bail out if it is absolutely not possible.
732
733 Sometimes you need to restrict allocations though. For this you can pass
734 additional constraints to C<allocate>. A constraint serves as a whitelist.
735 Every allocation must fulfil every constraint by having that attribute be one
736 of the given values.
737
738 In case even that is not enough, you may supply a custom check by passing a
739 function that will be given the allocation objects.
740
741 Note that both whitelists and constraints do not influence the order of
742 allocations, which is done purely from the initial parameters. They only serve
743 to reject allocations made in good faith which do fulfil required assertions.
744
745 =head1 ERROR HANDLING
746
747 C<allocate> and C<produce_assembly> will throw exceptions if the request can
748 not be completed. The usual reason will be insufficient onhand to allocate, or
749 insufficient allocations to process the request.
750
751 =head1 KNOWN PROBLEMS
752
753   * It's not currently possible to identify allocations between requests, for
754     example for presenting the user possible allocations and then actually using
755     them on the next request.
756   * It's not currently possible to give C<allocate> prior constraints.
757     Currently all constraints are treated as hints (and will be preferred) but
758     the internal ordering of the hints is fixed and more complex preferentials
759     are not supported.
760   * bestbefore handling is untested
761   * interaction with config option "transfer_default_ignore_onhand" is
762     currently undefined (and implicitly ignores it)
763
764 =head1 TODO
765
766   * define and describe error classes
767   * define wrapper classes for stock/onhand batch mode return values
768   * handle extra arguments in produce: shippingdate, project
769   * document no_ check
770   * tests
771
772 =head1 BUGS
773
774 None yet :)
775
776 =head1 AUTHOR
777
778 Sven Schöling E<lt>sven.schoeling@googlemail.comE<gt>
779
780 =cut