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