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