Inventory: allocate bevorzugt jetzt früherere Lagerbewegung
[kivitendo-erp.git] / SL / Helper / Inventory.pm
1 package SL::Helper::Inventory;
2
3 use strict;
4 use Carp;
5 use DateTime;
6 use Exporter qw(import);
7 use List::Util qw(min sum);
8 use List::UtilsBy qw(sort_by);
9 use List::MoreUtils qw(any);
10 use POSIX qw(ceil);
11
12 use SL::Locale::String qw(t8);
13 use SL::MoreCommon qw(listify);
14 use SL::DBUtils qw(selectall_hashref_query selectrow_query);
15 use SL::DB::TransferType;
16 use SL::Helper::Number qw(_round_qty _qty);
17 use SL::X;
18
19 our @EXPORT_OK = qw(get_stock get_onhand allocate allocate_for_assembly produce_assembly check_constraints);
20 our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
21
22 sub _get_stock_onhand {
23   my (%params) = @_;
24
25   my $onhand_mode = !!$params{onhand};
26
27   my @selects = (
28     'SUM(qty) AS qty',
29     'MIN(EXTRACT(epoch FROM inventory.itime)) AS itime',
30   );
31   my @values;
32   my @where;
33   my @groups;
34
35   if ($params{part}) {
36     my @ids = map { ref $_ ? $_->id : $_ } listify($params{part});
37     push @where, sprintf "parts_id IN (%s)", join ', ', ("?") x @ids;
38     push @values, @ids;
39   }
40
41   if ($params{bin}) {
42     my @ids = map { ref $_ ? $_->id : $_ } listify($params{bin});
43     push @where, sprintf "bin_id IN (%s)", join ', ', ("?") x @ids;
44     push @values, @ids;
45   }
46
47   if ($params{warehouse}) {
48     my @ids = map { ref $_ ? $_->id : $_ } listify($params{warehouse});
49     push @where, sprintf "warehouse.id IN (%s)", join ', ', ("?") x @ids;
50     push @values, @ids;
51   }
52
53   if ($params{chargenumber}) {
54     my @ids = listify($params{chargenumber});
55     push @where, sprintf "chargenumber IN (%s)", join ', ', ("?") x @ids;
56     push @values, @ids;
57   }
58
59   if ($params{date}) {
60     push @where, sprintf "shippingdate <= ?";
61     push @values, $params{date};
62   }
63
64   if ($params{bestbefore}) {
65     push @where, sprintf "bestbefore >= ?";
66     push @values, $params{bestbefore};
67   }
68
69   # reserve_warehouse
70   if ($params{onhand} && !$params{warehouse}) {
71     push @where, 'NOT warehouse.forreserve';
72   }
73
74   # reserve_for
75   if ($params{onhand} && !$params{reserve_for}) {
76     push @where, 'reserve_for_id IS NULL AND reserve_for_table IS NULL';
77   }
78
79   if ($params{reserve_for}) {
80     my @objects = listify($params{chargenumber});
81     my @tokens;
82     push @tokens, ( "(reserve_for_id = ? AND reserve_for_table = ?)") x @objects;
83     push @values, map { ($_->id, $_->meta->table) } @objects;
84     push @where, '(' . join(' OR ', @tokens) . ')';
85   }
86
87   # by
88   my %allowed_by = (
89     part          => [ qw(parts_id) ],
90     bin           => [ qw(bin_id inventory.warehouse_id warehouse.forreserve)],
91     warehouse     => [ qw(inventory.warehouse_id warehouse.forreserve) ],
92     chargenumber  => [ qw(chargenumber) ],
93     bestbefore    => [ qw(bestbefore) ],
94     reserve_for   => [ qw(reserve_for_id reserve_for_table) ],
95     for_allocate  => [ qw(parts_id bin_id inventory.warehouse_id warehouse.forreserve chargenumber bestbefore reserve_for_id reserve_for_table) ],
96   );
97
98   if ($params{by}) {
99     for (listify($params{by})) {
100       my $selects = $allowed_by{$_} or Carp::croak("unknown option for by: $_");
101       push @selects, @$selects;
102       push @groups,  @$selects;
103     }
104   }
105
106   my $select   = join ',', @selects;
107   my $where    = @where  ? 'WHERE ' . join ' AND ', @where : '';
108   my $group_by = @groups ? 'GROUP BY ' . join ', ', @groups : '';
109
110   my $query = <<"";
111     SELECT $select FROM inventory
112     LEFT JOIN bin ON bin_id = bin.id
113     LEFT JOIN warehouse ON bin.warehouse_id = warehouse.id
114     $where
115     $group_by
116     HAVING SUM(qty) > 0
117
118   my $results = selectall_hashref_query($::form, SL::DB->client->dbh, $query, @values);
119
120   my %with_objects = (
121     part         => 'SL::DB::Manager::Part',
122     bin          => 'SL::DB::Manager::Bin',
123     warehouse    => 'SL::DB::Manager::Warehouse',
124     reserve_for  => undef,
125   );
126
127   my %slots = (
128     part      =>  'parts_id',
129     bin       =>  'bin_id',
130     warehouse =>  'warehouse_id',
131   );
132
133   if ($params{by} && $params{with_objects}) {
134     for my $with_object (listify($params{with_objects})) {
135       Carp::croak("unknown with_object $with_object") if !exists $with_objects{$with_object};
136
137       if (my $manager = $with_objects{$with_object}) {
138         my $slot = $slots{$with_object};
139         next if !(my @ids = map { $_->{$slot} } @$results);
140         my $objects = $manager->get_all(query => [ id => \@ids ]);
141         my %objects_by_id = map { $_->id => $_ } @$objects;
142
143         $_->{$with_object} = $objects_by_id{$_->{$slot}} for @$results;
144       } else {
145         # need to fetch all reserve_for_table partitions
146       }
147     }
148   }
149
150   if ($params{by}) {
151     return $results;
152   } else {
153     return $results->[0]{qty};
154   }
155 }
156
157 sub get_stock {
158   _get_stock_onhand(@_, onhand => 0);
159 }
160
161 sub get_onhand {
162   _get_stock_onhand(@_, onhand => 1);
163 }
164
165 sub allocate {
166   my (%params) = @_;
167
168   my $part = $params{part} or Carp::croak('allocate needs a part');
169   my $qty  = $params{qty}  or Carp::croak('allocate needs a qty');
170
171   return () if $qty <= 0;
172
173   my $results = get_stock(part => $part, by => 'for_allocate');
174   my %bin_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{bin});
175   my %wh_whitelist  = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{warehouse});
176   my %chargenumbers = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{chargenumber});
177   my %reserve_whitelist;
178   if ($params{reserve_for}) {
179     $reserve_whitelist{ $_->meta->table }{ $_->id } = 1 for listify($params{reserve_for});
180   }
181
182   # filter the results. we don't want:
183   # - negative amounts
184   # - bins that are reserve but not in the white-list of warehouses or bins
185   # - reservations that are not white-listed
186
187   my @filtered_results = grep {
188        (!$_->{forreserve} || $bin_whitelist{$_->{bin_id}} || $wh_whitelist{$_->{warehouse_id}})
189     && (!$_->{reserve_for_id} || $reserve_whitelist{ $_->{reserve_for_table} }{ $_->{reserve_for_id} })
190   } @$results;
191
192   # sort results so that reserve_for is first, then chargenumbers, then wanted bins, then wanted warehouses
193   my @sorted_results = sort {
194        (!!$b->{reserve_for_id})    <=> (!!$a->{reserve_for_id})                   # sort by existing reserve_for_id first.
195     || exists $chargenumbers{$b->{chargenumber}}  <=> exists $chargenumbers{$a->{chargenumber}} # then prefer wanted chargenumbers
196     || exists $bin_whitelist{$b->{bin_id}}        <=> exists $bin_whitelist{$a->{bin_id}}       # then prefer wanted bins
197     || exists $wh_whitelist{$b->{warehouse_id}}   <=> exists $wh_whitelist{$a->{warehouse_id}}  # then prefer wanted bins
198     || $a->{itime}                                <=> $b->{itime}                               # and finally prefer earlier charges
199   } @filtered_results;
200   my @allocations;
201   my $rest_qty = $qty;
202
203   for my $chunk (@sorted_results) {
204     my $qty = min($chunk->{qty}, $rest_qty);
205     if ($qty > 0) {
206       push @allocations, SL::Helper::Inventory::Allocation->new(
207         parts_id          => $chunk->{parts_id},
208         qty               => $qty,
209         comment           => $params{comment},
210         bin_id            => $chunk->{bin_id},
211         warehouse_id      => $chunk->{warehouse_id},
212         chargenumber      => $chunk->{chargenumber},
213         bestbefore        => $chunk->{bestbefore},
214         reserve_for_id    => $chunk->{reserve_for_id},
215         reserve_for_table => $chunk->{reserve_for_table},
216         for_object_id     => undef,
217       );
218       $rest_qty -=  _round_qty($qty);
219     }
220     $rest_qty = _round_qty($rest_qty);
221     last if $rest_qty == 0;
222   }
223   if ($rest_qty > 0) {
224     die SL::X::Inventory::Allocation->new(
225       error => 'not enough to allocate',
226       msg => t8("can not allocate #1 units of #2, missing #3 units", _qty($qty), $part->displayable_name, _qty($rest_qty)),
227     );
228   } else {
229     if ($params{constraints}) {
230       check_constraints($params{constraints},\@allocations);
231     }
232     return @allocations;
233   }
234 }
235
236 sub allocate_for_assembly {
237   my (%params) = @_;
238
239   my $part = $params{part} or Carp::croak('allocate needs a part');
240   my $qty  = $params{qty}  or Carp::croak('allocate needs a qty');
241
242   Carp::croak('not an assembly') unless $part->is_assembly;
243
244   my %parts_to_allocate;
245
246   for my $assembly ($part->assemblies) {
247     next if $assembly->part->dispotype eq 'no_stock';
248
249     my $tmpqty = $assembly->assembly_part->is_recipe   ? $assembly->qty * $qty / $assembly->assembly_part->scalebasis
250                : $assembly->part->unit eq 'Stck' ? ceil($assembly->qty * $qty)
251                : $assembly->qty * $qty;
252     $parts_to_allocate{ $assembly->part->id } //= 0;
253     $parts_to_allocate{ $assembly->part->id } += $tmpqty;
254   }
255
256   my @allocations;
257
258   for my $part_id (keys %parts_to_allocate) {
259     my $part = SL::DB::Part->load_cached($part_id);
260     push @allocations, allocate(%params, part => $part, qty => $parts_to_allocate{$part_id});
261   }
262
263   @allocations;
264 }
265
266 sub check_constraints {
267   my ($constraints, $allocations) = @_;
268   if ('CODE' eq ref $constraints) {
269     if (!$constraints->(@$allocations)) {
270       die SL::X::Inventory::Allocation->new(
271         error => 'allocation constraints failure',
272         msg => t8("Allocations didn't pass constraints"),
273       );
274     }
275   } else {
276     croak 'constraints needs to be a hashref' unless 'HASH' eq ref $constraints;
277
278     my %supported_constraints = (
279       bin_id       => 'bin_id',
280       warehouse_id => 'warehouse_id',
281       chargenumber => 'chargenumber',
282     );
283
284     for (keys %$constraints ) {
285       croak "unsupported constraint '$_'" unless $supported_constraints{$_};
286
287       my %whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($constraints->{$_});
288       my $accessor = $supported_constraints{$_};
289
290       if (any { !$whitelist{$_->$accessor} } @$allocations) {
291         my %error_constraints = (
292           bin_id       => t8('Bins'),
293           warehouse_id => t8('Warehouses'),
294           chargenumber => t8('Chargenumbers'),
295         );
296         my @allocs = grep { $whitelist{$_->$accessor} } @$allocations;
297         my $needed = sum map { $_->qty } grep { !$whitelist{$_->$accessor} } @$allocations;
298         my $err    = t8("Cannot allocate parts.");
299         $err      .= ' '.t8('part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.',
300               SL::DB::Part->load_cached($_->parts_id)->description,
301               SL::DB::Bin->load_cached($_->bin_id)->full_description,
302               _qty($_->qty), _qty($needed), $_->chargenumber ? $_->chargenumber : '--') for @allocs;
303         die SL::X::Inventory::Allocation->new(
304           error => 'allocation constraints failure',
305           msg   => $err,
306         );
307       }
308     }
309   }
310 }
311
312 sub produce_assembly {
313   my (%params) = @_;
314
315   my $part = $params{part} or Carp::croak('produce_assembly needs a part');
316   my $qty  = $params{qty}  or Carp::croak('produce_assembly needs a qty');
317
318   my $allocations = $params{allocations};
319   if ($params{auto_allocate}) {
320     Carp::croak("produce_assembly: can't have both allocations and auto_allocate") if $params{allocations};
321     $allocations = [ allocate_for_assembly(part => $part, qty => $qty) ];
322   } else {
323     Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations};
324     $allocations = $params{allocations};
325   }
326
327   my $bin          = $params{bin} or Carp::croak("need target bin");
328   my $chargenumber = $params{chargenumber};
329   my $bestbefore   = $params{bestbefore};
330   my $for_object_id = $params{for_object_id};
331   my $comment      = $params{comment} // '';
332
333   my $production_order_item = $params{production_order_item};
334   my $invoice               = $params{invoice};
335   my $project               = $params{project};
336   my $reserve_for           = $params{reserve_for};
337
338   my $reserve_for_id    = $reserve_for ? $reserve_for->id          : undef;
339   my $reserve_for_table = $reserve_for ? $reserve_for->meta->table : undef;
340
341   my $shippingdate = $params{shippingsdate} // DateTime->now_local;
342
343   my $trans_id              = $params{trans_id};
344   ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id;
345
346   my $trans_type_out = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used');
347   my $trans_type_in  = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'assembled');
348
349   # check whether allocations are sane
350   if (!$params{no_check_allocations} && !$params{auto_allocate}) {
351     my %allocations_by_part = map { $_->parts_id  => $_->qty } @$allocations;
352     for my $assembly ($part->assemblies) {
353       $allocations_by_part{ $assembly->parts_id } -= $assembly->qty * $qty; # TODO recipe factor
354     }
355
356     die "allocations are insufficient for production" if any { $_ < 0 } values %allocations_by_part;
357   }
358
359   my @transfers;
360   for my $allocation (@$allocations) {
361     my $oe_id = delete $allocation->{for_object_id};
362     push @transfers, SL::DB::Inventory->new(
363       trans_id     => $trans_id,
364       %$allocation,
365       qty          => -$allocation->qty,
366       trans_type   => $trans_type_out,
367       shippingdate => $shippingdate,
368       employee     => SL::DB::Manager::Employee->current,
369       oe_id        => $allocation->for_object_id,
370     );
371   }
372
373   push @transfers, SL::DB::Inventory->new(
374     trans_id          => $trans_id,
375     trans_type        => $trans_type_in,
376     part              => $part,
377     qty               => $qty,
378     bin               => $bin,
379     warehouse         => $bin->warehouse_id,
380     chargenumber      => $chargenumber,
381     bestbefore        => $bestbefore,
382     reserve_for_id    => $reserve_for_id,
383     reserve_for_table => $reserve_for_table,
384     shippingdate      => $shippingdate,
385     project           => $project,
386     invoice           => $invoice,
387     comment           => $comment,
388     prod              => $production_order_item,
389     employee          => SL::DB::Manager::Employee->current,
390     oe_id             => $for_object_id,
391   );
392
393   SL::DB->client->with_transaction(sub {
394     $_->save for @transfers;
395     1;
396   }) or do {
397     die SL::DB->client->error;
398   };
399
400   @transfers;
401 }
402
403 package SL::Helper::Inventory::Allocation {
404   my @attributes = qw(parts_id qty bin_id warehouse_id chargenumber bestbefore comment reserve_for_id reserve_for_table for_object_id);
405   my %attributes = map { $_ => 1 } @attributes;
406
407   for my $name (@attributes) {
408     no strict 'refs';
409     *{"SL::Helper::Inventory::Allocation::$name"} = sub { $_[0]{$name} };
410   }
411
412   sub new {
413     my ($class, %params) = @_;
414
415     Carp::croak("missing attribute $_") for grep { !exists $params{$_}     } @attributes;
416     Carp::croak("unknown attribute $_") for grep { !exists $attributes{$_} } keys %params;
417     Carp::croak("$_ must be set")       for grep { !$params{$_} } qw(parts_id qty bin_id);
418     Carp::croak("$_ must be positive")  for grep { !($params{$_} > 0) } qw(parts_id qty bin_id);
419
420     bless { %params }, $class;
421   }
422 }
423
424 1;
425
426 =encoding utf-8
427
428 =head1 NAME
429
430 SL::WH - Warehouse and Inventory API
431
432 =head1 SYNOPSIS
433
434   # See description for an intro to the concepts used here.
435
436   use SL::Helper::Inventory qw(:ALL);
437
438   # stock, get "what's there" for a part with various conditions:
439   my $qty = get_stock(part => $part);                              # how much is on stock?
440   my $qty = get_stock(part => $part, date => $date);               # how much was on stock at a specific time?
441   my $qty = get_stock(part => $part, bin => $bin);                 # how is on stock in a specific bin?
442   my $qty = get_stock(part => $part, warehouse => $warehouse);     # how is on stock in a specific warehouse?
443   my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how is on stock of a specific chargenumber?
444
445   # onhand, get "what's available" for a part with various conditions:
446   my $qty = get_onhand(part => $part);                              # how much is available?
447   my $qty = get_onhand(part => $part, date => $date);               # how much was available at a specific time?
448   my $qty = get_onhand(part => $part, bin => $bin);                 # how much is available in a specific bin?
449   my $qty = get_onhand(part => $part, warehouse => $warehouse);     # how much is available in a specific warehouse?
450   my $qty = get_onhand(part => $part, chargenumber => $chargenumber); # how much is availbale of a specific chargenumber?
451   my $qty = get_onhand(part => $part, reserve_for => $order);       # how much is available if you include this reservation?
452
453   # onhand batch mode:
454   my $data = get_onhand(
455     warehouse    => $warehouse,
456     by           => [ qw(bin part chargenumber reserve_for) ],
457     with_objects => [ qw(bin part) ],
458   );
459
460   # allocate:
461   my @allocations, allocate(
462     part         => $part,          # part_id works too
463     qty          => $qty,           # must be positive
464     chargenumber => $chargenumber,  # optional, may be arrayref. if provided these charges will be used first
465     bestbefore   => $datetime,      # optional, defaults to today. items with bestbefore prior to that date wont be used
466     reserve_for  => $object,        # optional, may be arrayref. if provided the qtys reserved for these objects will be used first
467     bin          => $bin,           # optional, may be arrayref. if provided
468   );
469
470   # shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate
471   my @allocations, allocate_for_assembly(
472     part         => $assembly,      # part_id works too
473     qty          => $qty,           # must be positive
474   );
475
476   # create allocation manually, bypassing checks, all of these need to be passed, even undefs
477   my $allocation = SL::Helper::Inventory::Allocation->new(
478     part_id           => $part->id,
479     qty               => 15,
480     bin_id            => $bin_obj->id,
481     warehouse_id      => $bin_obj->warehouse_id,
482     chargenumber      => '1823772365',
483     bestbefore        => undef,
484     reserve_for_id    => undef,
485     reserve_for_table => undef,
486     for_object_id     => $order->id,
487   );
488
489   # produce_assembly:
490   produce_assembly(
491     part         => $part,           # target assembly
492     qty          => $qty,            # qty
493     allocations  => \@allocations,   # allocations to use. alternatively use "auto_allocate => 1,"
494
495     # where to put it
496     bin          => $bin,           # needed unless a global standard target is configured
497     chargenumber => $chargenumber,  # optional
498     bestbefore   => $datetime,      # optional
499     comment      => $comment,       # optional
500
501     # links, all optional
502     production_order_item => $item,
503     reserve_for           => $object,
504   );
505
506 =head1 DESCRIPTION
507
508 New functions for the warehouse and inventory api.
509
510 The WH api currently has three large shortcomings. It is very hard to just get
511 the current stock for an item, it's extremely complicated to use it to produce
512 assemblies while ensuring that no stock ends up negative, and it's very hard to
513 use it to get an overview over the actual contents of the inventory.
514
515 The first problem has spawned several dozen small functions in the program that
516 try to implement that, and those usually miss some details. They may ignore
517 reservations, or reserve warehouses, or bestbefore times.
518
519 To get this cleaned up a bit this code introduces two concepts: stock and onhand.
520
521 Stock is defined as the actual contents of the inventory, everything that is
522 there. Onhand is what is available, which means things that are stocked and not
523 reserved and not expired.
524
525 The two new functions C<get_stock> and C<get_onhand> encapsulate these principles and
526 allow simple access with some optional filters for chargenumbers or warehouses.
527 Both of them have a batch mode that can be used to get these information to
528 supllement smiple reports.
529
530 To address the safe assembly creation a new function has been added.
531 C<allocate> will try to find the requested quantity of a part in the inventory
532 and will return allocations of it which can then be used to create the
533 assembly. Allocation will happen with the C<onhand> semantics defined above,
534 meaning that by default no reservations or expired goods will be used. The
535 caller can supply hints of what shold be used and in those cases chargenumber
536 and reservations will be used up as much as possible first.  C<allocate> will
537 always try to fulfil the request even beyond those. Should the required amount
538 not be stocked, allocate will throw an exception.
539
540 C<produce_assembly> has been rewritten to only accept parameters about the
541 target of the production, and requires allocations to complete the request. The
542 allocations can be supplied manually, or can be generated automatically.
543 C<produce_assembly> will check whether enough allocations are given to create
544 the recipe, but will not check whether the allocations are backed. If the
545 allocations are not sufficient or if the auto-allocation fails an exception
546 is returned. If you need to produce something that is not in the inventory, you
547 can bypass those checks by creating the allocations yourself (see
548 L</"ALLOCATION DATA STRUCTURE">).
549
550 Note: this is only intended to cover the scenarios described above. For other cases:
551
552 =over 4
553
554 =item *
555
556 If you need the reserved amount for an order use C<SL::DB::Helper::Reservation>
557 instead.
558
559 =item *
560
561 If you need actual inventory objects because of record links, prod_id links or
562 something like that load them directly. And strongly consider redesigning that,
563 because it's really fragile.
564
565 =item *
566
567 You need weight or accounting information you're on your own. The inventory api
568 only concerns itself with the raw quantities.
569
570 =item *
571
572 If you need the first stock date of parts, or anything related to a specific
573 transfer type or direction, this is not covered yet.
574
575 =back
576
577 =head1 FUNCTIONS
578
579 =over 4
580
581 =item * get_stock PARAMS
582
583 Returns for single parts how much actually exists in the inventory.
584
585 Options:
586
587 =over 4
588
589 =item * part
590
591 The part. Must be present without C<by>. May be arrayref with C<by>. Can be object or id.
592
593 =item * bin
594
595 If given, will only return stock on these bins. Optional. May be array, May be object or id.
596
597 =item * warehouse
598
599 If given, will only return stock on these warehouses. Optional. May be array, May be object or id.
600
601 =item * date
602
603 If given, will return stock as it were on this timestamp. Optional. Must be L<DateTime> object.
604
605 =item * chargenumber
606
607 If given, will only show stock with this chargenumber. Optional. May be array.
608
609 =item * by
610
611 See L</"STOCK/ONHAND REPORT MODE">
612
613 =item * with_objects
614
615 See L</"STOCK/ONHAND REPORT MODE">
616
617 =back
618
619 Will return a single qty normally, see L</"STOCK/ONHAND REPORT MODE"> for batch
620 mode when C<by> is given.
621
622 =item * get_onhand PARAMS
623
624 Returns for single parts how much is available in the inventory. That excludes:
625 reserved quantities, reserved warehouses and stock with expired bestbefore.
626
627 It takes all options of L</get_stock> but treats some of the differently and has some additional ones:
628
629 =over 4
630
631 =item * warehouse
632
633 Usually C<onhand> will not include results from warehouses with the C<reserve>
634 flag. However giving an explicit list of warehouses will include there in the
635 search, as well as all others.
636
637 =item * reserve_for
638
639 =item * reserve_warehouse
640
641 =item * bestbefore
642
643 =back
644
645 =item * allocate PARAMS
646
647 Accepted parameters:
648
649 =over 4
650
651 =item * part
652
653 =item * qty
654
655 =item * bin
656
657 Bin object. Optional.
658
659 =item * warehouse
660
661 Warehouse object. Optional.
662
663 =item * chargenumber
664
665 Optional.
666
667 =item * bestbefore
668
669 Datetime. Optional.
670
671 =item * reserve_for
672
673 Needs to be a rose object, where id and table can be extracted. Optional.
674
675 =back
676
677 Tries to allocate the required quantity using what is currently onhand. If
678 given any of C<bin>, C<warehouse>, C<chargenumber>, C<reserve_for>
679
680
681 =item * allocate_for_assembly PARAMS
682
683 Shortcut to allocate everything for an assembly. Takes the same arguments. Will
684 compute the required amount for each assembly part and allocate all of them.
685
686 =item * produce_assembly
687
688
689 =back
690
691 =head1 STOCK/ONHAND REPORT MODE
692
693 If the special option C<by> is given with an arrayref, the result will instead
694 be an arrayref of partitioned stocks by those fields. Valid partitions are:
695
696 =over 4
697
698 =item * part
699
700 If this is given, part is optional in the parameters
701
702 =item * bin
703
704 =item * warehouse
705
706 =item * chargenumber
707
708 =item * bestbefore
709
710 =item * reserve_for
711
712 =back
713
714 Note: If you want to use the returned data to create allocations you I<need> to
715 enable all of these. To make this easier a special shortcut exists
716
717 In this mode, C<with_objects> can be used to load C<warehouse>, C<bin>,
718 C<parts>, and the C<reserve_for> objects in one go, just like with Rose. They
719 need to be present in C<by> before that though.
720
721 =head1 ALLOCATION ALGORITHM
722
723 When calling allocate, the current onhand (== available stock) of the item will
724 be used to decide which bins/chargenumbers/bestbefore can be used.
725
726 In general allocate will try to make the request happen, and will use the
727 provided charges up first, and then tap everything else. If you need to only
728 I<exactly> use the provided charges, you'll need to craft the allocations
729 yourself. See L</"ALLOCATION DATA STRUCTURE"> for that.
730
731 If C<reserve_for> is given, those will be used up first too.
732
733 If C<reserved_warehouse> is given, those will be used up second.
734
735 If C<chargenumber> is given, those will be used up next.
736
737 After that normal quantities will be used.
738
739 These are tiebreakers and expected to rarely matter in reality. If you need
740 finegrained control over which allocation is used, you may want to get the
741 onhands yourself and select the appropriate ones.
742
743 Only quantities with C<bestbefore> unset or after the given date will be
744 considered. If more than one charge is eligible, the earlier C<bestbefore>
745 will be used.
746
747 Allocations do NOT have an internal memory and can't react to other allocations
748 of the same part earlier. Never double allocate the same part within a
749 transaction.
750
751 =head1 ALLOCATION DATA STRUCTURE
752
753 Allocations are instances of the helper class C<SL::Helper::Inventory::Allocation>. They require
754 each of the following attributes to be set at creation time:
755
756 =over 4
757
758 =item * parts_id
759
760 =item * qty
761
762 =item * bin_id
763
764 =item * warehouse_id
765
766 =item * chargenumber
767
768 =item * bestbefore
769
770 =item * reserve_for_id
771
772 =item * reserve_for_table
773
774 =item * for_object_id
775
776 If set the allocations will be marked as allocated for the given object.
777 If these allocations are later used to produce an assembly, the resulting
778 consuming transactions will be marked as belonging to the given object.
779 The object may be an order, productionorder or other objects
780
781 =back
782
783 C<chargenumber>, C<bestbefore>, C<reserve_for_id>, C<reserve_for_table> and
784 C<for_object_id> may be C<undef> (but must still be present at creation time).
785 Instances are considered immutable.
786
787
788 =head1 CONSTRAINTS
789
790   # whitelist constraints
791   ->allocate(
792     ...
793     constraints => {
794       bin_id       => \@allowed_bins,
795       chargenumber => \@allowed_chargenumbers,
796     }
797   );
798
799   # custom constraints
800   ->allocate(
801     constraints => sub {
802       # only allow chargenumbers with specific format
803       all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_
804
805       &&
806       # and must be all reservations
807       all { $_->reserve_for_id } @_;
808     }
809   )
810
811 C<allocation> is "best effort" in nature. It will take the C<bin>,
812 C<chargenumber> etc hints from the parameters, but will try it's bvest to
813 fulfil the request anyway and only bail out if it is absolutely not possible.
814
815 Sometimes you need to restrict allocations though. For this you can pass
816 additional constraints to C<allocate>. A constraint serves as a whitelist.
817 Every allocation must fulfil every constraint by having that attribute be one
818 of the given values.
819
820 In case even that is not enough, you may supply a custom check by passing a
821 function that will be given the allocation objects.
822
823 Note that both whitelists and constraints do not influence the order of
824 allocations, which is done purely from the initial parameters. They only serve
825 to reject allocations made in good faith which do fulfil required assertions.
826
827 =head1 ERROR HANDLING
828
829 C<allocate> and C<produce_assembly> will throw exceptions if the request can
830 not be completed. The usual reason will be insufficient onhand to allocate, or
831 insufficient allocations to process the request.
832
833 =head1 TODO
834
835   * define and describe error classes
836   * define wrapper classes for stock/onhand batch mode return values
837   * handle extra arguments in produce: shippingdate, project, oe
838   * clean up allocation helper class
839   * with objects for reservations
840   * document no_ check
841   * tests
842
843 =head1 BUGS
844
845 None yet :)
846
847 =head1 AUTHOR
848
849 Sven Schöling E<lt>sven.schoeling@opendynamic.deE<gt>
850
851 =cut