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