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