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