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