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