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