Inventory: Doku update. Funktionen statt Klassenmthoden
[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 } grep defined, listify($params{bin});
172   my %wh_whitelist  = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{warehouse});
173   my %chargenumbers = map { (ref $_ ? $_->id : $_) => 1 } grep defined, 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         for_object_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 $for_object_id = $params{for_object_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     my $oe_id = delete $allocation->{for_object_id};
358     push @transfers, SL::DB::Inventory->new(
359       trans_id     => $trans_id,
360       %$allocation,
361       qty          => -$allocation->qty,
362       trans_type   => $trans_type_out,
363       shippingdate => $shippingdate,
364       employee     => SL::DB::Manager::Employee->current,
365       oe_id        => $allocation->for_object_id,
366     );
367   }
368
369   push @transfers, SL::DB::Inventory->new(
370     trans_id          => $trans_id,
371     trans_type        => $trans_type_in,
372     part              => $part,
373     qty               => $qty,
374     bin               => $bin,
375     warehouse         => $bin->warehouse_id,
376     chargenumber      => $chargenumber,
377     bestbefore        => $bestbefore,
378     reserve_for_id    => $reserve_for_id,
379     reserve_for_table => $reserve_for_table,
380     shippingdate      => $shippingdate,
381     project           => $project,
382     invoice           => $invoice,
383     comment           => $comment,
384     prod              => $production_order_item,
385     employee          => SL::DB::Manager::Employee->current,
386     oe_id             => $for_object_id,
387   );
388
389   SL::DB->client->with_transaction(sub {
390     $_->save for @transfers;
391     1;
392   }) or do {
393     die SL::DB->client->error;
394   };
395
396   @transfers;
397 }
398
399 package SL::Helper::Inventory::Allocation {
400   my @attributes = qw(parts_id qty bin_id warehouse_id chargenumber bestbefore comment reserve_for_id reserve_for_table for_object_id);
401   my %attributes = map { $_ => 1 } @attributes;
402
403   for my $name (@attributes) {
404     no strict 'refs';
405     *{"SL::Helper::Inventory::Allocation::$name"} = sub { $_[0]{$name} };
406   }
407
408   sub new {
409     my ($class, %params) = @_;
410
411     Carp::croak("missing attribute $_") for grep { !exists $params{$_}     } @attributes;
412     Carp::croak("unknown attribute $_") for grep { !exists $attributes{$_} } keys %params;
413     Carp::croak("$_ must be set")       for grep { !$params{$_} } qw(parts_id qty bin_id);
414     Carp::croak("$_ must be positive")  for grep { !($params{$_} > 0) } qw(parts_id qty bin_id);
415
416     bless { %params }, $class;
417   }
418 }
419
420 1;
421
422 =encoding utf-8
423
424 =head1 NAME
425
426 SL::WH - Warehouse and Inventory API
427
428 =head1 SYNOPSIS
429
430   # See description for an intro to the concepts used here.
431
432   use SL::Helper::Inventory qw(:ALL);
433
434   # stock, get "what's there" for a part with various conditions:
435   my $qty = get_stock(part => $part);                              # how much is on stock?
436   my $qty = get_stock(part => $part, date => $date);               # how much was on stock at a specific time?
437   my $qty = get_stock(part => $part, bin => $bin);                 # how is on stock in a specific bin?
438   my $qty = get_stock(part => $part, warehouse => $warehouse);     # how is on stock in a specific warehouse?
439   my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how is on stock of a specific chargenumber?
440
441   # onhand, get "what's available" for a part with various conditions:
442   my $qty = get_onhand(part => $part);                              # how much is available?
443   my $qty = get_onhand(part => $part, date => $date);               # how much was available at a specific time?
444   my $qty = get_onhand(part => $part, bin => $bin);                 # how much is available in a specific bin?
445   my $qty = get_onhand(part => $part, warehouse => $warehouse);     # how much is available in a specific warehouse?
446   my $qty = get_onhand(part => $part, chargenumber => $chargenumber); # how much is availbale of a specific chargenumber?
447   my $qty = get_onhand(part => $part, reserve_for => $order);       # how much is available if you include this reservation?
448
449   # onhand batch mode:
450   my $data = get_onhand(
451     warehouse    => $warehouse,
452     by           => [ qw(bin part chargenumber reserve_for) ],
453     with_objects => [ qw(bin part) ],
454   );
455
456   # allocate:
457   my @allocations, allocate(
458     part         => $part,          # part_id works too
459     qty          => $qty,           # must be positive
460     chargenumber => $chargenumber,  # optional, may be arrayref. if provided these charges will be used first
461     bestbefore   => $datetime,      # optional, defaults to today. items with bestbefore prior to that date wont be used
462     reserve_for  => $object,        # optional, may be arrayref. if provided the qtys reserved for these objects will be used first
463     bin          => $bin,           # optional, may be arrayref. if provided
464   );
465
466   # shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate
467   my @allocations, allocate_for_assembly(
468     part         => $assembly,      # part_id works too
469     qty          => $qty,           # must be positive
470   );
471
472   # create allocation manually, bypassing checks, all of these need to be passed, even undefs
473   my $allocation = SL::Helper::Inventory::Allocation->new(
474     part_id           => $part->id,
475     qty               => 15,
476     bin_id            => $bin_obj->id,
477     warehouse_id      => $bin_obj->warehouse_id,
478     chargenumber      => '1823772365',
479     bestbefore        => undef,
480     reserve_for_id    => undef,
481     reserve_for_table => undef,
482     for_object_id     => $order->id,
483   );
484
485   # produce_assembly:
486   produce_assembly(
487     part         => $part,           # target assembly
488     qty          => $qty,            # qty
489     allocations  => \@allocations,   # allocations to use. alternatively use "auto_allocate => 1,"
490
491     # where to put it
492     bin          => $bin,           # needed unless a global standard target is configured
493     chargenumber => $chargenumber,  # optional
494     bestbefore   => $datetime,      # optional
495     comment      => $comment,       # optional
496
497     # links, all optional
498     production_order_item => $item,
499     reserve_for           => $object,
500   );
501
502 =head1 DESCRIPTION
503
504 New functions for the warehouse and inventory api.
505
506 The WH api currently has three large shortcomings. It is very hard to just get
507 the current stock for an item, it's extremely complicated to use it to produce
508 assemblies while ensuring that no stock ends up negative, and it's very hard to
509 use it to get an overview over the actual contents of the inventory.
510
511 The first problem has spawned several dozen small functions in the program that
512 try to implement that, and those usually miss some details. They may ignore
513 reservations, or reserve warehouses, or bestbefore times.
514
515 To get this cleaned up a bit this code introduces two concepts: stock and onhand.
516
517 Stock is defined as the actual contents of the inventory, everything that is
518 there. Onhand is what is available, which means things that are stocked and not
519 reserved and not expired.
520
521 The two new functions C<get_stock> and C<get_onhand> encapsulate these principles and
522 allow simple access with some optional filters for chargenumbers or warehouses.
523 Both of them have a batch mode that can be used to get these information to
524 supllement smiple reports.
525
526 To address the safe assembly creation a new function has been added.
527 C<allocate> will try to find the requested quantity of a part in the inventory
528 and will return allocations of it which can then be used to create the
529 assembly. Allocation will happen with the C<onhand> semantics defined above,
530 meaning that by default no reservations or expired goods will be used. The
531 caller can supply hints of what shold be used and in those cases chargenumber
532 and reservations will be used up as much as possible first.  C<allocate> will
533 always try to fulfil the request even beyond those. Should the required amount
534 not be stocked, allocate will throw an exception.
535
536 C<produce_assembly> has been rewritten to only accept parameters about the
537 target of the production, and requires allocations to complete the request. The
538 allocations can be supplied manually, or can be generated automatically.
539 C<produce_assembly> will check whether enough allocations are given to create
540 the recipe, but will not check whether the allocations are backed. If the
541 allocations are not sufficient or if the auto-allocation fails an exception
542 is returned. If you need to produce something that is not in the inventory, you
543 can bypass those checks by creating the allocations yourself (see
544 L</"ALLOCATION DATA STRUCTURE">).
545
546 Note: this is only intended to cover the scenarios described above. For other cases:
547
548 =over 4
549
550 =item *
551
552 If you need the reserved amount for an order use C<SL::DB::Helper::Reservation>
553 instead.
554
555 =item *
556
557 If you need actual inventory objects because of record links, prod_id links or
558 something like that load them directly. And strongly consider redesigning that,
559 because it's really fragile.
560
561 =item *
562
563 You need weight or accounting information you're on your own. The inventory api
564 only concerns itself with the raw quantities.
565
566 =item *
567
568 If you need the first stock date of parts, or anything related to a specific
569 transfer type or direction, this is not covered yet.
570
571 =back
572
573 =head1 FUNCTIONS
574
575 =over 4
576
577 =item * get_stock PARAMS
578
579 Returns for single parts how much actually exists in the inventory.
580
581 Options:
582
583 =over 4
584
585 =item * part
586
587 The part. Must be present without C<by>. May be arrayref with C<by>. Can be object or id.
588
589 =item * bin
590
591 If given, will only return stock on these bins. Optional. May be array, May be object or id.
592
593 =item * warehouse
594
595 If given, will only return stock on these warehouses. Optional. May be array, May be object or id.
596
597 =item * date
598
599 If given, will return stock as it were on this timestamp. Optional. Must be L<DateTime> object.
600
601 =item * chargenumber
602
603 If given, will only show stock with this chargenumber. Optional. May be array.
604
605 =item * by
606
607 See L</"STOCK/ONHAND REPORT MODE">
608
609 =item * with_objects
610
611 See L</"STOCK/ONHAND REPORT MODE">
612
613 =back
614
615 Will return a single qty normally, see L</"STOCK/ONHAND REPORT MODE"> for batch
616 mode when C<by> is given.
617
618 =item * get_onhand PARAMS
619
620 Returns for single parts how much is available in the inventory. That excludes:
621 reserved quantities, reserved warehouses and stock with expired bestbefore.
622
623 It takes all options of L</get_stock> but treats some of the differently and has some additional ones:
624
625 =over 4
626
627 =item * warehouse
628
629 Usually C<onhand> will not include results from warehouses with the C<reserve>
630 flag. However giving an explicit list of warehouses will include there in the
631 search, as well as all others.
632
633 =item * reserve_for
634
635 =item * reserve_warehouse
636
637 =item * bestbefore
638
639 =back
640
641 =item * allocate PARAMS
642
643 Accepted parameters:
644
645 =over 4
646
647 =item * part
648
649 =item * qty
650
651 =item * bin
652
653 Bin object. Optional.
654
655 =item * warehouse
656
657 Warehouse object. Optional.
658
659 =item * chargenumber
660
661 Optional.
662
663 =item * bestbefore
664
665 Datetime. Optional.
666
667 =item * reserve_for
668
669 Needs to be a rose object, where id and table can be extracted. Optional.
670
671 =back
672
673 Tries to allocate the required quantity using what is currently onhand. If
674 given any of C<bin>, C<warehouse>, C<chargenumber>, C<reserve_for>
675
676
677 =item * allocate_for_assembly PARAMS
678
679 Shortcut to allocate everything for an assembly. Takes the same arguments. Will
680 compute the required amount for each assembly part and allocate all of them.
681
682 =item * produce_assembly
683
684
685 =back
686
687 =head1 STOCK/ONHAND REPORT MODE
688
689 If the special option C<by> is given with an arrayref, the result will instead
690 be an arrayref of partitioned stocks by those fields. Valid partitions are:
691
692 =over 4
693
694 =item * part
695
696 If this is given, part is optional in the parameters
697
698 =item * bin
699
700 =item * warehouse
701
702 =item * chargenumber
703
704 =item * bestbefore
705
706 =item * reserve_for
707
708 =back
709
710 Note: If you want to use the returned data to create allocations you I<need> to
711 enable all of these. To make this easier a special shortcut exists
712
713 In this mode, C<with_objects> can be used to load C<warehouse>, C<bin>,
714 C<parts>, and the C<reserve_for> objects in one go, just like with Rose. They
715 need to be present in C<by> before that though.
716
717 =head1 ALLOCATION ALGORITHM
718
719 When calling allocate, the current onhand (== available stock) of the item will
720 be used to decide which bins/chargenumbers/bestbefore can be used.
721
722 In general allocate will try to make the request happen, and will use the
723 provided charges up first, and then tap everything else. If you need to only
724 I<exactly> use the provided charges, you'll need to craft the allocations
725 yourself. See L</"ALLOCATION DATA STRUCTURE"> for that.
726
727 If C<reserve_for> is given, those will be used up first too.
728
729 If C<reserved_warehouse> is given, those will be used up second.
730
731 If C<chargenumber> is given, those will be used up next.
732
733 After that normal quantities will be used.
734
735 These are tiebreakers and expected to rarely matter in reality. If you need
736 finegrained control over which allocation is used, you may want to get the
737 onhands yourself and select the appropriate ones.
738
739 Only quantities with C<bestbefore> unset or after the given date will be
740 considered. If more than one charge is eligible, the earlier C<bestbefore>
741 will be used.
742
743 Allocations do NOT have an internal memory and can't react to other allocations
744 of the same part earlier. Never double allocate the same part within a
745 transaction.
746
747 =head1 ALLOCATION DATA STRUCTURE
748
749 Allocations are instances of the helper class C<SL::Helper::Inventory::Allocation>. They require
750 each of the following attributes to be set at creation time:
751
752 =over 4
753
754 =item * parts_id
755
756 =item * qty
757
758 =item * bin_id
759
760 =item * warehouse_id
761
762 =item * chargenumber
763
764 =item * bestbefore
765
766 =item * reserve_for_id
767
768 =item * reserve_for_table
769
770 =item * for_object_id
771
772 If set the allocations will be marked as allocated for the given object.
773 If these allocations are later used to produce an assembly, the resulting
774 consuming transactions will be marked as belonging to the given object.
775 The object may be an order, productionorder or other objects
776
777 =back
778
779 C<chargenumber>, C<bestbefore>, C<reserve_for_id>, C<reserve_for_table> and
780 C<for_object_id> may be C<undef> (but must still be present at creation time).
781 Instances are considered immutable.
782
783
784 =head1 CONSTRAINTS
785
786   # whitelist constraints
787   ->allocate(
788     ...
789     constraints => {
790       bin_id       => \@allowed_bins,
791       chargenumber => \@allowed_chargenumbers,
792     }
793   );
794
795   # custom constraints
796   ->allocate(
797     constraints => sub {
798       # only allow chargenumbers with specific format
799       all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_
800
801       &&
802       # and must be all reservations
803       all { $_->reserve_for_id } @_;
804     }
805   )
806
807 C<allocation> is "best effort" in nature. It will take the C<bin>,
808 C<chargenumber> etc hints from the parameters, but will try it's bvest to
809 fulfil the request anyway and only bail out if it is absolutely not possible.
810
811 Sometimes you need to restrict allocations though. For this you can pass
812 additional constraints to C<allocate>. A constraint serves as a whitelist.
813 Every allocation must fulfil every constraint by having that attribute be one
814 of the given values.
815
816 In case even that is not enough, you may supply a custom check by passing a
817 function that will be given the allocation objects.
818
819 Note that both whitelists and constraints do not influence the order of
820 allocations, which is done purely from the initial parameters. They only serve
821 to reject allocations made in good faith which do fulfil required assertions.
822
823 =head1 ERROR HANDLING
824
825 C<allocate> and C<produce_assembly> will throw exceptions if the request can
826 not be completed. The usual reason will be insufficient onhand to allocate, or
827 insufficient allocations to process the request.
828
829 =head1 TODO
830
831   * define and describe error classes
832   * define wrapper classes for stock/onhand batch mode return values
833   * handle extra arguments in produce: shippingdate, project, oe
834   * clean up allocation helper class
835   * with objects for reservations
836   * document no_ check
837   * tests
838
839 =head1 BUGS
840
841 None yet :)
842
843 =head1 AUTHOR
844
845 Sven Schöling E<lt>sven.schoeling@opendynamic.deE<gt>
846
847 =cut