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