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