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 none);
11 use Scalar::Util qw(blessed);
13 use SL::Locale::String qw(t8);
14 use SL::MoreCommon qw(listify);
15 use SL::DBUtils qw(selectall_hashref_query selectrow_query);
16 use SL::DB::TransferType;
17 use SL::Helper::Number qw(_format_number _round_number);
18 use SL::Helper::Inventory::Allocation;
21 our @EXPORT_OK = qw(get_stock get_onhand allocate allocate_for_assembly produce_assembly check_constraints check_allocations_for_assembly);
22 our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
24 sub _get_stock_onhand {
27 my $onhand_mode = !!$params{onhand};
31 'MIN(EXTRACT(epoch FROM inventory.itime)) AS itime',
38 my @ids = map { ref $_ ? $_->id : $_ } listify($params{part});
39 push @where, sprintf "parts_id IN (%s)", join ', ', ("?") x @ids;
44 my @ids = map { ref $_ ? $_->id : $_ } listify($params{bin});
45 push @where, sprintf "bin_id IN (%s)", join ', ', ("?") x @ids;
49 if ($params{warehouse}) {
50 my @ids = map { ref $_ ? $_->id : $_ } listify($params{warehouse});
51 push @where, sprintf "warehouse.id IN (%s)", join ', ', ("?") x @ids;
55 if ($params{chargenumber}) {
56 my @ids = listify($params{chargenumber});
57 push @where, sprintf "chargenumber IN (%s)", join ', ', ("?") x @ids;
62 Carp::croak("not DateTime ".$params{date}) unless ref($params{date}) eq 'DateTime';
63 push @where, sprintf "shippingdate <= ?";
64 push @values, $params{date};
67 if (!$params{bestbefore} && $onhand_mode && default_show_bestbefore()) {
68 $params{bestbefore} = DateTime->now_local;
71 if ($params{bestbefore}) {
72 Carp::croak("not DateTime ".$params{date}) unless ref($params{bestbefore}) eq 'DateTime';
73 push @where, sprintf "(bestbefore IS NULL OR bestbefore >= ?)";
74 push @values, $params{bestbefore};
79 part => [ qw(parts_id) ],
80 bin => [ qw(bin_id inventory.warehouse_id)],
81 warehouse => [ qw(inventory.warehouse_id) ],
82 chargenumber => [ qw(chargenumber) ],
83 bestbefore => [ qw(bestbefore) ],
84 for_allocate => [ qw(parts_id bin_id inventory.warehouse_id chargenumber bestbefore) ],
88 for (listify($params{by})) {
89 my $selects = $allowed_by{$_} or Carp::croak("unknown option for by: $_");
90 push @selects, @$selects;
91 push @groups, @$selects;
95 my $select = join ',', @selects;
96 my $where = @where ? 'WHERE ' . join ' AND ', @where : '';
97 my $group_by = @groups ? 'GROUP BY ' . join ', ', @groups : '';
100 SELECT $select FROM inventory
101 LEFT JOIN bin ON bin_id = bin.id
102 LEFT JOIN warehouse ON bin.warehouse_id = warehouse.id
107 $query .= ' HAVING SUM(qty) > 0';
110 my $results = selectall_hashref_query($::form, SL::DB->client->dbh, $query, @values);
113 part => 'SL::DB::Manager::Part',
114 bin => 'SL::DB::Manager::Bin',
115 warehouse => 'SL::DB::Manager::Warehouse',
121 warehouse => 'warehouse_id',
124 if ($params{by} && $params{with_objects}) {
125 for my $with_object (listify($params{with_objects})) {
126 Carp::croak("unknown with_object $with_object") if !exists $with_objects{$with_object};
128 my $manager = $with_objects{$with_object};
129 my $slot = $slots{$with_object};
130 next if !(my @ids = map { $_->{$slot} } @$results);
131 my $objects = $manager->get_all(query => [ id => \@ids ]);
132 my %objects_by_id = map { $_->id => $_ } @$objects;
134 $_->{$with_object} = $objects_by_id{$_->{$slot}} for @$results;
141 return $results->[0]{qty};
146 _get_stock_onhand(@_, onhand => 0);
150 _get_stock_onhand(@_, onhand => 1);
156 croak('allocate needs a part') unless $params{part};
157 croak('allocate needs a qty') unless $params{qty};
159 my $part = $params{part};
160 my $qty = $params{qty};
162 return () if $qty <= 0;
164 my $results = get_stock(part => $part, by => 'for_allocate');
165 my %bin_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{bin});
166 my %wh_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{warehouse});
167 my %chargenumbers = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{chargenumber});
169 # sort results so that chargenumbers are matched first, then wanted bins, then wanted warehouses
170 my @sorted_results = sort {
171 exists $chargenumbers{$b->{chargenumber}} <=> exists $chargenumbers{$a->{chargenumber}} # then prefer wanted chargenumbers
172 || exists $bin_whitelist{$b->{bin_id}} <=> exists $bin_whitelist{$a->{bin_id}} # then prefer wanted bins
173 || exists $wh_whitelist{$b->{warehouse_id}} <=> exists $wh_whitelist{$a->{warehouse_id}} # then prefer wanted bins
174 || $a->{itime} <=> $b->{itime} # and finally prefer earlier charges
179 for my $chunk (@sorted_results) {
180 my $qty = min($chunk->{qty}, $rest_qty);
182 # since allocate operates on stock, this also ensures that no negative stock results are used
184 push @allocations, SL::Helper::Inventory::Allocation->new(
185 parts_id => $chunk->{parts_id},
187 comment => $params{comment},
188 bin_id => $chunk->{bin_id},
189 warehouse_id => $chunk->{warehouse_id},
190 chargenumber => $chunk->{chargenumber},
191 bestbefore => $chunk->{bestbefore},
192 for_object_id => undef,
194 $rest_qty -= _round_number($qty, 5);
196 $rest_qty = _round_number($rest_qty, 5);
197 last if $rest_qty == 0;
200 die SL::X::Inventory::Allocation::MissingQty->new(
201 code => 'not enough to allocate',
202 message => t8("can not allocate #1 units of #2, missing #3 units", _format_number($qty), $part->displayable_name, _format_number($rest_qty)),
203 part_description => $part->displayable_name,
204 to_allocate_qty => $qty,
205 missing_qty => $rest_qty,
208 if ($params{constraints}) {
209 check_constraints($params{constraints},\@allocations);
215 sub allocate_for_assembly {
218 my $part = $params{part} or Carp::croak('allocate needs a part');
219 my $qty = $params{qty} or Carp::croak('allocate needs a qty');
220 my $wh = $params{warehouse};
221 my $wh_strict = $::instance_conf->get_produce_assembly_same_warehouse;
222 my $consume_service = $::instance_conf->get_produce_assembly_transfer_service;
224 Carp::croak('not an assembly') unless $part->is_assembly;
225 Carp::croak('No warehouse selected') if $wh_strict && !$wh;
227 my %parts_to_allocate;
229 for my $assembly ($part->assemblies) {
230 next if $assembly->part->type eq 'service' && !$consume_service;
231 $parts_to_allocate{ $assembly->part->id } //= 0;
232 $parts_to_allocate{ $assembly->part->id } += $assembly->qty * $qty;
235 my (@allocations, @errors);
237 for my $part_id (keys %parts_to_allocate) {
238 my $part = SL::DB::Part->load_cached($part_id);
241 push @allocations, allocate(%params, part => $part, qty => $parts_to_allocate{$part_id});
243 die SL::X::Inventory::Allocation->new(
244 code => "wrong warehouse for part",
245 message => t8('Part #1 exists in warehouse #2, but not in warehouse #3 ',
246 $part->partnumber . ' ' . $part->description,
247 SL::DB::Manager::Warehouse->find_by(id => $allocations[-1]->{warehouse_id})->description,
249 ) unless $allocations[-1]->{warehouse_id} == $wh->id;
254 die $ex unless blessed($ex) && $ex->can('rethrow');
256 if ($ex->isa('SL::X::Inventory::Allocation')) {
265 die SL::X::Inventory::Allocation::Multi->new(
266 code => "multiple errors during allocation",
267 message => "multiple errors during allocation",
275 sub check_constraints {
276 my ($constraints, $allocations) = @_;
277 if ('CODE' eq ref $constraints) {
278 if (!$constraints->(@$allocations)) {
279 die SL::X::Inventory::Allocation->new(
280 code => 'allocation constraints failure',
281 message => t8("Allocations didn't pass constraints"),
285 croak 'constraints needs to be a hashref' unless 'HASH' eq ref $constraints;
287 my %supported_constraints = (
289 warehouse_id => 'warehouse_id',
290 chargenumber => 'chargenumber',
293 for (keys %$constraints ) {
294 croak "unsupported constraint '$_'" unless $supported_constraints{$_};
295 next unless defined $constraints->{$_};
297 my %whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($constraints->{$_});
298 my $accessor = $supported_constraints{$_};
300 if (any { !$whitelist{$_->$accessor} } @$allocations) {
301 my %error_constraints = (
302 bin_id => t8('Bins'),
303 warehouse_id => t8('Warehouses'),
304 chargenumber => t8('Chargenumbers'),
306 my @allocs = grep { $whitelist{$_->$accessor} } @$allocations;
307 my $needed = sum map { $_->qty } grep { !$whitelist{$_->$accessor} } @$allocations;
308 my $err = t8("Cannot allocate parts.");
309 $err .= ' '.t8('part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.',
310 SL::DB::Part->load_cached($_->parts_id)->description,
311 SL::DB::Bin->load_cached($_->bin_id)->full_description,
312 _format_number($_->qty), _format_number($needed), $_->chargenumber ? $_->chargenumber : '--') for @allocs;
313 die SL::X::Inventory::Allocation->new(
314 code => 'allocation constraints failure',
322 sub produce_assembly {
325 my $part = $params{part} or Carp::croak('produce_assembly needs a part');
326 my $qty = $params{qty} or Carp::croak('produce_assembly needs a qty');
327 my $bin = $params{bin} or Carp::croak("need target bin");
329 my $allocations = $params{allocations};
330 my $strict_wh = $::instance_conf->get_produce_assembly_same_warehouse ? $bin->warehouse : undef;
331 my $consume_service = $::instance_conf->get_produce_assembly_transfer_service;
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, warehouse => $strict_wh, chargenumber => $params{chargenumber}) ];
337 Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations};
338 $allocations = $params{allocations};
341 my $chargenumber = $params{chargenumber};
342 my $bestbefore = $params{bestbefore};
343 my $for_object_id = $params{for_object_id};
344 my $comment = $params{comment} // '';
345 my $invoice = $params{invoice};
346 my $project = $params{project};
347 my $shippingdate = $params{shippingsdate} // DateTime->now_local;
348 my $trans_id = $params{trans_id};
350 ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id;
352 my $trans_type_out = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used');
353 my $trans_type_in = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'assembled');
355 # check whether allocations are sane
356 if (!$params{no_check_allocations} && !$params{auto_allocate}) {
357 die SL::X::Inventory::Allocation->new(
358 code => "allocations are insufficient for production",
359 message => t8('can not allocate enough resources for production'),
360 ) if !check_allocations_for_assembly(part => $part, qty => $qty, allocations => $allocations);
364 for my $allocation (@$allocations) {
365 my $oe_id = delete $allocation->{for_object_id};
366 push @transfers, $allocation->transfer_object(
367 trans_id => $trans_id,
368 qty => -$allocation->qty,
369 trans_type => $trans_type_out,
370 shippingdate => $shippingdate,
371 employee => SL::DB::Manager::Employee->current,
372 comment => t8('Used for assembly #1 #2', $part->partnumber, $part->description),
376 push @transfers, SL::DB::Inventory->new(
377 trans_id => $trans_id,
378 trans_type => $trans_type_in,
382 warehouse => $bin->warehouse_id,
383 chargenumber => $chargenumber,
384 bestbefore => $bestbefore,
385 shippingdate => $shippingdate,
389 employee => SL::DB::Manager::Employee->current,
390 oe_id => $for_object_id,
393 SL::DB->client->with_transaction(sub {
394 $_->save for @transfers;
397 die SL::DB->client->error;
403 sub check_allocations_for_assembly {
406 my $part = $params{part} or Carp::croak('check_allocations_for_assembly needs a part');
407 my $qty = $params{qty} or Carp::croak('check_allocations_for_assembly needs a qty');
409 my $check_overfulfilment = !!$params{check_overfulfilment};
410 my $allocations = $params{allocations};
412 my $consume_service = $::instance_conf->get_produce_assembly_transfer_service;
414 my %allocations_by_part;
415 for (@{ $allocations || []}) {
416 $allocations_by_part{$_->parts_id} //= 0;
417 $allocations_by_part{$_->parts_id} += $_->qty;
420 for my $assembly ($part->assemblies) {
421 next if $assembly->part->type eq 'service' && !$consume_service;
422 $allocations_by_part{ $assembly->parts_id } -= $assembly->qty * $qty;
425 return (none { $_ < 0 } values %allocations_by_part) && (!$check_overfulfilment || (none { $_ > 0 } values %allocations_by_part));
428 sub check_stock_out_transfer_requests {
431 my $transfer_requests = $params{transfer_requests} or Carp::croak('check_stock_out_transfer_requests needs transfer_requests');
432 my $default_transfer = $params{default_transfer} || 0;
434 my $grouped_qtys; # part_id -> bin_id -> chargenumber -> bestbefore -> qty;
438 foreach my $request (@$transfer_requests) {
440 ->{$request->parts_id}
442 ->{$request->chargenumber}
443 ->{$request->bestbefore} += -$request->qty; # qty is negative
444 $bin_ids{$request->bin_id} = 1;
445 $chargenumbers{$request->chargenumber} = 1;
448 my $stocks = get_stock(
449 by => [qw(part bin chargenumber bestbefore)],
450 part => [keys %$grouped_qtys],
451 bin => [keys %bin_ids],
452 chargenumber => [keys %chargenumbers],
455 # make stock searchable
457 foreach my $stock (@$stocks) {
459 ->{$stock->{parts_id}}
461 ->{$stock->{chargenumber}}
462 ->{DateTime->from_kivitendo($stock->{bestbefore}) || undef} = $stock->{qty};
466 foreach my $p_id (keys %{$grouped_qtys}) {
467 foreach my $b_id (keys %{$grouped_qtys->{$p_id}}) {
468 next if $default_transfer
469 && $::instance_conf->get_transfer_default_ignore_onhand
470 && $::instance_conf->get_bin_id_ignore_onhand eq $b_id;
471 foreach my $cn (keys %{$grouped_qtys->{$p_id}->{$b_id}}) {
472 foreach my $bb (keys %{$grouped_qtys->{$p_id}->{$b_id}->{$cn}}) {
473 my $available_stock = $available_qty->{$p_id}->{$b_id}->{$cn}->{$bb};
474 if ($available_stock < $grouped_qtys->{$p_id}->{$b_id}->{$cn}->{$bb}) {
475 my $part = SL::DB::Manager::Part->find_by(id => $p_id);
476 my $bin = SL::DB::Manager::Bin->find_by(id => $b_id);
477 push @missing_qtys, {
478 missing_qty => $grouped_qtys->{$p_id}->{$b_id}->{$cn}->{$bb} - $available_stock,
490 return @missing_qtys;
493 sub default_show_bestbefore {
494 $::instance_conf->get_show_bestbefore
503 SL::WH - Warehouse and Inventory API
507 # See description for an intro to the concepts used here.
509 use SL::Helper::Inventory qw(:ALL);
511 # stock, get "what's there" for a part with various conditions:
512 my $qty = get_stock(part => $part); # how much is on stock?
513 my $qty = get_stock(part => $part, date => $date); # how much was on stock at a specific time?
514 my $qty = get_stock(part => $part, bin => $bin); # how much is on stock in a specific bin?
515 my $qty = get_stock(part => $part, warehouse => $warehouse); # how much is on stock in a specific warehouse?
516 my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how much is on stock of a specific chargenumber?
518 # onhand, get "what's available" for a part with various conditions:
519 my $qty = get_onhand(part => $part); # how much is available?
520 my $qty = get_onhand(part => $part, date => $date); # how much was available at a specific time?
521 my $qty = get_onhand(part => $part, bin => $bin); # how much is available in a specific bin?
522 my $qty = get_onhand(part => $part, warehouse => $warehouse); # how much is available in a specific warehouse?
523 my $qty = get_onhand(part => $part, chargenumber => $chargenumber); # how much is availbale of a specific chargenumber?
526 my $data = get_onhand(
527 warehouse => $warehouse,
528 by => [ qw(bin part chargenumber) ],
529 with_objects => [ qw(bin part) ],
533 my @allocations = allocate(
534 part => $part, # part_id works too
535 qty => $qty, # must be positive
536 chargenumber => $chargenumber, # optional, may be arrayref. if provided these charges will be used first
537 bestbefore => $datetime, # optional, defaults to today. items with bestbefore prior to that date wont be used
538 bin => $bin, # optional, may be arrayref. if provided
541 # shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate
542 my @allocations = allocate_for_assembly(
543 part => $assembly, # part_id works too
544 qty => $qty, # must be positive
547 # create allocation manually, bypassing checks. all of these need to be passed, even undefs
548 my $allocation = SL::Helper::Inventory::Allocation->new(
549 parts_id => $part->id,
551 bin_id => $bin_obj->id,
552 warehouse_id => $bin_obj->warehouse_id,
553 chargenumber => '1823772365',
556 for_object_id => $order->id,
561 part => $part, # target assembly
563 allocations => \@allocations, # allocations to use. alternatively use "auto_allocate => 1,"
566 bin => $bin, # needed unless a global standard target is configured
567 chargenumber => $chargenumber, # optional
568 bestbefore => $datetime, # optional
569 comment => $comment, # optional
574 New functions for the warehouse and inventory api.
576 The WH api currently has three large shortcomings: It is very hard to just get
577 the current stock for an item, it's extremely complicated to use it to produce
578 assemblies while ensuring that no stock ends up negative, and it's very hard to
579 use it to get an overview over the actual contents of the inventory.
581 The first problem has spawned several dozen small functions in the program that
582 try to implement that, and those usually miss some details. They may ignore
583 bestbefore times, comments, ignore negative quantities etc.
585 To get this cleaned up a bit this code introduces two concepts: stock and onhand.
589 =item * Stock is defined as the actual contents of the inventory, everything that is
592 =item * Onhand is what is available, which means things that are stocked,
593 not expired and not in any other way reserved for other uses.
597 The two new functions C<get_stock> and C<get_onhand> encapsulate these principles and
598 allow simple access with some optional filters for chargenumbers or warehouses.
599 Both of them have a batch mode that can be used to get these information to
600 supplement simple reports.
602 To address the safe assembly creation a new function has been added.
603 C<allocate> will try to find the requested quantity of a part in the inventory
604 and will return allocations of it which can then be used to create the
605 assembly. Allocation will happen with the C<onhand> semantics defined above,
606 meaning that by default no expired goods will be used. The caller can supply
607 hints of what shold be used and in those cases chargenumbers will be used up as
608 much as possible first. C<allocate> will always try to fulfil the request even
609 beyond those. Should the required amount not be stocked, allocate will throw an
612 C<produce_assembly> has been rewritten to only accept parameters about the
613 target of the production, and requires allocations to complete the request. The
614 allocations can be supplied manually, or can be generated automatically.
615 C<produce_assembly> will check whether enough allocations are given to create
616 the assembly, but will not check whether the allocations are backed. If the
617 allocations are not sufficient or if the auto-allocation fails an exception
618 is returned. If you need to produce something that is not in the inventory, you
619 can bypass those checks by creating the allocations yourself (see
620 L</"ALLOCATION DATA STRUCTURE">).
622 Note: this is only intended to cover the scenarios described above. For other cases:
628 If you need actual inventory objects because of record links or something like
629 that load them directly. And strongly consider redesigning that, because it's
634 You need weight or accounting information you're on your own. The inventory api
635 only concerns itself with the raw quantities.
639 If you need the first stock date of parts, or anything related to a specific
640 transfer type or direction, this is not covered yet.
648 =item * get_stock PARAMS
650 Returns for single parts how much actually exists in the inventory.
658 The part. Must be present without C<by>. May be arrayref with C<by>. Can be object or id.
662 If given, will only return stock on these bins. Optional. May be array, May be object or id.
666 If given, will only return stock on these warehouses. Optional. May be array, May be object or id.
670 If given, will return stock as it were on this timestamp. Optional. Must be L<DateTime> object.
674 If given, will only show stock with this chargenumber. Optional. May be array.
678 See L</"STOCK/ONHAND REPORT MODE">
682 See L</"STOCK/ONHAND REPORT MODE">
686 Will return a single qty normally, see L</"STOCK/ONHAND REPORT MODE"> for batch
687 mode when C<by> is given.
689 =item * get_onhand PARAMS
691 Returns for single parts how much is available in the inventory. That excludes
692 stock with expired bestbefore.
694 It takes the same options as L</get_stock>.
700 If given, will only return stock with a bestbefore at or after the given date.
701 Optional. Must be L<DateTime> object.
705 =item * allocate PARAMS
717 Bin object. Optional.
721 Warehouse object. Optional.
733 Tries to allocate the required quantity using what is currently onhand. If
734 given any of C<bin>, C<warehouse>, C<chargenumber>
736 =item * allocate_for_assembly PARAMS
738 Shortcut to allocate everything for an assembly. Takes the same arguments. Will
739 compute the required amount for each assembly part and allocate all of them.
741 =item * produce_assembly
743 =item * check_allocations_for_assembly PARAMS
745 Checks if enough quantity is allocated for production. Returns a trueish
746 value if there is enough allocated, a falsish one otherwise (but see the
747 parameter C<check_overfulfilment>).
755 The part object to be assembled. Mandatory.
759 The quantity of the part to be assembled. Mandatory.
763 An array ref of the allocations.
765 =item * check_overfulfilment
767 Whether or not overfulfilment should be checked. If more quantity is allocated
768 than needed for production a falsish value is returned. Optional.
772 =item * check_stock_out_transfer_requests PARAMS
774 Checks if enough stock is availbale for the transfer requests. Returns a list
775 of missing quantities as hashref with the keys C<part>, C<bin>, C<missing_qty>, C<chargenumber>
776 and C<bestbefore>. C<chargenumber> and C<bestbefore> can be C<undef> if not set
777 in the transfer requests.
783 =item * transfer_requests
785 Transfer requests to stock out as arrayref. Mandatory.
787 =item * default_transfer
789 Has to be trueish if the transfer requests are for a delivery order called with
790 'Transfer out via default'. Optional, Default 0.
796 =head1 STOCK/ONHAND REPORT MODE
798 If the special option C<by> is given with an arrayref, the result will instead
799 be an arrayref of partitioned stocks by those fields. Valid partitions are:
805 If this is given, part is optional in the parameters
817 Note: If you want to use the returned data to create allocations you I<need> to
818 enable all of these. To make this easier a special shortcut exists
820 In this mode, C<with_objects> can be used to load C<warehouse>, C<bin>,
821 C<parts> objects in one go, just like with Rose. They
822 need to be present in C<by> before that though.
824 =head1 ALLOCATION ALGORITHM
826 When calling allocate, the current onhand (== available stock) of the item will
827 be used to decide which bins/chargenumbers/bestbefore can be used.
829 In general allocate will try to make the request happen, and will use the
830 provided charges up first, and then tap everything else. If you need to only
831 I<exactly> use the provided charges, you'll need to craft the allocations
832 yourself. See L</"ALLOCATION DATA STRUCTURE"> for that.
834 If C<chargenumber> is given, those will be used up next.
836 After that normal quantities will be used.
838 These are tiebreakers and expected to rarely matter in reality. If you need
839 finegrained control over which allocation is used, you may want to get the
840 onhands yourself and select the appropriate ones.
842 Only quantities with C<bestbefore> unset or after the given date will be
843 considered. If more than one charge is eligible, the earlier C<bestbefore>
846 Allocations do NOT have an internal memory and can't react to other allocations
847 of the same part earlier. Never double allocate the same part within a
850 =head1 ALLOCATION DATA STRUCTURE
852 Allocations are instances of the helper class C<SL::Helper::Inventory::Allocation>. They require
853 each of the following attributes to be set at creation time:
871 =item * for_object_id
873 If set the allocations will be marked as allocated for the given object.
874 If these allocations are later used to produce an assembly, the resulting
875 consuming transactions will be marked as belonging to the given object.
876 The object may be an order, productionorder or other objects
880 C<chargenumber>, C<bestbefore> and C<for_object_id> and C<comment> may be
881 C<undef> (but must still be present at creation time). Instances are considered
884 Allocations also provide the method C<transfer_object> which will create a new
885 C<SL::DB::Inventory> bject with all the playload.
889 # whitelist constraints
893 bin_id => \@allowed_bins,
894 chargenumber => \@allowed_chargenumbers,
901 # only allow chargenumbers with specific format
902 all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_
905 # and must all have a bestbefore date
906 all { $_->bestbefore } @_;
910 C<allocation> is "best effort" in nature. It will take the C<bin>,
911 C<chargenumber> etc hints from the parameters, but will try it's bvest to
912 fulfil the request anyway and only bail out if it is absolutely not possible.
914 Sometimes you need to restrict allocations though. For this you can pass
915 additional constraints to C<allocate>. A constraint serves as a whitelist.
916 Every allocation must fulfil every constraint by having that attribute be one
919 In case even that is not enough, you may supply a custom check by passing a
920 function that will be given the allocation objects.
922 Note that both whitelists and constraints do not influence the order of
923 allocations, which is done purely from the initial parameters. They only serve
924 to reject allocations made in good faith which do fulfil required assertions.
926 =head1 ERROR HANDLING
928 C<allocate> and C<produce_assembly> will throw exceptions if the request can
929 not be completed. The usual reason will be insufficient onhand to allocate, or
930 insufficient allocations to process the request.
932 =head1 KNOWN PROBLEMS
934 * It's not currently possible to identify allocations between requests, for
935 example for presenting the user possible allocations and then actually using
936 them on the next request.
937 * It's not currently possible to give C<allocate> prior constraints.
938 Currently all constraints are treated as hints (and will be preferred) but
939 the internal ordering of the hints is fixed and more complex preferentials
941 * bestbefore handling is untested
942 * interaction with config option "transfer_default_ignore_onhand" is
943 currently undefined (and implicitly ignores it)
947 * define and describe error classes
948 * define wrapper classes for stock/onhand batch mode return values
949 * handle extra arguments in produce: shippingdate, project
959 Sven Schöling E<lt>sven.schoeling@googlemail.comE<gt>