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