4 |
4 |
use parent qw(SL::Controller::Base);
|
5 |
5 |
|
6 |
6 |
use English qw(-no_match_vars);
|
7 |
|
use List::Util qw(sum0);
|
|
7 |
use List::Util qw(sum sum0);
|
8 |
8 |
use POSIX qw(strftime);
|
9 |
9 |
|
10 |
10 |
use SL::Controller::Helper::GetModels;
|
11 |
11 |
use SL::Controller::Helper::ReportGenerator;
|
12 |
12 |
use SL::DB::Employee;
|
|
13 |
use SL::DB::Inventory;
|
13 |
14 |
use SL::DB::StockCounting;
|
14 |
15 |
use SL::DB::StockCountingItem;
|
15 |
16 |
use SL::Helper::Flash qw(flash_later);
|
... | ... | |
18 |
19 |
use SL::ReportGenerator;
|
19 |
20 |
use SL::WH;
|
20 |
21 |
|
|
22 |
|
|
23 |
use Data::Dumper;
|
|
24 |
|
21 |
25 |
use Rose::Object::MakeMethods::Generic(
|
22 |
26 |
#scalar => [ qw() ],
|
23 |
27 |
'scalar --get_set_init' => [ qw(countings models) ],
|
... | ... | |
74 |
78 |
$objects = \@grouped_objects;
|
75 |
79 |
|
76 |
80 |
$self->get_stocked($objects);
|
|
81 |
$self->get_inbetweens($objects);
|
77 |
82 |
|
78 |
83 |
$self->setup_list_action_bar;
|
79 |
84 |
$self->report_generator_list_objects(report => $self->{report}, objects => $objects);
|
... | ... | |
82 |
87 |
sub action_reconcile {
|
83 |
88 |
my ($self) = @_;
|
84 |
89 |
|
85 |
|
my $counting_id = $::form->{filter}->{counting_id};
|
86 |
|
my $counting_items = SL::DB::Manager::StockCountingItem->get_all(query => [counting_id => $counting_id]);
|
87 |
|
|
88 |
|
my $counted_qty = sum0 map { $_->qty } @$counting_items;
|
89 |
|
my $stocked_qty = $counting_items->[0]->part->get_stock(bin_id => $counting_items->[0]->bin_id);
|
90 |
|
|
91 |
|
my $comment = t8('correction from stock counting (counting "#1")', $counting_items->[0]->counting->name);
|
|
90 |
my $counting = SL::DB::StockCounting->new(id => $::form->{counting_id})->load;
|
|
91 |
# todo: sanity checks
|
|
92 |
# return if $counting->is_reconciliated;
|
|
93 |
# return if scalar(@{$counting->items}) == 0;
|
|
94 |
|
|
95 |
my $counting_items = $counting->items;
|
|
96 |
|
|
97 |
$self->get_stocked($counting_items);
|
|
98 |
$self->get_inbetweens($counting_items);
|
|
99 |
|
|
100 |
my $counted_by_part_and_bin;
|
|
101 |
my $stocked_by_part_and_bin;
|
|
102 |
my $inbetweens_by_part_and_bin;
|
|
103 |
my $item_ids_by_part_and_bin;
|
|
104 |
foreach my $item (@$counting_items) {
|
|
105 |
$counted_by_part_and_bin ->{$item->part_id}->{$item->bin_id} += $item->qty;
|
|
106 |
$stocked_by_part_and_bin ->{$item->part_id}->{$item->bin_id} ||= $item->{stocked};
|
|
107 |
$inbetweens_by_part_and_bin->{$item->part_id}->{$item->bin_id} ||= $item->{inbetweens};
|
|
108 |
push @{$item_ids_by_part_and_bin->{$item->part_id}->{$item->bin_id}}, $item->id;
|
|
109 |
}
|
92 |
110 |
|
93 |
|
my $transfer_qty = $counted_qty - $stocked_qty;
|
94 |
|
my $src_or_dst = $transfer_qty < 0? 'src' : 'dst';
|
95 |
|
$transfer_qty = abs($transfer_qty);
|
|
111 |
my $comment = t8('correction from stock counting (counting "#1")', $counting->name);
|
96 |
112 |
|
97 |
113 |
my $transfer_error;
|
98 |
|
# do stock
|
99 |
114 |
$::form->throw_on_error(sub {
|
100 |
115 |
eval {
|
101 |
|
WH->transfer({
|
102 |
|
parts => $counting_items->[0]->part,
|
103 |
|
$src_or_dst.'_bin' => $counting_items->[0]->bin,
|
104 |
|
$src_or_dst.'_wh' => $counting_items->[0]->bin->warehouse,
|
105 |
|
qty => $transfer_qty,
|
106 |
|
unit => $counting_items->[0]->part->unit,
|
107 |
|
transfer_type => 'correction',
|
108 |
|
comment => $comment,
|
109 |
|
});
|
|
116 |
SL::DB->client->with_transaction(sub {
|
|
117 |
foreach my $part_id (keys %$counted_by_part_and_bin) {
|
|
118 |
foreach my $bin_id (keys %{$counted_by_part_and_bin->{$part_id}}) {
|
|
119 |
my $counted_qty = $counted_by_part_and_bin ->{$part_id}->{$bin_id};
|
|
120 |
my $stocked_qty = $stocked_by_part_and_bin ->{$part_id}->{$bin_id};
|
|
121 |
my $inbetween_qty = $inbetweens_by_part_and_bin->{$part_id}->{$bin_id};
|
|
122 |
|
|
123 |
my $transfer_qty = $counted_qty - $stocked_qty + $inbetween_qty;
|
|
124 |
|
|
125 |
my $src_or_dst = $transfer_qty < 0? 'src' : 'dst';
|
|
126 |
$transfer_qty = abs($transfer_qty);
|
|
127 |
|
|
128 |
# Do stock.
|
|
129 |
# todo: run in transaction and record the inventory id in the counting items
|
|
130 |
my %transfer_params = (
|
|
131 |
parts_id => $part_id,
|
|
132 |
$src_or_dst.'_bin_id' => $bin_id,
|
|
133 |
qty => $transfer_qty,
|
|
134 |
transfer_type => 'correction',
|
|
135 |
comment => $comment,
|
|
136 |
);
|
|
137 |
|
|
138 |
my @trans_ids = WH->transfer(\%transfer_params);
|
|
139 |
|
|
140 |
if (scalar(@trans_ids) != 1) {
|
|
141 |
die "Program logic error: no error, but no transfer" if scalar(@trans_ids) == 0;
|
|
142 |
die "Program logic error: too many transfers" if scalar(@trans_ids) > 1;
|
|
143 |
}
|
|
144 |
|
|
145 |
# Get inventory entries via trans_ids-
|
|
146 |
my $inventories = SL::DB::Manager::Inventory->get_all(where => [trans_id => $trans_ids[0]]);
|
|
147 |
if (scalar(@$inventories) != 1) {
|
|
148 |
die "Program logic error: no error, but no inventory entry" if scalar(@$inventories) == 0;
|
|
149 |
die "Program logic error: too many inventory entries" if scalar(@$inventories) > 1;
|
|
150 |
}
|
|
151 |
|
|
152 |
|
|
153 |
SL::DB::Manager::StockCountingItem->update_all(set => {correction_inventory_id => $inventories->[0]->id},
|
|
154 |
where => [id => $item_ids_by_part_and_bin->{$part_id}->{$bin_id}]);
|
|
155 |
}
|
|
156 |
}
|
|
157 |
|
|
158 |
1;
|
|
159 |
}) or do { die SL::DB->client->error; }; # end of with_transaction
|
|
160 |
|
110 |
161 |
1;
|
111 |
|
} or do { $transfer_error = ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR; }
|
112 |
|
});
|
|
162 |
} or do { $transfer_error = ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR; }; # end of eval
|
|
163 |
|
|
164 |
}); # end of throw_on_error
|
113 |
165 |
|
114 |
166 |
if ($transfer_error) {
|
115 |
167 |
flash_later('error', $transfer_error);
|
... | ... | |
146 |
198 |
my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
|
147 |
199 |
$self->{report} = $report;
|
148 |
200 |
|
149 |
|
my @columns = qw(ids counting part bin qty stocked);
|
|
201 |
my @columns = qw(counting part bin qty stocked inbetweens);
|
150 |
202 |
|
151 |
203 |
my %column_defs = (
|
152 |
204 |
counting => { text => t8('Stock Counting'), sub => sub { $_[0]->counting->name }, },
|
... | ... | |
156 |
208 |
bin => { text => t8('Bin'), sub => sub { $_[0]->bin->full_description } },
|
157 |
209 |
employee => { text => t8('Employee'), sub => sub { $_[0]->employee ? $_[0]->employee->safe_name : '---'} },
|
158 |
210 |
stocked => { text => t8('Stocked Qty'), sub => sub { _format_total($_[0]->{stocked}) }, align => 'right'},
|
|
211 |
inbetweens => { text => t8('Inbetweens Qty'), sub => sub { _format_total($_[0]->{inbetweens}) }, align => 'right'},
|
159 |
212 |
);
|
160 |
213 |
|
161 |
214 |
# remove columns from defs which are not in @columns
|
... | ... | |
187 |
240 |
|
188 |
241 |
$report->set_options(
|
189 |
242 |
raw_top_info_text => $self->render('stock_counting_reconciliation/report_top', { output => 0 }),
|
190 |
|
raw_bottom_info_text => $self->render('stock_counting_reconciliation/report_bottom', { output => 0 }, models => $self->models),
|
|
243 |
raw_bottom_info_text => $self->render('stock_counting_reconciliation/report_bottom', { output => 0 }, models => $self->models, counting_id => $::form->{filter}->{counting_id}),
|
191 |
244 |
attachment_basename => t8('stock_countings') . strftime('_%Y%m%d', localtime time),
|
192 |
245 |
);
|
193 |
246 |
}
|
... | ... | |
218 |
271 |
$_->{stocked} = $_->part->get_stock(bin_id => $_->bin_id) for @$objects;
|
219 |
272 |
}
|
220 |
273 |
|
|
274 |
sub get_inbetweens {
|
|
275 |
my ($self, $objects) = @_;
|
|
276 |
|
|
277 |
# Get changes in stock while a counting was active.
|
|
278 |
# (i.e. from start of counting till now).
|
|
279 |
# Use itime, because shippingdate has no time component.
|
|
280 |
# Ignore stock counting corrections.
|
|
281 |
# Todo: warn if itime::date != shippingdate?
|
|
282 |
|
|
283 |
my $correction_inventory_ids = SL::DB::Manager::StockCountingItem->get_all(where => ['!correction_inventory_id' => undef],
|
|
284 |
select => ['correction_inventory_id'],
|
|
285 |
distcint => 1);
|
|
286 |
foreach my $object (@$objects) {
|
|
287 |
my $start = $object->counting->start_time_of_counting;
|
|
288 |
my $inbetweens = SL::DB::Manager::Inventory->get_all(where => [itime => { ge => $start },
|
|
289 |
parts_id => $object->part_id,
|
|
290 |
bin_id => $object->bin_id,
|
|
291 |
'!id' => [map { $_->correction_inventory_id } @$correction_inventory_ids]],
|
|
292 |
select => ['qty']);
|
|
293 |
$object->{inbetweens} = sum map { $_->qty } @$inbetweens;
|
|
294 |
}
|
|
295 |
}
|
|
296 |
|
221 |
297 |
sub setup_list_action_bar {
|
222 |
298 |
my ($self) = @_;
|
223 |
299 |
|
Zwischeninventur-Abgleich: Bewegungen zwischen Zählung und Abgleich berücksichtigen