Projekt

Allgemein

Profil

Herunterladen (28,3 KB) Statistiken
| Zweig: | Markierung: | Revision:
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