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};
27 my @selects = ('SUM(qty) as qty');
33 my @ids = map { ref $_ ? $_->id : $_ } listify($params{part});
34 push @where, sprintf "parts_id IN (%s)", join ', ', ("?") x @ids;
39 my @ids = map { ref $_ ? $_->id : $_ } listify($params{bin});
40 push @where, sprintf "bin_id IN (%s)", join ', ', ("?") x @ids;
44 if ($params{warehouse}) {
45 my @ids = map { ref $_ ? $_->id : $_ } listify($params{warehouse});
46 push @where, sprintf "warehouse.id IN (%s)", join ', ', ("?") x @ids;
50 if ($params{chargenumber}) {
51 my @ids = listify($params{chargenumber});
52 push @where, sprintf "chargenumber IN (%s)", join ', ', ("?") x @ids;
57 push @where, sprintf "shippingdate <= ?";
58 push @values, $params{date};
61 if ($params{bestbefore}) {
62 push @where, sprintf "bestbefore >= ?";
63 push @values, $params{bestbefore};
67 if ($params{onhand} && !$params{warehouse}) {
68 push @where, 'NOT warehouse.forreserve';
72 if ($params{onhand} && !$params{reserve_for}) {
73 push @where, 'reserve_for_id IS NULL AND reserve_for_table IS NULL';
76 if ($params{reserve_for}) {
77 my @objects = listify($params{chargenumber});
79 push @tokens, ( "(reserve_for_id = ? AND reserve_for_table = ?)") x @objects;
80 push @values, map { ($_->id, $_->meta->table) } @objects;
81 push @where, '(' . join(' OR ', @tokens) . ')';
86 part => [ qw(parts_id) ],
87 bin => [ qw(bin_id inventory.warehouse_id warehouse.forreserve)],
88 warehouse => [ qw(inventory.warehouse_id warehouse.forreserve) ],
89 chargenumber => [ qw(chargenumber) ],
90 bestbefore => [ qw(bestbefore) ],
91 reserve_for => [ qw(reserve_for_id reserve_for_table) ],
92 for_allocate => [ qw(parts_id bin_id inventory.warehouse_id warehouse.forreserve chargenumber bestbefore reserve_for_id reserve_for_table) ],
96 for (listify($params{by})) {
97 my $selects = $allowed_by{$_} or Carp::croak("unknown option for by: $_");
98 push @selects, @$selects;
99 push @groups, @$selects;
103 my $select = join ',', @selects;
104 my $where = @where ? 'WHERE ' . join ' AND ', @where : '';
105 my $group_by = @groups ? 'GROUP BY ' . join ', ', @groups : '';
108 SELECT $select FROM inventory
109 LEFT JOIN bin ON bin_id = bin.id
110 LEFT JOIN warehouse ON bin.warehouse_id = warehouse.id
115 my $results = selectall_hashref_query($::form, SL::DB->client->dbh, $query, @values);
118 part => 'SL::DB::Manager::Part',
119 bin => 'SL::DB::Manager::Bin',
120 warehouse => 'SL::DB::Manager::Warehouse',
121 reserve_for => undef,
127 warehouse => 'warehouse_id',
130 if ($params{by} && $params{with_objects}) {
131 for my $with_object (listify($params{with_objects})) {
132 Carp::croak("unknown with_object $with_object") if !exists $with_objects{$with_object};
134 if (my $manager = $with_objects{$with_object}) {
135 my $slot = $slots{$with_object};
136 next if !(my @ids = map { $_->{$slot} } @$results);
137 my $objects = $manager->get_all(query => [ id => \@ids ]);
138 my %objects_by_id = map { $_->id => $_ } @$objects;
140 $_->{$with_object} = $objects_by_id{$_->{$slot}} for @$results;
142 # need to fetch all reserve_for_table partitions
150 return $results->[0]{qty};
155 _get_stock_onhand(@_, onhand => 0);
159 _get_stock_onhand(@_, onhand => 1);
165 my $part = $params{part} or Carp::croak('allocate needs a part');
166 my $qty = $params{qty} or Carp::croak('allocate needs a qty');
168 return () if $qty <= 0;
170 my $results = get_stock(part => $part, by => 'for_allocate');
171 my %bin_whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($params{bin});
172 my %wh_whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($params{warehouse});
173 my %chargenumbers = map { (ref $_ ? $_->id : $_) => 1 } listify($params{chargenumber});
174 my %reserve_whitelist;
175 if ($params{reserve_for}) {
176 $reserve_whitelist{ $_->meta->table }{ $_->id } = 1 for listify($params{reserve_for});
179 # filter the results. we don't want:
181 # - bins that are reserve but not in the white-list of warehouses or bins
182 # - reservations that are not white-listed
184 my @filtered_results = grep {
185 (!$_->{forreserve} || $bin_whitelist{$_->{bin_id}} || $wh_whitelist{$_->{warehouse_id}})
186 && (!$_->{reserve_for_id} || $reserve_whitelist{ $_->{reserve_for_table} }{ $_->{reserve_for_id} })
189 # sort results so that reserve_for is first, then chargenumbers, then wanted bins, then wanted warehouses
190 my @sorted_results = sort {
191 (!!$b->{reserve_for_id}) <=> (!!$a->{reserve_for_id}) # sort by existing reserve_for_id first.
192 || exists $chargenumbers{$b->{chargenumber}} <=> exists $chargenumbers{$a->{chargenumber}} # then prefer wanted chargenumbers
193 || exists $bin_whitelist{$b->{bin_id}} <=> exists $bin_whitelist{$a->{bin_id}} # then prefer wanted bins
194 || exists $wh_whitelist{$b->{warehouse_id}} <=> exists $wh_whitelist{$a->{warehouse_id}} # then prefer wanted bins
199 for my $chunk (@sorted_results) {
200 my $qty = min($chunk->{qty}, $rest_qty);
202 push @allocations, SL::Helper::Inventory::Allocation->new(
203 parts_id => $chunk->{parts_id},
205 comment => $params{comment},
206 bin_id => $chunk->{bin_id},
207 warehouse_id => $chunk->{warehouse_id},
208 chargenumber => $chunk->{chargenumber},
209 bestbefore => $chunk->{bestbefore},
210 reserve_for_id => $chunk->{reserve_for_id},
211 reserve_for_table => $chunk->{reserve_for_table},
212 for_object_id => undef,
214 $rest_qty -= _round_qty($qty);
216 $rest_qty = _round_qty($rest_qty);
217 last if $rest_qty == 0;
220 die SL::X::Inventory::Allocation->new(
221 error => 'not enough to allocate',
222 msg => t8("can not allocate #1 units of #2, missing #3 units", _qty($qty), $part->displayable_name, _qty($rest_qty)),
225 if ($params{constraints}) {
226 check_constraints($params{constraints},\@allocations);
232 sub allocate_for_assembly {
235 my $part = $params{part} or Carp::croak('allocate needs a part');
236 my $qty = $params{qty} or Carp::croak('allocate needs a qty');
238 Carp::croak('not an assembly') unless $part->is_assembly;
240 my %parts_to_allocate;
242 for my $assembly ($part->assemblies) {
243 next if $assembly->part->dispotype eq 'no_stock';
245 my $tmpqty = $assembly->assembly_part->is_recipe ? $assembly->qty * $qty / $assembly->assembly_part->scalebasis
246 : $assembly->part->unit eq 'Stck' ? ceil($assembly->qty * $qty)
247 : $assembly->qty * $qty;
248 $parts_to_allocate{ $assembly->part->id } //= 0;
249 $parts_to_allocate{ $assembly->part->id } += $tmpqty;
254 for my $part_id (keys %parts_to_allocate) {
255 my $part = SL::DB::Part->load_cached($part_id);
256 push @allocations, allocate(%params, part => $part, qty => $parts_to_allocate{$part_id});
262 sub check_constraints {
263 my ($constraints, $allocations) = @_;
264 if ('CODE' eq ref $constraints) {
265 if (!$constraints->(@$allocations)) {
266 die SL::X::Inventory::Allocation->new(
267 error => 'allocation constraints failure',
268 msg => t8("Allocations didn't pass constraints"),
272 croak 'constraints needs to be a hashref' unless 'HASH' eq ref $constraints;
274 my %supported_constraints = (
276 warehouse_id => 'warehouse_id',
277 chargenumber => 'chargenumber',
280 for (keys %$constraints ) {
281 croak "unsupported constraint '$_'" unless $supported_constraints{$_};
283 my %whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($constraints->{$_});
284 my $accessor = $supported_constraints{$_};
286 if (any { !$whitelist{$_->$accessor} } @$allocations) {
287 my %error_constraints = (
288 bin_id => t8('Bins'),
289 warehouse_id => t8('Warehouses'),
290 chargenumber => t8('Chargenumbers'),
292 my @allocs = grep { $whitelist{$_->$accessor} } @$allocations;
293 my $needed = sum map { $_->qty } grep { !$whitelist{$_->$accessor} } @$allocations;
294 my $err = t8("Cannot allocate parts.");
295 $err .= ' '.t8('part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.',
296 SL::DB::Part->load_cached($_->parts_id)->description,
297 SL::DB::Bin->load_cached($_->bin_id)->full_description,
298 _qty($_->qty), _qty($needed), $_->chargenumber ? $_->chargenumber : '--') for @allocs;
299 die SL::X::Inventory::Allocation->new(
300 error => 'allocation constraints failure',
308 sub produce_assembly {
311 my $part = $params{part} or Carp::croak('produce_assembly needs a part');
312 my $qty = $params{qty} or Carp::croak('produce_assembly needs a qty');
314 my $allocations = $params{allocations};
315 if ($params{auto_allocate}) {
316 Carp::croak("produce_assembly: can't have both allocations and auto_allocate") if $params{allocations};
317 $allocations = [ allocate_for_assembly(part => $part, qty => $qty) ];
319 Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations};
320 $allocations = $params{allocations};
323 my $bin = $params{bin} or Carp::croak("need target bin");
324 my $chargenumber = $params{chargenumber};
325 my $bestbefore = $params{bestbefore};
326 my $for_object_id = $params{for_object_id};
327 my $comment = $params{comment} // '';
329 my $production_order_item = $params{production_order_item};
330 my $invoice = $params{invoice};
331 my $project = $params{project};
332 my $reserve_for = $params{reserve_for};
334 my $reserve_for_id = $reserve_for ? $reserve_for->id : undef;
335 my $reserve_for_table = $reserve_for ? $reserve_for->meta->table : undef;
337 my $shippingdate = $params{shippingsdate} // DateTime->now_local;
339 my $trans_id = $params{trans_id};
340 ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id;
342 my $trans_type_out = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used');
343 my $trans_type_in = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'assembled');
345 # check whether allocations are sane
346 if (!$params{no_check_allocations} && !$params{auto_allocate}) {
347 my %allocations_by_part = map { $_->parts_id => $_->qty } @$allocations;
348 for my $assembly ($part->assemblies) {
349 $allocations_by_part{ $assembly->parts_id } -= $assembly->qty * $qty; # TODO recipe factor
352 die "allocations are insufficient for production" if any { $_ < 0 } values %allocations_by_part;
356 for my $allocation (@$allocations) {
357 my $oe_id = delete $allocation->{for_object_id};
358 push @transfers, SL::DB::Inventory->new(
359 trans_id => $trans_id,
361 qty => -$allocation->qty,
362 trans_type => $trans_type_out,
363 shippingdate => $shippingdate,
364 employee => SL::DB::Manager::Employee->current,
365 oe_id => $allocation->for_object_id,
369 push @transfers, SL::DB::Inventory->new(
370 trans_id => $trans_id,
371 trans_type => $trans_type_in,
375 warehouse => $bin->warehouse_id,
376 chargenumber => $chargenumber,
377 bestbefore => $bestbefore,
378 reserve_for_id => $reserve_for_id,
379 reserve_for_table => $reserve_for_table,
380 shippingdate => $shippingdate,
384 prod => $production_order_item,
385 employee => SL::DB::Manager::Employee->current,
386 oe_id => $for_object_id,
389 SL::DB->client->with_transaction(sub {
390 $_->save for @transfers;
393 die SL::DB->client->error;
399 package SL::Helper::Inventory::Allocation {
400 my @attributes = qw(parts_id qty bin_id warehouse_id chargenumber bestbefore comment reserve_for_id reserve_for_table for_object_id);
401 my %attributes = map { $_ => 1 } @attributes;
403 for my $name (@attributes) {
405 *{"SL::Helper::Inventory::Allocation::$name"} = sub { $_[0]{$name} };
409 my ($class, %params) = @_;
411 Carp::croak("missing attribute $_") for grep { !exists $params{$_} } @attributes;
412 Carp::croak("unknown attribute $_") for grep { !exists $attributes{$_} } keys %params;
413 Carp::croak("$_ must be set") for grep { !$params{$_} } qw(parts_id qty bin_id);
414 Carp::croak("$_ must be positive") for grep { !($params{$_} > 0) } qw(parts_id qty bin_id);
416 bless { %params }, $class;
426 SL::WH - Warehouse and Inventory API
430 # See description for an intro to the concepts used here.
432 use SL::Helper::Inventory;
434 # stock, get "what's there" for a part with various conditions:
435 my $qty = SL::Helper::Inventory->get_stock(part => $part); # how much is on stock?
436 my $qty = SL::Helper::Inventory->get_stock(part => $part, date => $date); # how much was on stock at a specific time?
437 my $qty = SL::Helper::Inventory->get_stock(part => $part, bin => $bin); # how is on stock in a specific bin?
438 my $qty = SL::Helper::Inventory->get_stock(part => $part, warehouse => $warehouse); # how is on stock in a specific warehouse?
439 my $qty = SL::Helper::Inventory->get_stock(part => $part, chargenumber => $chargenumber); # how is on stock of a specific chargenumber?
441 # onhand, get "what's available" for a part with various conditions:
442 my $qty = SL::Helper::Inventory->get_onhand(part => $part); # how much is available?
443 my $qty = SL::Helper::Inventory->get_onhand(part => $part, date => $date); # how much was available at a specific time?
444 my $qty = SL::Helper::Inventory->get_onhand(part => $part, bin => $bin); # how much is available in a specific bin?
445 my $qty = SL::Helper::Inventory->get_onhand(part => $part, warehouse => $warehouse); # how much is available in a specific warehouse?
446 my $qty = SL::Helper::Inventory->get_onhand(part => $part, chargenumber => $chargenumber); # how much is availbale of a specific chargenumber?
447 my $qty = SL::Helper::Inventory->get_onhand(part => $part, reserve_for => $order); # how much is available if you include this reservation?
450 my $data = SL::Helper::Inventory->get_onhand(
451 warehouse => $warehouse,
452 by => [ qw(bin part chargenumber reserve_for) ],
453 with_objects => [ qw(bin part) ],
457 my @allocations, SL::Helper::Inventory->allocate(
458 part => $part, # part_id works too
459 qty => $qty, # must be positive
460 chargenumber => $chargenumber, # optional, may be arrayref. if provided these charges will be used first
461 bestbefore => $datetime, # optional, defaults to today. items with bestbefore prior to that date wont be used
462 reserve_for => $object, # optional, may be arrayref. if provided the qtys reserved for these objects will be used first
463 bin => $bin, # optional, may be arrayref. if provided
466 # shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate
467 my @allocations, SL::Helper::Inventory->allocate_for_assembly(
468 part => $assembly, # part_id works too
469 qty => $qty, # must be positive
472 # create allocation manually, bypassing checks, all of these need to be passed, even undefs
473 my $allocation = SL::Helper::Inventory::Allocation->new(
474 part_id => $part->id,
476 bin_id => $bin_obj->id,
477 warehouse_id => $bin_obj->warehouse_id,
478 chargenumber => '1823772365',
480 reserve_for_id => undef,
481 reserve_for_table => undef,
482 for_object_id => $order->id,
486 SL::Helper::Inventory->produce_assembly(
487 part => $part, # target assembly
489 allocations => \@allocations, # allocations to use. alternatively use "auto_allocate => 1,"
492 bin => $bin, # needed unless a global standard target is configured
493 chargenumber => $chargenumber, # optional
494 bestbefore => $datetime, # optional
495 comment => $comment, # optional
497 # links, all optional
498 production_order_item => $item,
499 reserve_for => $object,
504 New functions for the warehouse and inventory api.
506 The WH api currently has three large shortcomings. It is very hard to just get
507 the current stock for an item, it's extremely complicated to use it to produce
508 assemblies while ensuring that no stock ends up negative, and it's very hard to
509 use it to get an overview over the actual contents of the inventory.
511 The first problem has spawned several dozen small functions in the program that
512 try to implement that, and those usually miss some details. They may ignore
513 reservations, or reserve warehouses, or bestbefore times.
515 To get this cleaned up a bit this code introduces two concepts: stock and onhand.
517 Stock is defined as the actual contents of the inventory, everything that is
518 there. Onhand is what is available, which means things that are stocked and not
519 reserved and not expired.
521 The two new functions C<get_stock> and C<get_onhand> encapsulate these principles and
522 allow simple access with some optional filters for chargenumbers or warehouses.
523 Both of them have a batch mode that can be used to get these information to
524 supllement smiple reports.
526 To address the safe assembly creation a new function has been added.
527 C<allocate> will try to find the requested quantity of a part in the inventory
528 and will return allocations of it which can then be used to create the
529 assembly. Allocation will happen with the C<onhand> semantics defined above,
530 meaning that by default no reservations or expired goods will be used. The
531 caller can supply hints of what shold be used and in those cases chargenumber
532 and reservations will be used up as much as possible first. C<allocate> will
533 always try to fulfil the request even beyond those. Should the required amount
534 not be stocked, allocate will throw an exception.
536 C<produce_assembly> has been rewritten to only accept parameters about the
537 target of the production, and requires allocations to complete the request. The
538 allocations can be supplied manually, or can be generated automatically.
539 C<produce_assembly> will check whether enough allocations are given to create
540 the recipe, but will not check whether the allocations are backed. If the
541 allocations are not sufficient or if the auto-allocation fails an exception
542 is returned. If you need to produce something that is not in the inventory, you
543 can bypass those checks by creating the allocations yourself (see
544 L</"ALLOCATION DATA STRUCTURE">).
546 Note: this is only intended to cover the scenarios described above. For other cases:
552 If you need the reserved amount for an order use C<SL::DB::Helper::Reservation>
557 If you need actual inventory objects because of record links, prod_id links or
558 something like that load them directly. And strongly consider redesigning that,
559 because it's really fragile.
563 You need weight or accounting information you're on your own. The inventory api
564 only concerns itself with the raw quantities.
568 If you need the first stock date of parts, or anything related to a specific
569 transfer type or direction, this is not covered yet.
577 =item * get_stock PARAMS
579 Returns for single parts how much actually exists in the inventory.
587 The part. Must be present without C<by>. May be arrayref with C<by>. Can be object or id.
591 If given, will only return stock on these bins. Optional. May be array, May be object or id.
595 If given, will only return stock on these warehouses. Optional. May be array, May be object or id.
599 If given, will return stock as it were on this timestamp. Optional. Must be L<DateTime> object.
603 If given, will only show stock with this chargenumber. Optional. May be array.
607 See L</"STOCK/ONHAND REPORT MODE">
611 See L</"STOCK/ONHAND REPORT MODE">
615 Will return a single qty normally, see L</"STOCK/ONHAND REPORT MODE"> for batch
616 mode when C<by> is given.
618 =item * get_onhand PARAMS
620 Returns for single parts how much is available in the inventory. That excludes:
621 reserved quantities, reserved warehouses and stock with expired bestbefore.
623 It takes all options of L</get_stock> but treats some of the differently and has some additional ones:
629 Usually C<onhand> will not include results from warehouses with the C<reserve>
630 flag. However giving an explicit list of warehouses will include there in the
631 search, as well as all others.
635 =item * reserve_warehouse
641 =item * allocate PARAMS
653 Bin object. Optional.
657 Warehouse object. Optional.
669 Needs to be a rose object, where id and table can be extracted. Optional.
673 Tries to allocate the required quantity using what is currently onhand. If
674 given any of C<bin>, C<warehouse>, C<chargenumber>, C<reserve_for>
677 =item * allocate_for_assembly PARAMS
679 Shortcut to allocate everything for an assembly. Takes the same arguments. Will
680 compute the required amount for each assembly part and allocate all of them.
682 =item * produce_assembly
687 =head1 STOCK/ONHAND REPORT MODE
689 If the special option C<by> is given with an arrayref, the result will instead
690 be an arrayref of partitioned stocks by those fields. Valid partitions are:
696 If this is given, part is optional in the parameters
710 Note: If you want to use the returned data to create allocations you I<need> to
711 enable all of these. To make this easier a special shortcut exists
713 In this mode, C<with_objects> can be used to load C<warehouse>, C<bin>,
714 C<parts>, and the C<reserve_for> objects in one go, just like with Rose. They
715 need to be present in C<by> before that though.
717 =head1 ALLOCATION ALGORITHM
719 When calling allocate, the current onhand (== available stock) of the item will
720 be used to decide which bins/chargenumbers/bestbefore can be used.
722 In general allocate will try to make the request happen, and will use the
723 provided charges up first, and then tap everything else. If you need to only
724 I<exactly> use the provided charges, you'll need to craft the allocations
725 yourself. See L</"ALLOCATION DATA STRUCTURE"> for that.
727 If C<reserve_for> is given, those will be used up first too.
729 If C<reserved_warehouse> is given, those will be used up second.
731 If C<chargenumber> is given, those will be used up next.
733 After that normal quantities will be used.
735 These are tiebreakers and expected to rarely matter in reality. If you need
736 finegrained control over which allocation is used, you may want to get the
737 onhands yourself and select the appropriate ones.
739 Only quantities with C<bestbefore> unset or after the given date will be
740 considered. If more than one charge is eligible, the earlier C<bestbefore>
743 Allocations do NOT have an internal memory and can't react to other allocations
744 of the same part earlier. Never double allocate the same part within a
747 =head1 ALLOCATION DATA STRUCTURE
749 Allocations are instances of the helper class C<SL::Helper::Inventory::Allocation>. They require
750 each of the following attributes to be set at creation time:
766 =item * reserve_for_id
768 =item * reserve_for_table
770 =item * for_object_id
772 If set the allocations will be marked as allocated for the given object.
773 If these allocations are later used to produce an assembly, the resulting
774 consuming transactions will be marked as belonging to the given object.
775 The object may be an order, productionorder or other objects
779 C<chargenumber>, C<bestbefore>, C<reserve_for_id>, C<reserve_for_table> and
780 C<for_object_id> may be C<undef> (but must still be present at creation time).
781 Instances are considered immutable.
786 # whitelist constraints
790 bin_id => \@allowed_bins,
791 chargenumber => \@allowed_chargenumbers,
798 # only allow chargenumbers with specific format
799 all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_
802 # and must be all reservations
803 all { $_->reserve_for_id } @_;
807 C<allocation> is "best effort" in nature. It will take the C<bin>,
808 C<chargenumber> etc hints from the parameters, but will try it's bvest to
809 fulfil the request anyway and only bail out if it is absolutely not possible.
811 Sometimes you need to restrict allocations though. For this you can pass
812 additional constraints to C<allocate>. A constraint serves as a whitelist.
813 Every allocation must fulfil every constraint by having that attribute be one
816 In case even that is not enough, you may supply a custom check by passing a
817 function that will be given the allocation objects.
819 Note that both whitelists and constraints do not influence the order of
820 allocations, which is done purely from the initial parameters. They only serve
821 to reject allocations made in good faith which do fulfil required assertions.
823 =head1 ERROR HANDLING
825 C<allocate> and C<produce_assembly> will throw exceptions if the request can
826 not be completed. The usual reason will be insufficient onhand to allocate, or
827 insufficient allocations to process the request.
831 * define and describe error classes
832 * define wrapper classes for stock/onhand batch mode return values
833 * handle extra arguments in produce: shippingdate, project, oe
834 * clean up allocation helper class
835 * with objects for reservations
845 Sven Schöling E<lt>sven.schoeling@opendynamic.deE<gt>