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