kivitendo/SL/Helper/Inventory.pm @ 7b1da9c3
9687d2ce | Sven Schöling | package SL::Helper::Inventory;
|
||
use strict;
|
||||
use Carp;
|
||||
use DateTime;
|
||||
use Exporter qw(import);
|
||||
3b322be4 | Martin Helmling | use List::Util qw(min sum);
|
||
9687d2ce | Sven Schöling | use List::UtilsBy qw(sort_by);
|
||
7eee416a | Bernd Bleßmann | use List::MoreUtils qw(any none);
|
||
24e928a5 | Martin Helmling | use POSIX qw(ceil);
|
||
7fc41a37 | Moritz Bunkus | use Scalar::Util qw(blessed);
|
||
9687d2ce | Sven Schöling | |||
use SL::Locale::String qw(t8);
|
||||
use SL::MoreCommon qw(listify);
|
||||
use SL::DBUtils qw(selectall_hashref_query selectrow_query);
|
||||
use SL::DB::TransferType;
|
||||
726e362a | Sven Schöling | use SL::Helper::Number qw(_format_number _round_number);
|
||
7bf726ca | Sven Schöling | use SL::Helper::Inventory::Allocation;
|
||
9687d2ce | Sven Schöling | use SL::X;
|
||
7eee416a | Bernd Bleßmann | our @EXPORT_OK = qw(get_stock get_onhand allocate allocate_for_assembly produce_assembly check_constraints check_allocations_for_assembly);
|
||
9687d2ce | Sven Schöling | our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
|
||
sub _get_stock_onhand {
|
||||
my (%params) = @_;
|
||||
my $onhand_mode = !!$params{onhand};
|
||||
15176cbb | Sven Schöling | my @selects = (
|
||
'SUM(qty) AS qty',
|
||||
'MIN(EXTRACT(epoch FROM inventory.itime)) AS itime',
|
||||
);
|
||||
9687d2ce | Sven Schöling | my @values;
|
||
my @where;
|
||||
my @groups;
|
||||
if ($params{part}) {
|
||||
my @ids = map { ref $_ ? $_->id : $_ } listify($params{part});
|
||||
push @where, sprintf "parts_id IN (%s)", join ', ', ("?") x @ids;
|
||||
push @values, @ids;
|
||||
}
|
||||
if ($params{bin}) {
|
||||
my @ids = map { ref $_ ? $_->id : $_ } listify($params{bin});
|
||||
push @where, sprintf "bin_id IN (%s)", join ', ', ("?") x @ids;
|
||||
push @values, @ids;
|
||||
}
|
||||
if ($params{warehouse}) {
|
||||
my @ids = map { ref $_ ? $_->id : $_ } listify($params{warehouse});
|
||||
push @where, sprintf "warehouse.id IN (%s)", join ', ', ("?") x @ids;
|
||||
push @values, @ids;
|
||||
}
|
||||
if ($params{chargenumber}) {
|
||||
my @ids = listify($params{chargenumber});
|
||||
push @where, sprintf "chargenumber IN (%s)", join ', ', ("?") x @ids;
|
||||
push @values, @ids;
|
||||
}
|
||||
if ($params{date}) {
|
||||
21b7295d | Martin Helmling | Carp::croak("not DateTime ".$params{date}) unless ref($params{date}) eq 'DateTime';
|
||
9687d2ce | Sven Schöling | push @where, sprintf "shippingdate <= ?";
|
||
push @values, $params{date};
|
||||
}
|
||||
bb12dc4d | Sven Schöling | if (!$params{bestbefore} && $onhand_mode && default_show_bestbefore()) {
|
||
$params{bestbefore} = DateTime->now_local;
|
||||
}
|
||||
9687d2ce | Sven Schöling | if ($params{bestbefore}) {
|
||
21b7295d | Martin Helmling | Carp::croak("not DateTime ".$params{date}) unless ref($params{bestbefore}) eq 'DateTime';
|
||
bb12dc4d | Sven Schöling | push @where, sprintf "(bestbefore IS NULL OR bestbefore >= ?)";
|
||
9687d2ce | Sven Schöling | push @values, $params{bestbefore};
|
||
}
|
||||
# by
|
||||
my %allowed_by = (
|
||||
part => [ qw(parts_id) ],
|
||||
654022f9 | Sven Schöling | bin => [ qw(bin_id inventory.warehouse_id)],
|
||
warehouse => [ qw(inventory.warehouse_id) ],
|
||||
9687d2ce | Sven Schöling | chargenumber => [ qw(chargenumber) ],
|
||
bestbefore => [ qw(bestbefore) ],
|
||||
654022f9 | Sven Schöling | for_allocate => [ qw(parts_id bin_id inventory.warehouse_id chargenumber bestbefore) ],
|
||
9687d2ce | Sven Schöling | );
|
||
if ($params{by}) {
|
||||
for (listify($params{by})) {
|
||||
my $selects = $allowed_by{$_} or Carp::croak("unknown option for by: $_");
|
||||
push @selects, @$selects;
|
||||
push @groups, @$selects;
|
||||
}
|
||||
}
|
||||
my $select = join ',', @selects;
|
||||
my $where = @where ? 'WHERE ' . join ' AND ', @where : '';
|
||||
my $group_by = @groups ? 'GROUP BY ' . join ', ', @groups : '';
|
||||
my $query = <<"";
|
||||
SELECT $select FROM inventory
|
||||
LEFT JOIN bin ON bin_id = bin.id
|
||||
LEFT JOIN warehouse ON bin.warehouse_id = warehouse.id
|
||||
$where
|
||||
$group_by
|
||||
bb12dc4d | Sven Schöling | |||
if ($onhand_mode) {
|
||||
$query .= ' HAVING SUM(qty) > 0';
|
||||
}
|
||||
9687d2ce | Sven Schöling | |||
my $results = selectall_hashref_query($::form, SL::DB->client->dbh, $query, @values);
|
||||
my %with_objects = (
|
||||
part => 'SL::DB::Manager::Part',
|
||||
bin => 'SL::DB::Manager::Bin',
|
||||
warehouse => 'SL::DB::Manager::Warehouse',
|
||||
);
|
||||
my %slots = (
|
||||
part => 'parts_id',
|
||||
bin => 'bin_id',
|
||||
warehouse => 'warehouse_id',
|
||||
);
|
||||
if ($params{by} && $params{with_objects}) {
|
||||
for my $with_object (listify($params{with_objects})) {
|
||||
Carp::croak("unknown with_object $with_object") if !exists $with_objects{$with_object};
|
||||
1d96e961 | Sven Schöling | my $manager = $with_objects{$with_object};
|
||
my $slot = $slots{$with_object};
|
||||
next if !(my @ids = map { $_->{$slot} } @$results);
|
||||
my $objects = $manager->get_all(query => [ id => \@ids ]);
|
||||
my %objects_by_id = map { $_->id => $_ } @$objects;
|
||||
$_->{$with_object} = $objects_by_id{$_->{$slot}} for @$results;
|
||||
9687d2ce | Sven Schöling | }
|
||
}
|
||||
if ($params{by}) {
|
||||
return $results;
|
||||
} else {
|
||||
return $results->[0]{qty};
|
||||
}
|
||||
}
|
||||
sub get_stock {
|
||||
_get_stock_onhand(@_, onhand => 0);
|
||||
}
|
||||
sub get_onhand {
|
||||
_get_stock_onhand(@_, onhand => 1);
|
||||
}
|
||||
sub allocate {
|
||||
my (%params) = @_;
|
||||
50a6450f | Moritz Bunkus | croak('allocate needs a part') unless $params{part};
|
||
croak('allocate needs a qty') unless $params{qty};
|
||||
0845ca9d | Martin Helmling | |||
my $part = $params{part};
|
||||
my $qty = $params{qty};
|
||||
9687d2ce | Sven Schöling | |||
return () if $qty <= 0;
|
||||
my $results = get_stock(part => $part, by => 'for_allocate');
|
||||
acf478a3 | Sven Schöling | my %bin_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{bin});
|
||
my %wh_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{warehouse});
|
||||
my %chargenumbers = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{chargenumber});
|
||||
9687d2ce | Sven Schöling | |||
654022f9 | Sven Schöling | # sort results so that chargenumbers are matched first, then wanted bins, then wanted warehouses
|
||
9687d2ce | Sven Schöling | my @sorted_results = sort {
|
||
654022f9 | Sven Schöling | exists $chargenumbers{$b->{chargenumber}} <=> exists $chargenumbers{$a->{chargenumber}} # then prefer wanted chargenumbers
|
||
0f19ca7e | Sven Schöling | || exists $bin_whitelist{$b->{bin_id}} <=> exists $bin_whitelist{$a->{bin_id}} # then prefer wanted bins
|
||
|| exists $wh_whitelist{$b->{warehouse_id}} <=> exists $wh_whitelist{$a->{warehouse_id}} # then prefer wanted bins
|
||||
15176cbb | Sven Schöling | || $a->{itime} <=> $b->{itime} # and finally prefer earlier charges
|
||
654022f9 | Sven Schöling | } @$results;
|
||
9687d2ce | Sven Schöling | my @allocations;
|
||
my $rest_qty = $qty;
|
||||
for my $chunk (@sorted_results) {
|
||||
my $qty = min($chunk->{qty}, $rest_qty);
|
||||
bb12dc4d | Sven Schöling | |||
# since allocate operates on stock, this also ensures that no negative stock results are used
|
||||
9687d2ce | Sven Schöling | if ($qty > 0) {
|
||
push @allocations, SL::Helper::Inventory::Allocation->new(
|
||||
parts_id => $chunk->{parts_id},
|
||||
qty => $qty,
|
||||
comment => $params{comment},
|
||||
bin_id => $chunk->{bin_id},
|
||||
warehouse_id => $chunk->{warehouse_id},
|
||||
chargenumber => $chunk->{chargenumber},
|
||||
bestbefore => $chunk->{bestbefore},
|
||||
155b8aa4 | Sven Schöling | for_object_id => undef,
|
||
9687d2ce | Sven Schöling | );
|
||
2951ed30 | Sven Schöling | $rest_qty -= _round_number($qty, 5);
|
||
9687d2ce | Sven Schöling | }
|
||
2951ed30 | Sven Schöling | $rest_qty = _round_number($rest_qty, 5);
|
||
9687d2ce | Sven Schöling | last if $rest_qty == 0;
|
||
}
|
||||
if ($rest_qty > 0) {
|
||||
7fc41a37 | Moritz Bunkus | die SL::X::Inventory::Allocation::MissingQty->new(
|
||
code => 'not enough to allocate',
|
||||
message => t8("can not allocate #1 units of #2, missing #3 units", _format_number($qty), $part->displayable_name, _format_number($rest_qty)),
|
||||
part_description => $part->displayable_name,
|
||||
to_allocate_qty => $qty,
|
||||
missing_qty => $rest_qty,
|
||||
9687d2ce | Sven Schöling | );
|
||
} else {
|
||||
1672b7f7 | Martin Helmling | if ($params{constraints}) {
|
||
check_constraints($params{constraints},\@allocations);
|
||||
}
|
||||
9687d2ce | Sven Schöling | return @allocations;
|
||
}
|
||||
}
|
||||
sub allocate_for_assembly {
|
||||
my (%params) = @_;
|
||||
my $part = $params{part} or Carp::croak('allocate needs a part');
|
||||
my $qty = $params{qty} or Carp::croak('allocate needs a qty');
|
||||
3e1190f9 | Jan Büren | my $wh = $params{warehouse};
|
||
c592c768 | Jan Büren | my $wh_strict = $::instance_conf->get_produce_assembly_same_warehouse;
|
||
my $consume_service = $::instance_conf->get_produce_assembly_transfer_service;
|
||||
9687d2ce | Sven Schöling | |||
3e1190f9 | Jan Büren | Carp::croak('not an assembly') unless $part->is_assembly;
|
||
Carp::croak('No warehouse selected') if $wh_strict && !$wh;
|
||||
9687d2ce | Sven Schöling | |||
my %parts_to_allocate;
|
||||
for my $assembly ($part->assemblies) {
|
||||
c592c768 | Jan Büren | next if $assembly->part->type eq 'service' && !$consume_service;
|
||
9687d2ce | Sven Schöling | $parts_to_allocate{ $assembly->part->id } //= 0;
|
||
f5c44fc1 | Sven Schöling | $parts_to_allocate{ $assembly->part->id } += $assembly->qty * $qty;
|
||
9687d2ce | Sven Schöling | }
|
||
7fc41a37 | Moritz Bunkus | my (@allocations, @errors);
|
||
9687d2ce | Sven Schöling | |||
for my $part_id (keys %parts_to_allocate) {
|
||||
eb0b223d | Martin Helmling | my $part = SL::DB::Part->load_cached($part_id);
|
||
7fc41a37 | Moritz Bunkus | |||
eval {
|
||||
push @allocations, allocate(%params, part => $part, qty => $parts_to_allocate{$part_id});
|
||||
if ($wh_strict) {
|
||||
die SL::X::Inventory::Allocation->new(
|
||||
code => "wrong warehouse for part",
|
||||
message => t8('Part #1 exists in warehouse #2, but not in warehouse #3 ',
|
||||
$part->partnumber . ' ' . $part->description,
|
||||
SL::DB::Manager::Warehouse->find_by(id => $allocations[-1]->{warehouse_id})->description,
|
||||
$wh->description),
|
||||
) unless $allocations[-1]->{warehouse_id} == $wh->id;
|
||||
}
|
||||
1;
|
||||
} or do {
|
||||
my $ex = $@;
|
||||
die $ex unless blessed($ex) && $ex->can('rethrow');
|
||||
if ($ex->isa('SL::X::Inventory::Allocation')) {
|
||||
push @errors, $@;
|
||||
} else {
|
||||
$ex->rethrow;
|
||||
}
|
||||
};
|
||||
}
|
||||
if (@errors) {
|
||||
die SL::X::Inventory::Allocation::Multi->new(
|
||||
code => "multiple errors during allocation",
|
||||
message => "multiple errors during allocation",
|
||||
errors => \@errors,
|
||||
);
|
||||
9687d2ce | Sven Schöling | }
|
||
@allocations;
|
||||
}
|
||||
1672b7f7 | Martin Helmling | sub check_constraints {
|
||
my ($constraints, $allocations) = @_;
|
||||
if ('CODE' eq ref $constraints) {
|
||||
if (!$constraints->(@$allocations)) {
|
||||
die SL::X::Inventory::Allocation->new(
|
||||
cb53cdd0 | Sven Schöling | code => 'allocation constraints failure',
|
||
message => t8("Allocations didn't pass constraints"),
|
||||
1672b7f7 | Martin Helmling | );
|
||
}
|
||||
} else {
|
||||
croak 'constraints needs to be a hashref' unless 'HASH' eq ref $constraints;
|
||||
my %supported_constraints = (
|
||||
bin_id => 'bin_id',
|
||||
warehouse_id => 'warehouse_id',
|
||||
chargenumber => 'chargenumber',
|
||||
);
|
||||
for (keys %$constraints ) {
|
||||
croak "unsupported constraint '$_'" unless $supported_constraints{$_};
|
||||
3b9f657c | Martin Helmling | next unless defined $constraints->{$_};
|
||
1672b7f7 | Martin Helmling | |||
my %whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($constraints->{$_});
|
||||
my $accessor = $supported_constraints{$_};
|
||||
if (any { !$whitelist{$_->$accessor} } @$allocations) {
|
||||
my %error_constraints = (
|
||||
21b7295d | Martin Helmling | bin_id => t8('Bins'),
|
||
warehouse_id => t8('Warehouses'),
|
||||
chargenumber => t8('Chargenumbers'),
|
||||
1672b7f7 | Martin Helmling | );
|
||
3b322be4 | Martin Helmling | my @allocs = grep { $whitelist{$_->$accessor} } @$allocations;
|
||
my $needed = sum map { $_->qty } grep { !$whitelist{$_->$accessor} } @$allocations;
|
||||
my $err = t8("Cannot allocate parts.");
|
||||
$err .= ' '.t8('part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.',
|
||||
SL::DB::Part->load_cached($_->parts_id)->description,
|
||||
SL::DB::Bin->load_cached($_->bin_id)->full_description,
|
||||
726e362a | Sven Schöling | _format_number($_->qty), _format_number($needed), $_->chargenumber ? $_->chargenumber : '--') for @allocs;
|
||
1672b7f7 | Martin Helmling | die SL::X::Inventory::Allocation->new(
|
||
cb53cdd0 | Sven Schöling | code => 'allocation constraints failure',
|
||
message => $err,
|
||||
1672b7f7 | Martin Helmling | );
|
||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9687d2ce | Sven Schöling | sub produce_assembly {
|
||
my (%params) = @_;
|
||||
982ea316 | Sven Schöling | my $part = $params{part} or Carp::croak('produce_assembly needs a part');
|
||
my $qty = $params{qty} or Carp::croak('produce_assembly needs a qty');
|
||||
2436f6e9 | Jan Büren | my $bin = $params{bin} or Carp::croak("need target bin");
|
||
9687d2ce | Sven Schöling | |||
my $allocations = $params{allocations};
|
||||
3e1190f9 | Jan Büren | my $strict_wh = $::instance_conf->get_produce_assembly_same_warehouse ? $bin->warehouse : undef;
|
||
c2e1e2cf | Bernd Bleßmann | my $consume_service = $::instance_conf->get_produce_assembly_transfer_service;
|
||
982ea316 | Sven Schöling | if ($params{auto_allocate}) {
|
||
Carp::croak("produce_assembly: can't have both allocations and auto_allocate") if $params{allocations};
|
||||
e569098e | Jan Büren | $allocations = [ allocate_for_assembly(part => $part, qty => $qty, warehouse => $strict_wh, chargenumber => $params{chargenumber}) ];
|
||
9687d2ce | Sven Schöling | } else {
|
||
982ea316 | Sven Schöling | Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations};
|
||
$allocations = $params{allocations};
|
||||
9687d2ce | Sven Schöling | }
|
||
2436f6e9 | Jan Büren | my $chargenumber = $params{chargenumber};
|
||
my $bestbefore = $params{bestbefore};
|
||||
155b8aa4 | Sven Schöling | my $for_object_id = $params{for_object_id};
|
||
2436f6e9 | Jan Büren | my $comment = $params{comment} // '';
|
||
my $invoice = $params{invoice};
|
||||
my $project = $params{project};
|
||||
my $shippingdate = $params{shippingsdate} // DateTime->now_local;
|
||||
my $trans_id = $params{trans_id};
|
||||
9687d2ce | Sven Schöling | |||
($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id;
|
||||
my $trans_type_out = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used');
|
||||
3e1190f9 | Jan Büren | my $trans_type_in = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'assembled');
|
||
9687d2ce | Sven Schöling | |||
# check whether allocations are sane
|
||||
if (!$params{no_check_allocations} && !$params{auto_allocate}) {
|
||||
cb53cdd0 | Sven Schöling | die SL::X::Inventory::Allocation->new(
|
||
code => "allocations are insufficient for production",
|
||||
message => t8('can not allocate enough resources for production'),
|
||||
7eee416a | Bernd Bleßmann | ) if !check_allocations_for_assembly(part => $part, qty => $qty, allocations => $allocations);
|
||
9687d2ce | Sven Schöling | }
|
||
my @transfers;
|
||||
for my $allocation (@$allocations) {
|
||||
155b8aa4 | Sven Schöling | my $oe_id = delete $allocation->{for_object_id};
|
||
5d7aadc1 | Sven Schöling | push @transfers, $allocation->transfer_object(
|
||
9687d2ce | Sven Schöling | trans_id => $trans_id,
|
||
qty => -$allocation->qty,
|
||||
trans_type => $trans_type_out,
|
||||
shippingdate => $shippingdate,
|
||||
employee => SL::DB::Manager::Employee->current,
|
||||
72ab222c | Jan Büren | comment => t8('Used for assembly #1 #2', $part->partnumber, $part->description),
|
||
9687d2ce | Sven Schöling | );
|
||
}
|
||||
push @transfers, SL::DB::Inventory->new(
|
||||
trans_id => $trans_id,
|
||||
trans_type => $trans_type_in,
|
||||
part => $part,
|
||||
qty => $qty,
|
||||
bin => $bin,
|
||||
warehouse => $bin->warehouse_id,
|
||||
chargenumber => $chargenumber,
|
||||
bestbefore => $bestbefore,
|
||||
shippingdate => $shippingdate,
|
||||
project => $project,
|
||||
invoice => $invoice,
|
||||
comment => $comment,
|
||||
employee => SL::DB::Manager::Employee->current,
|
||||
155b8aa4 | Sven Schöling | oe_id => $for_object_id,
|
||
9687d2ce | Sven Schöling | );
|
||
SL::DB->client->with_transaction(sub {
|
||||
$_->save for @transfers;
|
||||
1;
|
||||
}) or do {
|
||||
die SL::DB->client->error;
|
||||
};
|
||||
@transfers;
|
||||
}
|
||||
7eee416a | Bernd Bleßmann | sub check_allocations_for_assembly {
|
||
my (%params) = @_;
|
||||
my $part = $params{part} or Carp::croak('check_allocations_for_assembly needs a part');
|
||||
my $qty = $params{qty} or Carp::croak('check_allocations_for_assembly needs a qty');
|
||||
4a8d696c | Bernd Bleßmann | my $check_overfulfilment = !!$params{check_overfulfilment};
|
||
my $allocations = $params{allocations};
|
||||
7eee416a | Bernd Bleßmann | |||
4a8d696c | Bernd Bleßmann | my $consume_service = $::instance_conf->get_produce_assembly_transfer_service;
|
||
7eee416a | Bernd Bleßmann | |||
my %allocations_by_part;
|
||||
for (@{ $allocations || []}) {
|
||||
$allocations_by_part{$_->parts_id} //= 0;
|
||||
$allocations_by_part{$_->parts_id} += $_->qty;
|
||||
}
|
||||
for my $assembly ($part->assemblies) {
|
||||
next if $assembly->part->type eq 'service' && !$consume_service;
|
||||
$allocations_by_part{ $assembly->parts_id } -= $assembly->qty * $qty;
|
||||
}
|
||||
4a8d696c | Bernd Bleßmann | return (none { $_ < 0 } values %allocations_by_part) && (!$check_overfulfilment || (none { $_ > 0 } values %allocations_by_part));
|
||
7eee416a | Bernd Bleßmann | }
|
||
bb12dc4d | Sven Schöling | sub default_show_bestbefore {
|
||
$::instance_conf->get_show_bestbefore
|
||||
}
|
||||
9687d2ce | Sven Schöling | 1;
|
||
=encoding utf-8
|
||||
=head1 NAME
|
||||
SL::WH - Warehouse and Inventory API
|
||||
=head1 SYNOPSIS
|
||||
# See description for an intro to the concepts used here.
|
||||
003e290c | Sven Schöling | use SL::Helper::Inventory qw(:ALL);
|
||
9687d2ce | Sven Schöling | |||
# stock, get "what's there" for a part with various conditions:
|
||||
003e290c | Sven Schöling | my $qty = get_stock(part => $part); # how much is on stock?
|
||
my $qty = get_stock(part => $part, date => $date); # how much was on stock at a specific time?
|
||||
c591d7cc | Sven Schöling | my $qty = get_stock(part => $part, bin => $bin); # how much is on stock in a specific bin?
|
||
my $qty = get_stock(part => $part, warehouse => $warehouse); # how much is on stock in a specific warehouse?
|
||||
my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how much is on stock of a specific chargenumber?
|
||||
9687d2ce | Sven Schöling | |||
# onhand, get "what's available" for a part with various conditions:
|
||||
003e290c | Sven Schöling | my $qty = get_onhand(part => $part); # how much is available?
|
||
my $qty = get_onhand(part => $part, date => $date); # how much was available at a specific time?
|
||||
my $qty = get_onhand(part => $part, bin => $bin); # how much is available in a specific bin?
|
||||
my $qty = get_onhand(part => $part, warehouse => $warehouse); # how much is available in a specific warehouse?
|
||||
my $qty = get_onhand(part => $part, chargenumber => $chargenumber); # how much is availbale of a specific chargenumber?
|
||||
9687d2ce | Sven Schöling | |||
# onhand batch mode:
|
||||
003e290c | Sven Schöling | my $data = get_onhand(
|
||
9687d2ce | Sven Schöling | warehouse => $warehouse,
|
||
654022f9 | Sven Schöling | by => [ qw(bin part chargenumber) ],
|
||
9687d2ce | Sven Schöling | with_objects => [ qw(bin part) ],
|
||
);
|
||||
# allocate:
|
||||
c591d7cc | Sven Schöling | my @allocations = allocate(
|
||
9687d2ce | Sven Schöling | part => $part, # part_id works too
|
||
qty => $qty, # must be positive
|
||||
chargenumber => $chargenumber, # optional, may be arrayref. if provided these charges will be used first
|
||||
bestbefore => $datetime, # optional, defaults to today. items with bestbefore prior to that date wont be used
|
||||
bin => $bin, # optional, may be arrayref. if provided
|
||||
);
|
||||
# shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate
|
||||
c591d7cc | Sven Schöling | my @allocations = allocate_for_assembly(
|
||
9687d2ce | Sven Schöling | part => $assembly, # part_id works too
|
||
qty => $qty, # must be positive
|
||||
);
|
||||
c591d7cc | Sven Schöling | # create allocation manually, bypassing checks. all of these need to be passed, even undefs
|
||
9687d2ce | Sven Schöling | my $allocation = SL::Helper::Inventory::Allocation->new(
|
||
52753418 | Bernd Bleßmann | parts_id => $part->id,
|
||
9687d2ce | Sven Schöling | qty => 15,
|
||
bin_id => $bin_obj->id,
|
||||
warehouse_id => $bin_obj->warehouse_id,
|
||||
chargenumber => '1823772365',
|
||||
bestbefore => undef,
|
||||
52753418 | Bernd Bleßmann | comment => undef,
|
||
155b8aa4 | Sven Schöling | for_object_id => $order->id,
|
||
9687d2ce | Sven Schöling | );
|
||
# produce_assembly:
|
||||
003e290c | Sven Schöling | produce_assembly(
|
||
9687d2ce | Sven Schöling | part => $part, # target assembly
|
||
qty => $qty, # qty
|
||||
allocations => \@allocations, # allocations to use. alternatively use "auto_allocate => 1,"
|
||||
# where to put it
|
||||
bin => $bin, # needed unless a global standard target is configured
|
||||
chargenumber => $chargenumber, # optional
|
||||
bestbefore => $datetime, # optional
|
||||
comment => $comment, # optional
|
||||
);
|
||||
=head1 DESCRIPTION
|
||||
New functions for the warehouse and inventory api.
|
||||
c591d7cc | Sven Schöling | The WH api currently has three large shortcomings: It is very hard to just get
|
||
9687d2ce | Sven Schöling | the current stock for an item, it's extremely complicated to use it to produce
|
||
assemblies while ensuring that no stock ends up negative, and it's very hard to
|
||||
use it to get an overview over the actual contents of the inventory.
|
||||
The first problem has spawned several dozen small functions in the program that
|
||||
try to implement that, and those usually miss some details. They may ignore
|
||||
bb12dc4d | Sven Schöling | bestbefore times, comments, ignore negative quantities etc.
|
||
9687d2ce | Sven Schöling | |||
To get this cleaned up a bit this code introduces two concepts: stock and onhand.
|
||||
bb12dc4d | Sven Schöling | =over 4
|
||
=item * Stock is defined as the actual contents of the inventory, everything that is
|
||||
there.
|
||||
=item * Onhand is what is available, which means things that are stocked,
|
||||
c591d7cc | Sven Schöling | not expired and not in any other way reserved for other uses.
|
||
bb12dc4d | Sven Schöling | |||
=back
|
||||
9687d2ce | Sven Schöling | |||
The two new functions C<get_stock> and C<get_onhand> encapsulate these principles and
|
||||
allow simple access with some optional filters for chargenumbers or warehouses.
|
||||
Both of them have a batch mode that can be used to get these information to
|
||||
a910619e | Sven Schöling | supplement simple reports.
|
||
9687d2ce | Sven Schöling | |||
To address the safe assembly creation a new function has been added.
|
||||
C<allocate> will try to find the requested quantity of a part in the inventory
|
||||
and will return allocations of it which can then be used to create the
|
||||
assembly. Allocation will happen with the C<onhand> semantics defined above,
|
||||
bb12dc4d | Sven Schöling | meaning that by default no expired goods will be used. The caller can supply
|
||
hints of what shold be used and in those cases chargenumbers will be used up as
|
||||
much as possible first. C<allocate> will always try to fulfil the request even
|
||||
beyond those. Should the required amount not be stocked, allocate will throw an
|
||||
exception.
|
||||
9687d2ce | Sven Schöling | |||
C<produce_assembly> has been rewritten to only accept parameters about the
|
||||
target of the production, and requires allocations to complete the request. The
|
||||
allocations can be supplied manually, or can be generated automatically.
|
||||
C<produce_assembly> will check whether enough allocations are given to create
|
||||
c591d7cc | Sven Schöling | the assembly, but will not check whether the allocations are backed. If the
|
||
9687d2ce | Sven Schöling | allocations are not sufficient or if the auto-allocation fails an exception
|
||
is returned. If you need to produce something that is not in the inventory, you
|
||||
can bypass those checks by creating the allocations yourself (see
|
||||
L</"ALLOCATION DATA STRUCTURE">).
|
||||
Note: this is only intended to cover the scenarios described above. For other cases:
|
||||
=over 4
|
||||
=item *
|
||||
bb12dc4d | Sven Schöling | If you need actual inventory objects because of record links or something like
|
||
that load them directly. And strongly consider redesigning that, because it's
|
||||
really fragile.
|
||||
9687d2ce | Sven Schöling | |||
=item *
|
||||
You need weight or accounting information you're on your own. The inventory api
|
||||
only concerns itself with the raw quantities.
|
||||
=item *
|
||||
If you need the first stock date of parts, or anything related to a specific
|
||||
transfer type or direction, this is not covered yet.
|
||||
=back
|
||||
=head1 FUNCTIONS
|
||||
=over 4
|
||||
=item * get_stock PARAMS
|
||||
Returns for single parts how much actually exists in the inventory.
|
||||
Options:
|
||||
=over 4
|
||||
=item * part
|
||||
The part. Must be present without C<by>. May be arrayref with C<by>. Can be object or id.
|
||||
=item * bin
|
||||
If given, will only return stock on these bins. Optional. May be array, May be object or id.
|
||||
=item * warehouse
|
||||
If given, will only return stock on these warehouses. Optional. May be array, May be object or id.
|
||||
=item * date
|
||||
If given, will return stock as it were on this timestamp. Optional. Must be L<DateTime> object.
|
||||
=item * chargenumber
|
||||
If given, will only show stock with this chargenumber. Optional. May be array.
|
||||
=item * by
|
||||
See L</"STOCK/ONHAND REPORT MODE">
|
||||
=item * with_objects
|
||||
See L</"STOCK/ONHAND REPORT MODE">
|
||||
=back
|
||||
Will return a single qty normally, see L</"STOCK/ONHAND REPORT MODE"> for batch
|
||||
mode when C<by> is given.
|
||||
=item * get_onhand PARAMS
|
||||
654022f9 | Sven Schöling | Returns for single parts how much is available in the inventory. That excludes
|
||
stock with expired bestbefore.
|
||||
9687d2ce | Sven Schöling | |||
bb12dc4d | Sven Schöling | It takes the same options as L</get_stock>.
|
||
9687d2ce | Sven Schöling | |||
=over 4
|
||||
=item * bestbefore
|
||||
bb12dc4d | Sven Schöling | If given, will only return stock with a bestbefore at or after the given date.
|
||
Optional. Must be L<DateTime> object.
|
||||
9687d2ce | Sven Schöling | =back
|
||
=item * allocate PARAMS
|
||||
Accepted parameters:
|
||||
=over 4
|
||||
=item * part
|
||||
=item * qty
|
||||
=item * bin
|
||||
Bin object. Optional.
|
||||
=item * warehouse
|
||||
Warehouse object. Optional.
|
||||
=item * chargenumber
|
||||
Optional.
|
||||
=item * bestbefore
|
||||
Datetime. Optional.
|
||||
=back
|
||||
Tries to allocate the required quantity using what is currently onhand. If
|
||||
654022f9 | Sven Schöling | given any of C<bin>, C<warehouse>, C<chargenumber>
|
||
9687d2ce | Sven Schöling | |||
=item * allocate_for_assembly PARAMS
|
||||
Shortcut to allocate everything for an assembly. Takes the same arguments. Will
|
||||
compute the required amount for each assembly part and allocate all of them.
|
||||
=item * produce_assembly
|
||||
7eee416a | Bernd Bleßmann | =item * check_allocations_for_assembly PARAMS
|
||
Checks if enough quantity is allocated for production. Returns a trueish
|
||||
4a8d696c | Bernd Bleßmann | value if there is enough allocated, a falsish one otherwise (but see the
|
||
parameter C<check_overfulfilment>).
|
||||
7eee416a | Bernd Bleßmann | |||
Accepted parameters:
|
||||
=over 4
|
||||
=item * part
|
||||
The part object to be assembled. Mandatory.
|
||||
=item * qty
|
||||
The quantity of the part to be assembled. Mandatory.
|
||||
=item * allocations
|
||||
An array ref of the allocations.
|
||||
4a8d696c | Bernd Bleßmann | =item * check_overfulfilment
|
||
Whether or not overfulfilment should be checked. If more quantity is allocated
|
||||
than needed for production a falsish value is returned. Optional.
|
||||
7eee416a | Bernd Bleßmann | =back
|
||
9687d2ce | Sven Schöling | |||
=back
|
||||
=head1 STOCK/ONHAND REPORT MODE
|
||||
If the special option C<by> is given with an arrayref, the result will instead
|
||||
be an arrayref of partitioned stocks by those fields. Valid partitions are:
|
||||
=over 4
|
||||
=item * part
|
||||
If this is given, part is optional in the parameters
|
||||
=item * bin
|
||||
=item * warehouse
|
||||
=item * chargenumber
|
||||
=item * bestbefore
|
||||
=back
|
||||
Note: If you want to use the returned data to create allocations you I<need> to
|
||||
enable all of these. To make this easier a special shortcut exists
|
||||
In this mode, C<with_objects> can be used to load C<warehouse>, C<bin>,
|
||||
654022f9 | Sven Schöling | C<parts> objects in one go, just like with Rose. They
|
||
9687d2ce | Sven Schöling | need to be present in C<by> before that though.
|
||
=head1 ALLOCATION ALGORITHM
|
||||
When calling allocate, the current onhand (== available stock) of the item will
|
||||
be used to decide which bins/chargenumbers/bestbefore can be used.
|
||||
In general allocate will try to make the request happen, and will use the
|
||||
provided charges up first, and then tap everything else. If you need to only
|
||||
I<exactly> use the provided charges, you'll need to craft the allocations
|
||||
yourself. See L</"ALLOCATION DATA STRUCTURE"> for that.
|
||||
If C<chargenumber> is given, those will be used up next.
|
||||
After that normal quantities will be used.
|
||||
These are tiebreakers and expected to rarely matter in reality. If you need
|
||||
finegrained control over which allocation is used, you may want to get the
|
||||
onhands yourself and select the appropriate ones.
|
||||
Only quantities with C<bestbefore> unset or after the given date will be
|
||||
considered. If more than one charge is eligible, the earlier C<bestbefore>
|
||||
will be used.
|
||||
Allocations do NOT have an internal memory and can't react to other allocations
|
||||
of the same part earlier. Never double allocate the same part within a
|
||||
transaction.
|
||||
=head1 ALLOCATION DATA STRUCTURE
|
||||
Allocations are instances of the helper class C<SL::Helper::Inventory::Allocation>. They require
|
||||
each of the following attributes to be set at creation time:
|
||||
=over 4
|
||||
=item * parts_id
|
||||
=item * qty
|
||||
=item * bin_id
|
||||
=item * warehouse_id
|
||||
=item * chargenumber
|
||||
=item * bestbefore
|
||||
52753418 | Bernd Bleßmann | =item * comment
|
||
155b8aa4 | Sven Schöling | =item * for_object_id
|
||
ecc3f8bc | Martin Helmling | |||
155b8aa4 | Sven Schöling | If set the allocations will be marked as allocated for the given object.
|
||
If these allocations are later used to produce an assembly, the resulting
|
||||
consuming transactions will be marked as belonging to the given object.
|
||||
The object may be an order, productionorder or other objects
|
||||
ecc3f8bc | Martin Helmling | |||
9687d2ce | Sven Schöling | =back
|
||
5d7aadc1 | Sven Schöling | C<chargenumber>, C<bestbefore> and C<for_object_id> and C<comment> may be
|
||
C<undef> (but must still be present at creation time). Instances are considered
|
||||
immutable.
|
||||
9687d2ce | Sven Schöling | |||
5d7aadc1 | Sven Schöling | Allocations also provide the method C<transfer_object> which will create a new
|
||
C<SL::DB::Inventory> bject with all the playload.
|
||||
ecc3f8bc | Martin Helmling | |||
1672b7f7 | Martin Helmling | =head1 CONSTRAINTS
|
||
# whitelist constraints
|
||||
->allocate(
|
||||
...
|
||||
constraints => {
|
||||
bin_id => \@allowed_bins,
|
||||
chargenumber => \@allowed_chargenumbers,
|
||||
}
|
||||
);
|
||||
# custom constraints
|
||||
->allocate(
|
||||
constraints => sub {
|
||||
# only allow chargenumbers with specific format
|
||||
all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_
|
||||
&&
|
||||
654022f9 | Sven Schöling | # and must all have a bestbefore date
|
||
all { $_->bestbefore } @_;
|
||||
1672b7f7 | Martin Helmling | }
|
||
)
|
||||
C<allocation> is "best effort" in nature. It will take the C<bin>,
|
||||
C<chargenumber> etc hints from the parameters, but will try it's bvest to
|
||||
fulfil the request anyway and only bail out if it is absolutely not possible.
|
||||
Sometimes you need to restrict allocations though. For this you can pass
|
||||
additional constraints to C<allocate>. A constraint serves as a whitelist.
|
||||
Every allocation must fulfil every constraint by having that attribute be one
|
||||
of the given values.
|
||||
In case even that is not enough, you may supply a custom check by passing a
|
||||
function that will be given the allocation objects.
|
||||
Note that both whitelists and constraints do not influence the order of
|
||||
allocations, which is done purely from the initial parameters. They only serve
|
||||
to reject allocations made in good faith which do fulfil required assertions.
|
||||
9687d2ce | Sven Schöling | =head1 ERROR HANDLING
|
||
C<allocate> and C<produce_assembly> will throw exceptions if the request can
|
||||
not be completed. The usual reason will be insufficient onhand to allocate, or
|
||||
insufficient allocations to process the request.
|
||||
bb12dc4d | Sven Schöling | =head1 KNOWN PROBLEMS
|
||
* It's not currently possible to identify allocations between requests, for
|
||||
example for presenting the user possible allocations and then actually using
|
||||
them on the next request.
|
||||
* It's not currently possible to give C<allocate> prior constraints.
|
||||
Currently all constraints are treated as hints (and will be preferred) but
|
||||
the internal ordering of the hints is fixed and more complex preferentials
|
||||
are not supported.
|
||||
* bestbefore handling is untested
|
||||
c591d7cc | Sven Schöling | * interaction with config option "transfer_default_ignore_onhand" is
|
||
currently undefined (and implicitly ignores it)
|
||||
bb12dc4d | Sven Schöling | |||
9687d2ce | Sven Schöling | =head1 TODO
|
||
* define and describe error classes
|
||||
* define wrapper classes for stock/onhand batch mode return values
|
||||
bb12dc4d | Sven Schöling | * handle extra arguments in produce: shippingdate, project
|
||
9687d2ce | Sven Schöling | * document no_ check
|
||
* tests
|
||||
=head1 BUGS
|
||||
None yet :)
|
||||
=head1 AUTHOR
|
||||
c591d7cc | Sven Schöling | Sven Schöling E<lt>sven.schoeling@googlemail.comE<gt>
|
||
9687d2ce | Sven Schöling | |||
=cut
|