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