Inventory-Helper: Constraints erweitert mit "reserve_for"
[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     Carp::croak("not DateTime ".$params{date}) unless ref($params{date}) eq 'DateTime';
61     push @where, sprintf "shippingdate <= ?";
62     push @values, $params{date};
63   }
64
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};
69   }
70
71   # reserve_warehouse
72   if ($params{onhand} && !$params{warehouse}) {
73     push @where, 'NOT warehouse.forreserve';
74   }
75
76   # reserve_for
77   if ($params{onhand} && !$params{reserve_for}) {
78     push @where, 'reserve_for_id IS NULL AND reserve_for_table IS NULL';
79   }
80
81   if ($params{reserve_for}) {
82     my @objects = listify($params{reserve_for});
83     my @tokens;
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) . ')';
87   }
88
89   # by
90   my %allowed_by = (
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) ],
98   );
99
100   if ($params{by}) {
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;
105     }
106   }
107
108   my $select   = join ',', @selects;
109   my $where    = @where  ? 'WHERE ' . join ' AND ', @where : '';
110   my $group_by = @groups ? 'GROUP BY ' . join ', ', @groups : '';
111
112   my $query = <<"";
113     SELECT $select FROM inventory
114     LEFT JOIN bin ON bin_id = bin.id
115     LEFT JOIN warehouse ON bin.warehouse_id = warehouse.id
116     $where
117     $group_by
118     HAVING SUM(qty) > 0
119
120   my $results = selectall_hashref_query($::form, SL::DB->client->dbh, $query, @values);
121
122   my %with_objects = (
123     part         => 'SL::DB::Manager::Part',
124     bin          => 'SL::DB::Manager::Bin',
125     warehouse    => 'SL::DB::Manager::Warehouse',
126     reserve_for  => undef,
127   );
128
129   my %slots = (
130     part      =>  'parts_id',
131     bin       =>  'bin_id',
132     warehouse =>  'warehouse_id',
133   );
134
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};
138
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;
144
145         $_->{$with_object} = $objects_by_id{$_->{$slot}} for @$results;
146       } else {
147         # need to fetch all reserve_for_table partitions
148       }
149     }
150   }
151
152   if ($params{by}) {
153     return $results;
154   } else {
155     return $results->[0]{qty};
156   }
157 }
158
159 sub get_stock {
160   _get_stock_onhand(@_, onhand => 0);
161 }
162
163 sub get_onhand {
164   _get_stock_onhand(@_, onhand => 1);
165 }
166
167 sub allocate {
168   my (%params) = @_;
169
170   my $part = $params{part} or Carp::croak('allocate needs a part');
171   my $qty  = $params{qty}  or Carp::croak('allocate needs a qty');
172
173   return () if $qty <= 0;
174
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});
182   }
183
184   # filter the results. we don't want:
185   # - negative amounts
186   # - bins that are reserve but not in the white-list of warehouses or bins
187   # - reservations that are not white-listed
188
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} })
192   } @$results;
193
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
201   } @filtered_results;
202   my @allocations;
203   my $rest_qty = $qty;
204
205   for my $chunk (@sorted_results) {
206     my $qty = min($chunk->{qty}, $rest_qty);
207     if ($qty > 0) {
208       push @allocations, SL::Helper::Inventory::Allocation->new(
209         parts_id          => $chunk->{parts_id},
210         qty               => $qty,
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,
219       );
220       $rest_qty -=  _round_qty($qty);
221     }
222     $rest_qty = _round_qty($rest_qty);
223     last if $rest_qty == 0;
224   }
225   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)),
229     );
230   } else {
231     if ($params{constraints}) {
232       check_constraints($params{constraints},\@allocations);
233     }
234     return @allocations;
235   }
236 }
237
238 sub allocate_for_assembly {
239   my (%params) = @_;
240
241   my $part = $params{part} or Carp::croak('allocate needs a part');
242   my $qty  = $params{qty}  or Carp::croak('allocate needs a qty');
243
244   Carp::croak('not an assembly') unless $part->is_assembly;
245
246   my %parts_to_allocate;
247
248   for my $assembly ($part->assemblies) {
249     next if $assembly->part->dispotype eq 'no_stock';
250
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;
256   }
257
258   my @allocations;
259
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});
263   }
264
265   @allocations;
266 }
267
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"),
275       );
276     }
277   } else {
278     croak 'constraints needs to be a hashref' unless 'HASH' eq ref $constraints;
279
280     my %supported_constraints = (
281       bin_id       => 'bin_id',
282       warehouse_id => 'warehouse_id',
283       chargenumber => 'chargenumber',
284       reserve_for  => 'reserve_for_id',
285     );
286
287     for (keys %$constraints ) {
288       croak "unsupported constraint '$_'" unless $supported_constraints{$_};
289       next unless defined $constraints->{$_};
290
291       my %whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($constraints->{$_});
292       my $accessor = $supported_constraints{$_};
293
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'),
300         );
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',
310           msg   => $err,
311         );
312       }
313     }
314   }
315 }
316
317 sub produce_assembly {
318   my (%params) = @_;
319
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');
322
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) ];
327   } else {
328     Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations};
329     $allocations = $params{allocations};
330   }
331
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} // '';
337
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};
342
343   my $reserve_for_id    = $reserve_for ? $reserve_for->id          : undef;
344   my $reserve_for_table = $reserve_for ? $reserve_for->meta->table : undef;
345
346   my $shippingdate = $params{shippingsdate} // DateTime->now_local;
347
348   my $trans_id              = $params{trans_id};
349   ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id;
350
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');
353
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
359     }
360
361     die "allocations are insufficient for production" if any { $_ < 0 } values %allocations_by_part;
362   }
363
364   my @transfers;
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,
369       %$allocation,
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,
375     );
376   }
377
378   push @transfers, SL::DB::Inventory->new(
379     trans_id          => $trans_id,
380     trans_type        => $trans_type_in,
381     part              => $part,
382     qty               => $qty,
383     bin               => $bin,
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,
390     project           => $project,
391     invoice           => $invoice,
392     comment           => $comment,
393     prod              => $production_order_item,
394     employee          => SL::DB::Manager::Employee->current,
395     oe_id             => $for_object_id,
396   );
397
398   SL::DB->client->with_transaction(sub {
399     $_->save for @transfers;
400     1;
401   }) or do {
402     die SL::DB->client->error;
403   };
404
405   @transfers;
406 }
407
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;
411
412   for my $name (@attributes) {
413     no strict 'refs';
414     *{"SL::Helper::Inventory::Allocation::$name"} = sub { $_[0]{$name} };
415   }
416
417   sub new {
418     my ($class, %params) = @_;
419
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);
424
425     bless { %params }, $class;
426   }
427 }
428
429 1;
430
431 =encoding utf-8
432
433 =head1 NAME
434
435 SL::WH - Warehouse and Inventory API
436
437 =head1 SYNOPSIS
438
439   # See description for an intro to the concepts used here.
440
441   use SL::Helper::Inventory qw(:ALL);
442
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?
449
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?
457
458   # onhand batch mode:
459   my $data = get_onhand(
460     warehouse    => $warehouse,
461     by           => [ qw(bin part chargenumber reserve_for) ],
462     with_objects => [ qw(bin part) ],
463   );
464
465   # allocate:
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
473   );
474
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
479   );
480
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,
484     qty               => 15,
485     bin_id            => $bin_obj->id,
486     warehouse_id      => $bin_obj->warehouse_id,
487     chargenumber      => '1823772365',
488     bestbefore        => undef,
489     reserve_for_id    => undef,
490     reserve_for_table => undef,
491     for_object_id     => $order->id,
492   );
493
494   # produce_assembly:
495   produce_assembly(
496     part         => $part,           # target assembly
497     qty          => $qty,            # qty
498     allocations  => \@allocations,   # allocations to use. alternatively use "auto_allocate => 1,"
499
500     # where to put it
501     bin          => $bin,           # needed unless a global standard target is configured
502     chargenumber => $chargenumber,  # optional
503     bestbefore   => $datetime,      # optional
504     comment      => $comment,       # optional
505
506     # links, all optional
507     production_order_item => $item,
508     reserve_for           => $object,
509   );
510
511 =head1 DESCRIPTION
512
513 New functions for the warehouse and inventory api.
514
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.
519
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.
523
524 To get this cleaned up a bit this code introduces two concepts: stock and onhand.
525
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.
529
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.
534
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.
544
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">).
554
555 Note: this is only intended to cover the scenarios described above. For other cases:
556
557 =over 4
558
559 =item *
560
561 If you need the reserved amount for an order use C<SL::DB::Helper::Reservation>
562 instead.
563
564 =item *
565
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.
569
570 =item *
571
572 You need weight or accounting information you're on your own. The inventory api
573 only concerns itself with the raw quantities.
574
575 =item *
576
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.
579
580 =back
581
582 =head1 FUNCTIONS
583
584 =over 4
585
586 =item * get_stock PARAMS
587
588 Returns for single parts how much actually exists in the inventory.
589
590 Options:
591
592 =over 4
593
594 =item * part
595
596 The part. Must be present without C<by>. May be arrayref with C<by>. Can be object or id.
597
598 =item * bin
599
600 If given, will only return stock on these bins. Optional. May be array, May be object or id.
601
602 =item * warehouse
603
604 If given, will only return stock on these warehouses. Optional. May be array, May be object or id.
605
606 =item * date
607
608 If given, will return stock as it were on this timestamp. Optional. Must be L<DateTime> object.
609
610 =item * chargenumber
611
612 If given, will only show stock with this chargenumber. Optional. May be array.
613
614 =item * by
615
616 See L</"STOCK/ONHAND REPORT MODE">
617
618 =item * with_objects
619
620 See L</"STOCK/ONHAND REPORT MODE">
621
622 =back
623
624 Will return a single qty normally, see L</"STOCK/ONHAND REPORT MODE"> for batch
625 mode when C<by> is given.
626
627 =item * get_onhand PARAMS
628
629 Returns for single parts how much is available in the inventory. That excludes:
630 reserved quantities, reserved warehouses and stock with expired bestbefore.
631
632 It takes all options of L</get_stock> but treats some of the differently and has some additional ones:
633
634 =over 4
635
636 =item * warehouse
637
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.
641
642 =item * reserve_for
643
644 =item * reserve_warehouse
645
646 =item * bestbefore
647
648 =back
649
650 =item * allocate PARAMS
651
652 Accepted parameters:
653
654 =over 4
655
656 =item * part
657
658 =item * qty
659
660 =item * bin
661
662 Bin object. Optional.
663
664 =item * warehouse
665
666 Warehouse object. Optional.
667
668 =item * chargenumber
669
670 Optional.
671
672 =item * bestbefore
673
674 Datetime. Optional.
675
676 =item * reserve_for
677
678 Needs to be a rose object, where id and table can be extracted. Optional.
679
680 =back
681
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>
684
685
686 =item * allocate_for_assembly PARAMS
687
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.
690
691 =item * produce_assembly
692
693
694 =back
695
696 =head1 STOCK/ONHAND REPORT MODE
697
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:
700
701 =over 4
702
703 =item * part
704
705 If this is given, part is optional in the parameters
706
707 =item * bin
708
709 =item * warehouse
710
711 =item * chargenumber
712
713 =item * bestbefore
714
715 =item * reserve_for
716
717 =back
718
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
721
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.
725
726 =head1 ALLOCATION ALGORITHM
727
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.
730
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.
735
736 If C<reserve_for> is given, those will be used up first too.
737
738 If C<reserved_warehouse> is given, those will be used up second.
739
740 If C<chargenumber> is given, those will be used up next.
741
742 After that normal quantities will be used.
743
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.
747
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>
750 will be used.
751
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
754 transaction.
755
756 =head1 ALLOCATION DATA STRUCTURE
757
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:
760
761 =over 4
762
763 =item * parts_id
764
765 =item * qty
766
767 =item * bin_id
768
769 =item * warehouse_id
770
771 =item * chargenumber
772
773 =item * bestbefore
774
775 =item * reserve_for_id
776
777 =item * reserve_for_table
778
779 =item * for_object_id
780
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
785
786 =back
787
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.
791
792
793 =head1 CONSTRAINTS
794
795   # whitelist constraints
796   ->allocate(
797     ...
798     constraints => {
799       bin_id       => \@allowed_bins,
800       chargenumber => \@allowed_chargenumbers,
801     }
802   );
803
804   # custom constraints
805   ->allocate(
806     constraints => sub {
807       # only allow chargenumbers with specific format
808       all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_
809
810       &&
811       # and must be all reservations
812       all { $_->reserve_for_id } @_;
813     }
814   )
815
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.
819
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
823 of the given values.
824
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.
827
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.
831
832 =head1 ERROR HANDLING
833
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.
837
838 =head1 TODO
839
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
845   * document no_ check
846   * tests
847
848 =head1 BUGS
849
850 None yet :)
851
852 =head1 AUTHOR
853
854 Sven Schöling E<lt>sven.schoeling@opendynamic.deE<gt>
855
856 =cut