1 package SL::Helper::Inventory;
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);
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);
19 our @EXPORT_OK = qw(get_stock get_onhand allocate allocate_for_assembly produce_assembly check_constraints);
20 our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
22 sub _get_stock_onhand {
25 my $onhand_mode = !!$params{onhand};
29 'MIN(EXTRACT(epoch FROM inventory.itime)) AS itime',
36 my @ids = map { ref $_ ? $_->id : $_ } listify($params{part});
37 push @where, sprintf "parts_id IN (%s)", join ', ', ("?") x @ids;
42 my @ids = map { ref $_ ? $_->id : $_ } listify($params{bin});
43 push @where, sprintf "bin_id IN (%s)", join ', ', ("?") x @ids;
47 if ($params{warehouse}) {
48 my @ids = map { ref $_ ? $_->id : $_ } listify($params{warehouse});
49 push @where, sprintf "warehouse.id IN (%s)", join ', ', ("?") x @ids;
53 if ($params{chargenumber}) {
54 my @ids = listify($params{chargenumber});
55 push @where, sprintf "chargenumber IN (%s)", join ', ', ("?") x @ids;
60 Carp::croak("not DateTime ".$params{date}) unless ref($params{date}) eq 'DateTime';
61 push @where, sprintf "shippingdate <= ?";
62 push @values, $params{date};
65 if ($params{bestbefore}) {
66 Carp::croak("not DateTime ".$params{date}) unless ref($params{bestbefore}) eq 'DateTime';
67 push @where, sprintf "bestbefore >= ?";
68 push @values, $params{bestbefore};
72 if ($params{onhand} && !$params{warehouse}) {
73 push @where, 'NOT warehouse.forreserve';
77 if ($params{onhand} && !$params{reserve_for}) {
78 push @where, 'reserve_for_id IS NULL AND reserve_for_table IS NULL';
81 if ($params{reserve_for}) {
82 my @objects = listify($params{reserve_for});
84 push @tokens, ( "(reserve_for_id = ? AND reserve_for_table = ?)") x @objects;
85 push @values, map { ($_->id, $_->meta->table) } @objects;
86 push @where, '(' . join(' OR ', @tokens) . ')';
91 part => [ qw(parts_id) ],
92 bin => [ qw(bin_id inventory.warehouse_id warehouse.forreserve)],
93 warehouse => [ qw(inventory.warehouse_id warehouse.forreserve) ],
94 chargenumber => [ qw(chargenumber) ],
95 bestbefore => [ qw(bestbefore) ],
96 reserve_for => [ qw(reserve_for_id reserve_for_table) ],
97 for_allocate => [ qw(parts_id bin_id inventory.warehouse_id warehouse.forreserve chargenumber bestbefore reserve_for_id reserve_for_table) ],
101 for (listify($params{by})) {
102 my $selects = $allowed_by{$_} or Carp::croak("unknown option for by: $_");
103 push @selects, @$selects;
104 push @groups, @$selects;
108 my $select = join ',', @selects;
109 my $where = @where ? 'WHERE ' . join ' AND ', @where : '';
110 my $group_by = @groups ? 'GROUP BY ' . join ', ', @groups : '';
113 SELECT $select FROM inventory
114 LEFT JOIN bin ON bin_id = bin.id
115 LEFT JOIN warehouse ON bin.warehouse_id = warehouse.id
120 my $results = selectall_hashref_query($::form, SL::DB->client->dbh, $query, @values);
123 part => 'SL::DB::Manager::Part',
124 bin => 'SL::DB::Manager::Bin',
125 warehouse => 'SL::DB::Manager::Warehouse',
126 reserve_for => undef,
132 warehouse => 'warehouse_id',
135 if ($params{by} && $params{with_objects}) {
136 for my $with_object (listify($params{with_objects})) {
137 Carp::croak("unknown with_object $with_object") if !exists $with_objects{$with_object};
139 if (my $manager = $with_objects{$with_object}) {
140 my $slot = $slots{$with_object};
141 next if !(my @ids = map { $_->{$slot} } @$results);
142 my $objects = $manager->get_all(query => [ id => \@ids ]);
143 my %objects_by_id = map { $_->id => $_ } @$objects;
145 $_->{$with_object} = $objects_by_id{$_->{$slot}} for @$results;
147 # need to fetch all reserve_for_table partitions
155 return $results->[0]{qty};
160 _get_stock_onhand(@_, onhand => 0);
164 _get_stock_onhand(@_, onhand => 1);
170 die SL::X::Inventory::Allocation->new(
171 error => 'allocate needs a part',
172 msg => t8("Method allocate needs the parameter 'part'"),
173 ) unless $params{part};
174 die SL::X::Inventory::Allocation->new(
175 error => 'allocate needs a qty',
176 msg => t8("Method allocate needs the parameter 'qty'"),
177 ) unless $params{qty};
179 my $part = $params{part};
180 my $qty = $params{qty};
182 return () if $qty <= 0;
184 my $results = get_stock(part => $part, by => 'for_allocate');
185 my %bin_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{bin});
186 my %wh_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{warehouse});
187 my %chargenumbers = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{chargenumber});
188 my %reserve_whitelist;
189 if ($params{reserve_for}) {
190 $reserve_whitelist{ $_->meta->table }{ $_->id } = 1 for listify($params{reserve_for});
193 # filter the results. we don't want:
195 # - bins that are reserve but not in the white-list of warehouses or bins
196 # - reservations that are not white-listed
198 my @filtered_results = grep {
199 (!$_->{forreserve} || $bin_whitelist{$_->{bin_id}} || $wh_whitelist{$_->{warehouse_id}})
200 && (!$_->{reserve_for_id} || $reserve_whitelist{ $_->{reserve_for_table} }{ $_->{reserve_for_id} })
203 # sort results so that reserve_for is first, then chargenumbers, then wanted bins, then wanted warehouses
204 my @sorted_results = sort {
205 (!!$b->{reserve_for_id}) <=> (!!$a->{reserve_for_id}) # sort by existing reserve_for_id first.
206 || exists $chargenumbers{$b->{chargenumber}} <=> exists $chargenumbers{$a->{chargenumber}} # then prefer wanted chargenumbers
207 || exists $bin_whitelist{$b->{bin_id}} <=> exists $bin_whitelist{$a->{bin_id}} # then prefer wanted bins
208 || exists $wh_whitelist{$b->{warehouse_id}} <=> exists $wh_whitelist{$a->{warehouse_id}} # then prefer wanted bins
209 || $a->{itime} <=> $b->{itime} # and finally prefer earlier charges
214 for my $chunk (@sorted_results) {
215 my $qty = min($chunk->{qty}, $rest_qty);
217 push @allocations, SL::Helper::Inventory::Allocation->new(
218 parts_id => $chunk->{parts_id},
220 comment => $params{comment},
221 bin_id => $chunk->{bin_id},
222 warehouse_id => $chunk->{warehouse_id},
223 chargenumber => $chunk->{chargenumber},
224 bestbefore => $chunk->{bestbefore},
225 reserve_for_id => $chunk->{reserve_for_id},
226 reserve_for_table => $chunk->{reserve_for_table},
227 for_object_id => undef,
229 $rest_qty -= _round_number($qty, 5);
231 $rest_qty = _round_number($rest_qty, 5);
232 last if $rest_qty == 0;
235 die SL::X::Inventory::Allocation->new(
236 error => 'not enough to allocate',
237 msg => t8("can not allocate #1 units of #2, missing #3 units", _number(\%::myconfig, $qty), $part->displayable_name, _number(\%::myconfig, $rest_qty)),
240 if ($params{constraints}) {
241 check_constraints($params{constraints},\@allocations);
247 sub allocate_for_assembly {
250 my $part = $params{part} or Carp::croak('allocate needs a part');
251 my $qty = $params{qty} or Carp::croak('allocate needs a qty');
253 Carp::croak('not an assembly') unless $part->is_assembly;
255 my %parts_to_allocate;
257 for my $assembly ($part->assemblies) {
258 next if $assembly->part->dispotype eq 'no_stock';
260 my $tmpqty = $assembly->assembly_part->is_recipe ? $assembly->qty * $qty / $assembly->assembly_part->scalebasis
261 : $assembly->part->unit eq 'Stck' ? ceil($assembly->qty * $qty)
262 : $assembly->qty * $qty;
263 $parts_to_allocate{ $assembly->part->id } //= 0;
264 $parts_to_allocate{ $assembly->part->id } += $tmpqty;
269 for my $part_id (keys %parts_to_allocate) {
270 my $part = SL::DB::Part->load_cached($part_id);
271 push @allocations, allocate(%params, part => $part, qty => $parts_to_allocate{$part_id});
277 sub check_constraints {
278 my ($constraints, $allocations) = @_;
279 if ('CODE' eq ref $constraints) {
280 if (!$constraints->(@$allocations)) {
281 die SL::X::Inventory::Allocation->new(
282 error => 'allocation constraints failure',
283 msg => t8("Allocations didn't pass constraints"),
287 croak 'constraints needs to be a hashref' unless 'HASH' eq ref $constraints;
289 my %supported_constraints = (
291 warehouse_id => 'warehouse_id',
292 chargenumber => 'chargenumber',
293 reserve_for => 'reserve_for_id',
296 for (keys %$constraints ) {
297 croak "unsupported constraint '$_'" unless $supported_constraints{$_};
298 next unless defined $constraints->{$_};
300 my %whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($constraints->{$_});
301 my $accessor = $supported_constraints{$_};
303 if (any { !$whitelist{$_->$accessor} } @$allocations) {
304 my %error_constraints = (
305 bin_id => t8('Bins'),
306 warehouse_id => t8('Warehouses'),
307 chargenumber => t8('Chargenumbers'),
308 reserve_for => t8('Reserve For'),
310 my @allocs = grep { $whitelist{$_->$accessor} } @$allocations;
311 my $needed = sum map { $_->qty } grep { !$whitelist{$_->$accessor} } @$allocations;
312 my $err = t8("Cannot allocate parts.");
313 $err .= ' '.t8('part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.',
314 SL::DB::Part->load_cached($_->parts_id)->description,
315 SL::DB::Bin->load_cached($_->bin_id)->full_description,
316 _number($_->qty), _number($needed), $_->chargenumber ? $_->chargenumber : '--') for @allocs;
317 die SL::X::Inventory::Allocation->new(
318 error => 'allocation constraints failure',
326 sub produce_assembly {
329 my $part = $params{part} or Carp::croak('produce_assembly needs a part');
330 my $qty = $params{qty} or Carp::croak('produce_assembly needs a qty');
332 my $allocations = $params{allocations};
333 if ($params{auto_allocate}) {
334 Carp::croak("produce_assembly: can't have both allocations and auto_allocate") if $params{allocations};
335 $allocations = [ allocate_for_assembly(part => $part, qty => $qty) ];
337 Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations};
338 $allocations = $params{allocations};
341 my $bin = $params{bin} or Carp::croak("need target bin");
342 my $chargenumber = $params{chargenumber};
343 my $bestbefore = $params{bestbefore};
344 my $for_object_id = $params{for_object_id};
345 my $comment = $params{comment} // '';
347 my $production_order_item = $params{production_order_item};
348 my $invoice = $params{invoice};
349 my $project = $params{project};
350 my $reserve_for = $params{reserve_for};
352 my $reserve_for_id = $reserve_for ? $reserve_for->id : undef;
353 my $reserve_for_table = $reserve_for ? $reserve_for->meta->table : undef;
355 my $shippingdate = $params{shippingsdate} // DateTime->now_local;
357 my $trans_id = $params{trans_id};
358 ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id;
360 my $trans_type_out = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used');
361 my $trans_type_in = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'assembled');
363 # check whether allocations are sane
364 if (!$params{no_check_allocations} && !$params{auto_allocate}) {
365 my %allocations_by_part = map { $_->parts_id => $_->qty } @$allocations;
366 for my $assembly ($part->assemblies) {
367 $allocations_by_part{ $assembly->parts_id } -= $assembly->qty * $qty; # TODO recipe factor
370 die "allocations are insufficient for production" if any { $_ < 0 } values %allocations_by_part;
374 for my $allocation (@$allocations) {
375 my $oe_id = delete $allocation->{for_object_id};
376 push @transfers, SL::DB::Inventory->new(
377 trans_id => $trans_id,
379 qty => -$allocation->qty,
380 trans_type => $trans_type_out,
381 shippingdate => $shippingdate,
382 employee => SL::DB::Manager::Employee->current,
383 oe_id => $allocation->for_object_id,
387 push @transfers, SL::DB::Inventory->new(
388 trans_id => $trans_id,
389 trans_type => $trans_type_in,
393 warehouse => $bin->warehouse_id,
394 chargenumber => $chargenumber,
395 bestbefore => $bestbefore,
396 reserve_for_id => $reserve_for_id,
397 reserve_for_table => $reserve_for_table,
398 shippingdate => $shippingdate,
402 prod => $production_order_item,
403 employee => SL::DB::Manager::Employee->current,
404 oe_id => $for_object_id,
407 SL::DB->client->with_transaction(sub {
408 $_->save for @transfers;
411 die SL::DB->client->error;
417 package SL::Helper::Inventory::Allocation {
418 my @attributes = qw(parts_id qty bin_id warehouse_id chargenumber bestbefore comment reserve_for_id reserve_for_table for_object_id);
419 my %attributes = map { $_ => 1 } @attributes;
421 for my $name (@attributes) {
423 *{"SL::Helper::Inventory::Allocation::$name"} = sub { $_[0]{$name} };
427 my ($class, %params) = @_;
429 Carp::croak("missing attribute $_") for grep { !exists $params{$_} } @attributes;
430 Carp::croak("unknown attribute $_") for grep { !exists $attributes{$_} } keys %params;
431 Carp::croak("$_ must be set") for grep { !$params{$_} } qw(parts_id qty bin_id);
432 Carp::croak("$_ must be positive") for grep { !($params{$_} > 0) } qw(parts_id qty bin_id);
434 bless { %params }, $class;
444 SL::WH - Warehouse and Inventory API
448 # See description for an intro to the concepts used here.
450 use SL::Helper::Inventory qw(:ALL);
452 # stock, get "what's there" for a part with various conditions:
453 my $qty = get_stock(part => $part); # how much is on stock?
454 my $qty = get_stock(part => $part, date => $date); # how much was on stock at a specific time?
455 my $qty = get_stock(part => $part, bin => $bin); # how is on stock in a specific bin?
456 my $qty = get_stock(part => $part, warehouse => $warehouse); # how is on stock in a specific warehouse?
457 my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how is on stock of a specific chargenumber?
459 # onhand, get "what's available" for a part with various conditions:
460 my $qty = get_onhand(part => $part); # how much is available?
461 my $qty = get_onhand(part => $part, date => $date); # how much was available at a specific time?
462 my $qty = get_onhand(part => $part, bin => $bin); # how much is available in a specific bin?
463 my $qty = get_onhand(part => $part, warehouse => $warehouse); # how much is available in a specific warehouse?
464 my $qty = get_onhand(part => $part, chargenumber => $chargenumber); # how much is availbale of a specific chargenumber?
465 my $qty = get_onhand(part => $part, reserve_for => $order); # how much is available if you include this reservation?
468 my $data = get_onhand(
469 warehouse => $warehouse,
470 by => [ qw(bin part chargenumber reserve_for) ],
471 with_objects => [ qw(bin part) ],
475 my @allocations, allocate(
476 part => $part, # part_id works too
477 qty => $qty, # must be positive
478 chargenumber => $chargenumber, # optional, may be arrayref. if provided these charges will be used first
479 bestbefore => $datetime, # optional, defaults to today. items with bestbefore prior to that date wont be used
480 reserve_for => $object, # optional, may be arrayref. if provided the qtys reserved for these objects will be used first
481 bin => $bin, # optional, may be arrayref. if provided
484 # shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate
485 my @allocations, allocate_for_assembly(
486 part => $assembly, # part_id works too
487 qty => $qty, # must be positive
490 # create allocation manually, bypassing checks, all of these need to be passed, even undefs
491 my $allocation = SL::Helper::Inventory::Allocation->new(
492 part_id => $part->id,
494 bin_id => $bin_obj->id,
495 warehouse_id => $bin_obj->warehouse_id,
496 chargenumber => '1823772365',
498 reserve_for_id => undef,
499 reserve_for_table => undef,
500 for_object_id => $order->id,
505 part => $part, # target assembly
507 allocations => \@allocations, # allocations to use. alternatively use "auto_allocate => 1,"
510 bin => $bin, # needed unless a global standard target is configured
511 chargenumber => $chargenumber, # optional
512 bestbefore => $datetime, # optional
513 comment => $comment, # optional
515 # links, all optional
516 production_order_item => $item,
517 reserve_for => $object,
522 New functions for the warehouse and inventory api.
524 The WH api currently has three large shortcomings. It is very hard to just get
525 the current stock for an item, it's extremely complicated to use it to produce
526 assemblies while ensuring that no stock ends up negative, and it's very hard to
527 use it to get an overview over the actual contents of the inventory.
529 The first problem has spawned several dozen small functions in the program that
530 try to implement that, and those usually miss some details. They may ignore
531 reservations, or reserve warehouses, or bestbefore times.
533 To get this cleaned up a bit this code introduces two concepts: stock and onhand.
535 Stock is defined as the actual contents of the inventory, everything that is
536 there. Onhand is what is available, which means things that are stocked and not
537 reserved and not expired.
539 The two new functions C<get_stock> and C<get_onhand> encapsulate these principles and
540 allow simple access with some optional filters for chargenumbers or warehouses.
541 Both of them have a batch mode that can be used to get these information to
542 supllement smiple reports.
544 To address the safe assembly creation a new function has been added.
545 C<allocate> will try to find the requested quantity of a part in the inventory
546 and will return allocations of it which can then be used to create the
547 assembly. Allocation will happen with the C<onhand> semantics defined above,
548 meaning that by default no reservations or expired goods will be used. The
549 caller can supply hints of what shold be used and in those cases chargenumber
550 and reservations will be used up as much as possible first. C<allocate> will
551 always try to fulfil the request even beyond those. Should the required amount
552 not be stocked, allocate will throw an exception.
554 C<produce_assembly> has been rewritten to only accept parameters about the
555 target of the production, and requires allocations to complete the request. The
556 allocations can be supplied manually, or can be generated automatically.
557 C<produce_assembly> will check whether enough allocations are given to create
558 the recipe, but will not check whether the allocations are backed. If the
559 allocations are not sufficient or if the auto-allocation fails an exception
560 is returned. If you need to produce something that is not in the inventory, you
561 can bypass those checks by creating the allocations yourself (see
562 L</"ALLOCATION DATA STRUCTURE">).
564 Note: this is only intended to cover the scenarios described above. For other cases:
570 If you need the reserved amount for an order use C<SL::DB::Helper::Reservation>
575 If you need actual inventory objects because of record links, prod_id links or
576 something like that load them directly. And strongly consider redesigning that,
577 because it's really fragile.
581 You need weight or accounting information you're on your own. The inventory api
582 only concerns itself with the raw quantities.
586 If you need the first stock date of parts, or anything related to a specific
587 transfer type or direction, this is not covered yet.
595 =item * get_stock PARAMS
597 Returns for single parts how much actually exists in the inventory.
605 The part. Must be present without C<by>. May be arrayref with C<by>. Can be object or id.
609 If given, will only return stock on these bins. Optional. May be array, May be object or id.
613 If given, will only return stock on these warehouses. Optional. May be array, May be object or id.
617 If given, will return stock as it were on this timestamp. Optional. Must be L<DateTime> object.
621 If given, will only show stock with this chargenumber. Optional. May be array.
625 See L</"STOCK/ONHAND REPORT MODE">
629 See L</"STOCK/ONHAND REPORT MODE">
633 Will return a single qty normally, see L</"STOCK/ONHAND REPORT MODE"> for batch
634 mode when C<by> is given.
636 =item * get_onhand PARAMS
638 Returns for single parts how much is available in the inventory. That excludes:
639 reserved quantities, reserved warehouses and stock with expired bestbefore.
641 It takes all options of L</get_stock> but treats some of the differently and has some additional ones:
647 Usually C<onhand> will not include results from warehouses with the C<reserve>
648 flag. However giving an explicit list of warehouses will include there in the
649 search, as well as all others.
653 =item * reserve_warehouse
659 =item * allocate PARAMS
671 Bin object. Optional.
675 Warehouse object. Optional.
687 Needs to be a rose object, where id and table can be extracted. Optional.
691 Tries to allocate the required quantity using what is currently onhand. If
692 given any of C<bin>, C<warehouse>, C<chargenumber>, C<reserve_for>
695 =item * allocate_for_assembly PARAMS
697 Shortcut to allocate everything for an assembly. Takes the same arguments. Will
698 compute the required amount for each assembly part and allocate all of them.
700 =item * produce_assembly
705 =head1 STOCK/ONHAND REPORT MODE
707 If the special option C<by> is given with an arrayref, the result will instead
708 be an arrayref of partitioned stocks by those fields. Valid partitions are:
714 If this is given, part is optional in the parameters
728 Note: If you want to use the returned data to create allocations you I<need> to
729 enable all of these. To make this easier a special shortcut exists
731 In this mode, C<with_objects> can be used to load C<warehouse>, C<bin>,
732 C<parts>, and the C<reserve_for> objects in one go, just like with Rose. They
733 need to be present in C<by> before that though.
735 =head1 ALLOCATION ALGORITHM
737 When calling allocate, the current onhand (== available stock) of the item will
738 be used to decide which bins/chargenumbers/bestbefore can be used.
740 In general allocate will try to make the request happen, and will use the
741 provided charges up first, and then tap everything else. If you need to only
742 I<exactly> use the provided charges, you'll need to craft the allocations
743 yourself. See L</"ALLOCATION DATA STRUCTURE"> for that.
745 If C<reserve_for> is given, those will be used up first too.
747 If C<reserved_warehouse> is given, those will be used up second.
749 If C<chargenumber> is given, those will be used up next.
751 After that normal quantities will be used.
753 These are tiebreakers and expected to rarely matter in reality. If you need
754 finegrained control over which allocation is used, you may want to get the
755 onhands yourself and select the appropriate ones.
757 Only quantities with C<bestbefore> unset or after the given date will be
758 considered. If more than one charge is eligible, the earlier C<bestbefore>
761 Allocations do NOT have an internal memory and can't react to other allocations
762 of the same part earlier. Never double allocate the same part within a
765 =head1 ALLOCATION DATA STRUCTURE
767 Allocations are instances of the helper class C<SL::Helper::Inventory::Allocation>. They require
768 each of the following attributes to be set at creation time:
784 =item * reserve_for_id
786 =item * reserve_for_table
788 =item * for_object_id
790 If set the allocations will be marked as allocated for the given object.
791 If these allocations are later used to produce an assembly, the resulting
792 consuming transactions will be marked as belonging to the given object.
793 The object may be an order, productionorder or other objects
797 C<chargenumber>, C<bestbefore>, C<reserve_for_id>, C<reserve_for_table> and
798 C<for_object_id> may be C<undef> (but must still be present at creation time).
799 Instances are considered immutable.
804 # whitelist constraints
808 bin_id => \@allowed_bins,
809 chargenumber => \@allowed_chargenumbers,
816 # only allow chargenumbers with specific format
817 all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_
820 # and must be all reservations
821 all { $_->reserve_for_id } @_;
825 C<allocation> is "best effort" in nature. It will take the C<bin>,
826 C<chargenumber> etc hints from the parameters, but will try it's bvest to
827 fulfil the request anyway and only bail out if it is absolutely not possible.
829 Sometimes you need to restrict allocations though. For this you can pass
830 additional constraints to C<allocate>. A constraint serves as a whitelist.
831 Every allocation must fulfil every constraint by having that attribute be one
834 In case even that is not enough, you may supply a custom check by passing a
835 function that will be given the allocation objects.
837 Note that both whitelists and constraints do not influence the order of
838 allocations, which is done purely from the initial parameters. They only serve
839 to reject allocations made in good faith which do fulfil required assertions.
841 =head1 ERROR HANDLING
843 C<allocate> and C<produce_assembly> will throw exceptions if the request can
844 not be completed. The usual reason will be insufficient onhand to allocate, or
845 insufficient allocations to process the request.
849 * define and describe error classes
850 * define wrapper classes for stock/onhand batch mode return values
851 * handle extra arguments in produce: shippingdate, project, oe
852 * clean up allocation helper class
853 * with objects for reservations
863 Sven Schöling E<lt>sven.schoeling@opendynamic.deE<gt>