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(_round_qty _qty);
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 my $part = $params{part} or Carp::croak('allocate needs a part');
171 my $qty = $params{qty} or Carp::croak('allocate needs a qty');
173 return () if $qty <= 0;
175 my $results = get_stock(part => $part, by => 'for_allocate');
176 my %bin_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{bin});
177 my %wh_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{warehouse});
178 my %chargenumbers = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{chargenumber});
179 my %reserve_whitelist;
180 if ($params{reserve_for}) {
181 $reserve_whitelist{ $_->meta->table }{ $_->id } = 1 for listify($params{reserve_for});
184 # filter the results. we don't want:
186 # - bins that are reserve but not in the white-list of warehouses or bins
187 # - reservations that are not white-listed
189 my @filtered_results = grep {
190 (!$_->{forreserve} || $bin_whitelist{$_->{bin_id}} || $wh_whitelist{$_->{warehouse_id}})
191 && (!$_->{reserve_for_id} || $reserve_whitelist{ $_->{reserve_for_table} }{ $_->{reserve_for_id} })
194 # sort results so that reserve_for is first, then chargenumbers, then wanted bins, then wanted warehouses
195 my @sorted_results = sort {
196 (!!$b->{reserve_for_id}) <=> (!!$a->{reserve_for_id}) # sort by existing reserve_for_id first.
197 || exists $chargenumbers{$b->{chargenumber}} <=> exists $chargenumbers{$a->{chargenumber}} # then prefer wanted chargenumbers
198 || exists $bin_whitelist{$b->{bin_id}} <=> exists $bin_whitelist{$a->{bin_id}} # then prefer wanted bins
199 || exists $wh_whitelist{$b->{warehouse_id}} <=> exists $wh_whitelist{$a->{warehouse_id}} # then prefer wanted bins
200 || $a->{itime} <=> $b->{itime} # and finally prefer earlier charges
205 for my $chunk (@sorted_results) {
206 my $qty = min($chunk->{qty}, $rest_qty);
208 push @allocations, SL::Helper::Inventory::Allocation->new(
209 parts_id => $chunk->{parts_id},
211 comment => $params{comment},
212 bin_id => $chunk->{bin_id},
213 warehouse_id => $chunk->{warehouse_id},
214 chargenumber => $chunk->{chargenumber},
215 bestbefore => $chunk->{bestbefore},
216 reserve_for_id => $chunk->{reserve_for_id},
217 reserve_for_table => $chunk->{reserve_for_table},
218 for_object_id => undef,
220 $rest_qty -= _round_qty($qty);
222 $rest_qty = _round_qty($rest_qty);
223 last if $rest_qty == 0;
226 die SL::X::Inventory::Allocation->new(
227 error => 'not enough to allocate',
228 msg => t8("can not allocate #1 units of #2, missing #3 units", _qty($qty), $part->displayable_name, _qty($rest_qty)),
231 if ($params{constraints}) {
232 check_constraints($params{constraints},\@allocations);
238 sub allocate_for_assembly {
241 my $part = $params{part} or Carp::croak('allocate needs a part');
242 my $qty = $params{qty} or Carp::croak('allocate needs a qty');
244 Carp::croak('not an assembly') unless $part->is_assembly;
246 my %parts_to_allocate;
248 for my $assembly ($part->assemblies) {
249 next if $assembly->part->dispotype eq 'no_stock';
251 my $tmpqty = $assembly->assembly_part->is_recipe ? $assembly->qty * $qty / $assembly->assembly_part->scalebasis
252 : $assembly->part->unit eq 'Stck' ? ceil($assembly->qty * $qty)
253 : $assembly->qty * $qty;
254 $parts_to_allocate{ $assembly->part->id } //= 0;
255 $parts_to_allocate{ $assembly->part->id } += $tmpqty;
260 for my $part_id (keys %parts_to_allocate) {
261 my $part = SL::DB::Part->load_cached($part_id);
262 push @allocations, allocate(%params, part => $part, qty => $parts_to_allocate{$part_id});
268 sub check_constraints {
269 my ($constraints, $allocations) = @_;
270 if ('CODE' eq ref $constraints) {
271 if (!$constraints->(@$allocations)) {
272 die SL::X::Inventory::Allocation->new(
273 error => 'allocation constraints failure',
274 msg => t8("Allocations didn't pass constraints"),
278 croak 'constraints needs to be a hashref' unless 'HASH' eq ref $constraints;
280 my %supported_constraints = (
282 warehouse_id => 'warehouse_id',
283 chargenumber => 'chargenumber',
284 reserve_for => 'reserve_for_id',
287 for (keys %$constraints ) {
288 croak "unsupported constraint '$_'" unless $supported_constraints{$_};
289 next unless defined $constraints->{$_};
291 my %whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($constraints->{$_});
292 my $accessor = $supported_constraints{$_};
294 if (any { !$whitelist{$_->$accessor} } @$allocations) {
295 my %error_constraints = (
296 bin_id => t8('Bins'),
297 warehouse_id => t8('Warehouses'),
298 chargenumber => t8('Chargenumbers'),
299 reserve_for => t8('Reserve For'),
301 my @allocs = grep { $whitelist{$_->$accessor} } @$allocations;
302 my $needed = sum map { $_->qty } grep { !$whitelist{$_->$accessor} } @$allocations;
303 my $err = t8("Cannot allocate parts.");
304 $err .= ' '.t8('part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.',
305 SL::DB::Part->load_cached($_->parts_id)->description,
306 SL::DB::Bin->load_cached($_->bin_id)->full_description,
307 _qty($_->qty), _qty($needed), $_->chargenumber ? $_->chargenumber : '--') for @allocs;
308 die SL::X::Inventory::Allocation->new(
309 error => 'allocation constraints failure',
317 sub produce_assembly {
320 my $part = $params{part} or Carp::croak('produce_assembly needs a part');
321 my $qty = $params{qty} or Carp::croak('produce_assembly needs a qty');
323 my $allocations = $params{allocations};
324 if ($params{auto_allocate}) {
325 Carp::croak("produce_assembly: can't have both allocations and auto_allocate") if $params{allocations};
326 $allocations = [ allocate_for_assembly(part => $part, qty => $qty) ];
328 Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations};
329 $allocations = $params{allocations};
332 my $bin = $params{bin} or Carp::croak("need target bin");
333 my $chargenumber = $params{chargenumber};
334 my $bestbefore = $params{bestbefore};
335 my $for_object_id = $params{for_object_id};
336 my $comment = $params{comment} // '';
338 my $production_order_item = $params{production_order_item};
339 my $invoice = $params{invoice};
340 my $project = $params{project};
341 my $reserve_for = $params{reserve_for};
343 my $reserve_for_id = $reserve_for ? $reserve_for->id : undef;
344 my $reserve_for_table = $reserve_for ? $reserve_for->meta->table : undef;
346 my $shippingdate = $params{shippingsdate} // DateTime->now_local;
348 my $trans_id = $params{trans_id};
349 ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id;
351 my $trans_type_out = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used');
352 my $trans_type_in = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'assembled');
354 # check whether allocations are sane
355 if (!$params{no_check_allocations} && !$params{auto_allocate}) {
356 my %allocations_by_part = map { $_->parts_id => $_->qty } @$allocations;
357 for my $assembly ($part->assemblies) {
358 $allocations_by_part{ $assembly->parts_id } -= $assembly->qty * $qty; # TODO recipe factor
361 die "allocations are insufficient for production" if any { $_ < 0 } values %allocations_by_part;
365 for my $allocation (@$allocations) {
366 my $oe_id = delete $allocation->{for_object_id};
367 push @transfers, SL::DB::Inventory->new(
368 trans_id => $trans_id,
370 qty => -$allocation->qty,
371 trans_type => $trans_type_out,
372 shippingdate => $shippingdate,
373 employee => SL::DB::Manager::Employee->current,
374 oe_id => $allocation->for_object_id,
378 push @transfers, SL::DB::Inventory->new(
379 trans_id => $trans_id,
380 trans_type => $trans_type_in,
384 warehouse => $bin->warehouse_id,
385 chargenumber => $chargenumber,
386 bestbefore => $bestbefore,
387 reserve_for_id => $reserve_for_id,
388 reserve_for_table => $reserve_for_table,
389 shippingdate => $shippingdate,
393 prod => $production_order_item,
394 employee => SL::DB::Manager::Employee->current,
395 oe_id => $for_object_id,
398 SL::DB->client->with_transaction(sub {
399 $_->save for @transfers;
402 die SL::DB->client->error;
408 package SL::Helper::Inventory::Allocation {
409 my @attributes = qw(parts_id qty bin_id warehouse_id chargenumber bestbefore comment reserve_for_id reserve_for_table for_object_id);
410 my %attributes = map { $_ => 1 } @attributes;
412 for my $name (@attributes) {
414 *{"SL::Helper::Inventory::Allocation::$name"} = sub { $_[0]{$name} };
418 my ($class, %params) = @_;
420 Carp::croak("missing attribute $_") for grep { !exists $params{$_} } @attributes;
421 Carp::croak("unknown attribute $_") for grep { !exists $attributes{$_} } keys %params;
422 Carp::croak("$_ must be set") for grep { !$params{$_} } qw(parts_id qty bin_id);
423 Carp::croak("$_ must be positive") for grep { !($params{$_} > 0) } qw(parts_id qty bin_id);
425 bless { %params }, $class;
435 SL::WH - Warehouse and Inventory API
439 # See description for an intro to the concepts used here.
441 use SL::Helper::Inventory qw(:ALL);
443 # stock, get "what's there" for a part with various conditions:
444 my $qty = get_stock(part => $part); # how much is on stock?
445 my $qty = get_stock(part => $part, date => $date); # how much was on stock at a specific time?
446 my $qty = get_stock(part => $part, bin => $bin); # how is on stock in a specific bin?
447 my $qty = get_stock(part => $part, warehouse => $warehouse); # how is on stock in a specific warehouse?
448 my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how is on stock of a specific chargenumber?
450 # onhand, get "what's available" for a part with various conditions:
451 my $qty = get_onhand(part => $part); # how much is available?
452 my $qty = get_onhand(part => $part, date => $date); # how much was available at a specific time?
453 my $qty = get_onhand(part => $part, bin => $bin); # how much is available in a specific bin?
454 my $qty = get_onhand(part => $part, warehouse => $warehouse); # how much is available in a specific warehouse?
455 my $qty = get_onhand(part => $part, chargenumber => $chargenumber); # how much is availbale of a specific chargenumber?
456 my $qty = get_onhand(part => $part, reserve_for => $order); # how much is available if you include this reservation?
459 my $data = get_onhand(
460 warehouse => $warehouse,
461 by => [ qw(bin part chargenumber reserve_for) ],
462 with_objects => [ qw(bin part) ],
466 my @allocations, allocate(
467 part => $part, # part_id works too
468 qty => $qty, # must be positive
469 chargenumber => $chargenumber, # optional, may be arrayref. if provided these charges will be used first
470 bestbefore => $datetime, # optional, defaults to today. items with bestbefore prior to that date wont be used
471 reserve_for => $object, # optional, may be arrayref. if provided the qtys reserved for these objects will be used first
472 bin => $bin, # optional, may be arrayref. if provided
475 # shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate
476 my @allocations, allocate_for_assembly(
477 part => $assembly, # part_id works too
478 qty => $qty, # must be positive
481 # create allocation manually, bypassing checks, all of these need to be passed, even undefs
482 my $allocation = SL::Helper::Inventory::Allocation->new(
483 part_id => $part->id,
485 bin_id => $bin_obj->id,
486 warehouse_id => $bin_obj->warehouse_id,
487 chargenumber => '1823772365',
489 reserve_for_id => undef,
490 reserve_for_table => undef,
491 for_object_id => $order->id,
496 part => $part, # target assembly
498 allocations => \@allocations, # allocations to use. alternatively use "auto_allocate => 1,"
501 bin => $bin, # needed unless a global standard target is configured
502 chargenumber => $chargenumber, # optional
503 bestbefore => $datetime, # optional
504 comment => $comment, # optional
506 # links, all optional
507 production_order_item => $item,
508 reserve_for => $object,
513 New functions for the warehouse and inventory api.
515 The WH api currently has three large shortcomings. It is very hard to just get
516 the current stock for an item, it's extremely complicated to use it to produce
517 assemblies while ensuring that no stock ends up negative, and it's very hard to
518 use it to get an overview over the actual contents of the inventory.
520 The first problem has spawned several dozen small functions in the program that
521 try to implement that, and those usually miss some details. They may ignore
522 reservations, or reserve warehouses, or bestbefore times.
524 To get this cleaned up a bit this code introduces two concepts: stock and onhand.
526 Stock is defined as the actual contents of the inventory, everything that is
527 there. Onhand is what is available, which means things that are stocked and not
528 reserved and not expired.
530 The two new functions C<get_stock> and C<get_onhand> encapsulate these principles and
531 allow simple access with some optional filters for chargenumbers or warehouses.
532 Both of them have a batch mode that can be used to get these information to
533 supllement smiple reports.
535 To address the safe assembly creation a new function has been added.
536 C<allocate> will try to find the requested quantity of a part in the inventory
537 and will return allocations of it which can then be used to create the
538 assembly. Allocation will happen with the C<onhand> semantics defined above,
539 meaning that by default no reservations or expired goods will be used. The
540 caller can supply hints of what shold be used and in those cases chargenumber
541 and reservations will be used up as much as possible first. C<allocate> will
542 always try to fulfil the request even beyond those. Should the required amount
543 not be stocked, allocate will throw an exception.
545 C<produce_assembly> has been rewritten to only accept parameters about the
546 target of the production, and requires allocations to complete the request. The
547 allocations can be supplied manually, or can be generated automatically.
548 C<produce_assembly> will check whether enough allocations are given to create
549 the recipe, but will not check whether the allocations are backed. If the
550 allocations are not sufficient or if the auto-allocation fails an exception
551 is returned. If you need to produce something that is not in the inventory, you
552 can bypass those checks by creating the allocations yourself (see
553 L</"ALLOCATION DATA STRUCTURE">).
555 Note: this is only intended to cover the scenarios described above. For other cases:
561 If you need the reserved amount for an order use C<SL::DB::Helper::Reservation>
566 If you need actual inventory objects because of record links, prod_id links or
567 something like that load them directly. And strongly consider redesigning that,
568 because it's really fragile.
572 You need weight or accounting information you're on your own. The inventory api
573 only concerns itself with the raw quantities.
577 If you need the first stock date of parts, or anything related to a specific
578 transfer type or direction, this is not covered yet.
586 =item * get_stock PARAMS
588 Returns for single parts how much actually exists in the inventory.
596 The part. Must be present without C<by>. May be arrayref with C<by>. Can be object or id.
600 If given, will only return stock on these bins. Optional. May be array, May be object or id.
604 If given, will only return stock on these warehouses. Optional. May be array, May be object or id.
608 If given, will return stock as it were on this timestamp. Optional. Must be L<DateTime> object.
612 If given, will only show stock with this chargenumber. Optional. May be array.
616 See L</"STOCK/ONHAND REPORT MODE">
620 See L</"STOCK/ONHAND REPORT MODE">
624 Will return a single qty normally, see L</"STOCK/ONHAND REPORT MODE"> for batch
625 mode when C<by> is given.
627 =item * get_onhand PARAMS
629 Returns for single parts how much is available in the inventory. That excludes:
630 reserved quantities, reserved warehouses and stock with expired bestbefore.
632 It takes all options of L</get_stock> but treats some of the differently and has some additional ones:
638 Usually C<onhand> will not include results from warehouses with the C<reserve>
639 flag. However giving an explicit list of warehouses will include there in the
640 search, as well as all others.
644 =item * reserve_warehouse
650 =item * allocate PARAMS
662 Bin object. Optional.
666 Warehouse object. Optional.
678 Needs to be a rose object, where id and table can be extracted. Optional.
682 Tries to allocate the required quantity using what is currently onhand. If
683 given any of C<bin>, C<warehouse>, C<chargenumber>, C<reserve_for>
686 =item * allocate_for_assembly PARAMS
688 Shortcut to allocate everything for an assembly. Takes the same arguments. Will
689 compute the required amount for each assembly part and allocate all of them.
691 =item * produce_assembly
696 =head1 STOCK/ONHAND REPORT MODE
698 If the special option C<by> is given with an arrayref, the result will instead
699 be an arrayref of partitioned stocks by those fields. Valid partitions are:
705 If this is given, part is optional in the parameters
719 Note: If you want to use the returned data to create allocations you I<need> to
720 enable all of these. To make this easier a special shortcut exists
722 In this mode, C<with_objects> can be used to load C<warehouse>, C<bin>,
723 C<parts>, and the C<reserve_for> objects in one go, just like with Rose. They
724 need to be present in C<by> before that though.
726 =head1 ALLOCATION ALGORITHM
728 When calling allocate, the current onhand (== available stock) of the item will
729 be used to decide which bins/chargenumbers/bestbefore can be used.
731 In general allocate will try to make the request happen, and will use the
732 provided charges up first, and then tap everything else. If you need to only
733 I<exactly> use the provided charges, you'll need to craft the allocations
734 yourself. See L</"ALLOCATION DATA STRUCTURE"> for that.
736 If C<reserve_for> is given, those will be used up first too.
738 If C<reserved_warehouse> is given, those will be used up second.
740 If C<chargenumber> is given, those will be used up next.
742 After that normal quantities will be used.
744 These are tiebreakers and expected to rarely matter in reality. If you need
745 finegrained control over which allocation is used, you may want to get the
746 onhands yourself and select the appropriate ones.
748 Only quantities with C<bestbefore> unset or after the given date will be
749 considered. If more than one charge is eligible, the earlier C<bestbefore>
752 Allocations do NOT have an internal memory and can't react to other allocations
753 of the same part earlier. Never double allocate the same part within a
756 =head1 ALLOCATION DATA STRUCTURE
758 Allocations are instances of the helper class C<SL::Helper::Inventory::Allocation>. They require
759 each of the following attributes to be set at creation time:
775 =item * reserve_for_id
777 =item * reserve_for_table
779 =item * for_object_id
781 If set the allocations will be marked as allocated for the given object.
782 If these allocations are later used to produce an assembly, the resulting
783 consuming transactions will be marked as belonging to the given object.
784 The object may be an order, productionorder or other objects
788 C<chargenumber>, C<bestbefore>, C<reserve_for_id>, C<reserve_for_table> and
789 C<for_object_id> may be C<undef> (but must still be present at creation time).
790 Instances are considered immutable.
795 # whitelist constraints
799 bin_id => \@allowed_bins,
800 chargenumber => \@allowed_chargenumbers,
807 # only allow chargenumbers with specific format
808 all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_
811 # and must be all reservations
812 all { $_->reserve_for_id } @_;
816 C<allocation> is "best effort" in nature. It will take the C<bin>,
817 C<chargenumber> etc hints from the parameters, but will try it's bvest to
818 fulfil the request anyway and only bail out if it is absolutely not possible.
820 Sometimes you need to restrict allocations though. For this you can pass
821 additional constraints to C<allocate>. A constraint serves as a whitelist.
822 Every allocation must fulfil every constraint by having that attribute be one
825 In case even that is not enough, you may supply a custom check by passing a
826 function that will be given the allocation objects.
828 Note that both whitelists and constraints do not influence the order of
829 allocations, which is done purely from the initial parameters. They only serve
830 to reject allocations made in good faith which do fulfil required assertions.
832 =head1 ERROR HANDLING
834 C<allocate> and C<produce_assembly> will throw exceptions if the request can
835 not be completed. The usual reason will be insufficient onhand to allocate, or
836 insufficient allocations to process the request.
840 * define and describe error classes
841 * define wrapper classes for stock/onhand batch mode return values
842 * handle extra arguments in produce: shippingdate, project, oe
843 * clean up allocation helper class
844 * with objects for reservations
854 Sven Schöling E<lt>sven.schoeling@opendynamic.deE<gt>