|
1 |
package SL::Controller::StockCountingReconciliation;
|
|
2 |
|
|
3 |
use strict;
|
|
4 |
use parent qw(SL::Controller::Base);
|
|
5 |
|
|
6 |
use English qw(-no_match_vars);
|
|
7 |
use List::Util qw(sum0);
|
|
8 |
use POSIX qw(strftime);
|
|
9 |
|
|
10 |
use SL::Controller::Helper::GetModels;
|
|
11 |
use SL::Controller::Helper::ReportGenerator;
|
|
12 |
use SL::DB::Employee;
|
|
13 |
use SL::DB::StockCounting;
|
|
14 |
use SL::DB::StockCountingItem;
|
|
15 |
use SL::Helper::Flash qw(flash_later);
|
|
16 |
use SL::Helper::Number qw(_format_total);
|
|
17 |
use SL::JSON;
|
|
18 |
use SL::Locale::String qw(t8);
|
|
19 |
use SL::Presenter::Tag qw(checkbox_tag);
|
|
20 |
use SL::ReportGenerator;
|
|
21 |
use SL::WH;
|
|
22 |
|
|
23 |
use Rose::Object::MakeMethods::Generic(
|
|
24 |
#scalar => [ qw() ],
|
|
25 |
'scalar --get_set_init' => [ qw(countings models) ],
|
|
26 |
);
|
|
27 |
|
|
28 |
# check permissions
|
|
29 |
__PACKAGE__->run_before(sub { $::auth->assert('warehouse_management'); });
|
|
30 |
|
|
31 |
|
|
32 |
my %sort_columns = (
|
|
33 |
counting => t8('Stock Counting'),
|
|
34 |
counted_at => t8('Counted At'),
|
|
35 |
qty => t8('Qty'),
|
|
36 |
part => t8('Article'),
|
|
37 |
bin => t8('Bin'),
|
|
38 |
employee => t8('Employee'),
|
|
39 |
);
|
|
40 |
|
|
41 |
|
|
42 |
sub action_list {
|
|
43 |
my ($self, %params) = @_;
|
|
44 |
|
|
45 |
$self->make_filter_summary;
|
|
46 |
$self->prepare_report;
|
|
47 |
|
|
48 |
my $objects = $self->models->get;
|
|
49 |
|
|
50 |
if ($::form->{group_counting_items}) {
|
|
51 |
my $grouped_objects_by;
|
|
52 |
my @grouped_objects;
|
|
53 |
foreach my $object (@$objects) {
|
|
54 |
my $group_object;
|
|
55 |
if (!$grouped_objects_by->{$object->counting_id}->{$object->part_id}->{$object->bin_id}) {
|
|
56 |
$group_object = SL::DB::StockCountingItem->new(
|
|
57 |
counting => $object->counting, part => $object->part, bin => $object->bin, qty => 0);
|
|
58 |
push @grouped_objects, $group_object;
|
|
59 |
$grouped_objects_by->{$object->counting_id}->{$object->part_id}->{$object->bin_id} = $group_object;
|
|
60 |
|
|
61 |
} else {
|
|
62 |
$group_object = $grouped_objects_by->{$object->counting_id}->{$object->part_id}->{$object->bin_id}
|
|
63 |
}
|
|
64 |
|
|
65 |
$group_object->id($group_object->id ? ($group_object->id . ',' . $object->id) : $object->id);
|
|
66 |
$group_object->qty($group_object->qty + $object->qty);
|
|
67 |
}
|
|
68 |
|
|
69 |
$objects = \@grouped_objects;
|
|
70 |
}
|
|
71 |
|
|
72 |
$self->get_stocked($objects);
|
|
73 |
|
|
74 |
$self->setup_list_action_bar;
|
|
75 |
$self->report_generator_list_objects(report => $self->{report}, objects => $objects);
|
|
76 |
}
|
|
77 |
|
|
78 |
sub action_reconcile {
|
|
79 |
my ($self) = @_;
|
|
80 |
|
|
81 |
my @transfer_errors;
|
|
82 |
|
|
83 |
foreach my $selection (@{$::form->{ids}}) {
|
|
84 |
my $ids = SL::JSON::from_json($selection);
|
|
85 |
my @counting_item_ids = split ',', $ids;
|
|
86 |
my $counting_items = SL::DB::Manager::StockCountingItem->get_all(query => [id => \@counting_item_ids]);
|
|
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);
|
|
92 |
|
|
93 |
my $transfer_qty = $counted_qty - $stocked_qty;
|
|
94 |
my $src_or_dst = $transfer_qty < 0? 'src' : 'dst';
|
|
95 |
$transfer_qty = abs($transfer_qty);
|
|
96 |
|
|
97 |
my $transfer_error;
|
|
98 |
# do stock
|
|
99 |
$::form->throw_on_error(sub {
|
|
100 |
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 |
});
|
|
110 |
1;
|
|
111 |
} or do { $transfer_error = ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR; }
|
|
112 |
});
|
|
113 |
|
|
114 |
push @transfer_errors, $transfer_error if $transfer_error;
|
|
115 |
}
|
|
116 |
|
|
117 |
if (@transfer_errors) {
|
|
118 |
flash_later('error', @transfer_errors);
|
|
119 |
} else {
|
|
120 |
flash_later('info', t8('successfully reconciled'));
|
|
121 |
}
|
|
122 |
|
|
123 |
return $self->redirect_to($::form->{callback}) if $::form->{callback};
|
|
124 |
}
|
|
125 |
|
|
126 |
sub init_models {
|
|
127 |
my ($self) = @_;
|
|
128 |
|
|
129 |
SL::Controller::Helper::GetModels->new(
|
|
130 |
controller => $_[0],
|
|
131 |
model => 'StockCountingItem',
|
|
132 |
sorted => \%sort_columns,
|
|
133 |
disable_plugin => 'paginated',
|
|
134 |
with_objects => [ 'counting', 'employee', 'part' ],
|
|
135 |
);
|
|
136 |
}
|
|
137 |
|
|
138 |
sub init_countings {
|
|
139 |
SL::DB::Manager::StockCounting->get_all_sorted;
|
|
140 |
}
|
|
141 |
|
|
142 |
|
|
143 |
sub prepare_report {
|
|
144 |
my ($self) = @_;
|
|
145 |
|
|
146 |
my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
|
|
147 |
$self->{report} = $report;
|
|
148 |
|
|
149 |
my @columns = $::form->{group_counting_items} ? qw(ids counting part bin qty stocked)
|
|
150 |
: qw(ids counting counted_at part bin qty stocked employee);
|
|
151 |
|
|
152 |
my %column_defs = (
|
|
153 |
ids => { raw_header_data => checkbox_tag("", id => "check_all", checkall => "[data-checkall=1]"),
|
|
154 |
align => 'center',
|
|
155 |
raw_data => sub { $_[0]->correction_inventory_id ? '' : checkbox_tag("ids[]", value => SL::JSON::to_json($_[0]->id), "data-checkall" => 1) } },
|
|
156 |
counting => { text => t8('Stock Counting'), sub => sub { $_[0]->counting->name }, },
|
|
157 |
counted_at => { text => t8('Counted At'), sub => sub { $_[0]->counted_at_as_timestamp }, },
|
|
158 |
qty => { text => t8('Qty'), sub => sub { $_[0]->qty_as_number }, align => 'right' },
|
|
159 |
part => { text => t8('Article'), sub => sub { $_[0]->part && $_[0]->part->displayable_name } },
|
|
160 |
bin => { text => t8('Bin'), sub => sub { $_[0]->bin->full_description } },
|
|
161 |
employee => { text => t8('Employee'), sub => sub { $_[0]->employee ? $_[0]->employee->safe_name : '---'} },
|
|
162 |
stocked => { text => t8('Stocked Qty'), sub => sub { _format_total($_[0]->{stocked}) }, align => 'right'},
|
|
163 |
);
|
|
164 |
|
|
165 |
# remove columns from defs which are not in @columns
|
|
166 |
foreach my $column (keys %column_defs) {
|
|
167 |
delete $column_defs{$column} if !grep { $column eq $_ } @columns;
|
|
168 |
}
|
|
169 |
|
|
170 |
my $title = t8('Stock Countings');
|
|
171 |
$report->{title} = $title; # for browser titlebar (title-tag)
|
|
172 |
|
|
173 |
$report->set_options(
|
|
174 |
controller_class => 'StockCountingReconciliation',
|
|
175 |
std_column_visibility => 1,
|
|
176 |
output_format => 'HTML',
|
|
177 |
title => $title, # for heading
|
|
178 |
allow_pdf_export => 1,
|
|
179 |
allow_csv_export => 1,
|
|
180 |
);
|
|
181 |
|
|
182 |
$report->set_columns(%column_defs);
|
|
183 |
$report->set_column_order(@columns);
|
|
184 |
$report->set_export_options(qw(list filter group_counting_items));
|
|
185 |
$report->set_options_from_form;
|
|
186 |
|
|
187 |
$self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
|
|
188 |
$self->models->add_additional_url_params(filter => $::form->{filter}, group_counting_items => $::form->{group_counting_items});
|
|
189 |
$self->models->finalize;
|
|
190 |
$self->models->set_report_generator_sort_options(report => $report, sortable_columns => [keys %sort_columns]);
|
|
191 |
|
|
192 |
$report->set_options(
|
|
193 |
raw_top_info_text => $self->render('stock_counting_reconciliation/report_top', { output => 0 }),
|
|
194 |
raw_bottom_info_text => $self->render('stock_counting_reconciliation/report_bottom', { output => 0 }, models => $self->models),
|
|
195 |
attachment_basename => t8('stock_countings') . strftime('_%Y%m%d', localtime time),
|
|
196 |
);
|
|
197 |
}
|
|
198 |
|
|
199 |
sub make_filter_summary {
|
|
200 |
my ($self) = @_;
|
|
201 |
|
|
202 |
my @filter_strings;
|
|
203 |
|
|
204 |
push @filter_strings, t8('Group Counting Items') if $::form->{group_counting_items};
|
|
205 |
|
|
206 |
my $filter = $::form->{filter} || {};
|
|
207 |
|
|
208 |
my $counting = $filter->{counting_id} ? SL::DB::StockCounting->new(id => $filter->{counting_id})->load->name : '';
|
|
209 |
|
|
210 |
my @filters = (
|
|
211 |
[ $counting, t8('Stock Counting') ],
|
|
212 |
);
|
|
213 |
|
|
214 |
for (@filters) {
|
|
215 |
push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
|
|
216 |
}
|
|
217 |
|
|
218 |
$self->{filter_summary} = join ', ', @filter_strings;
|
|
219 |
}
|
|
220 |
|
|
221 |
sub get_stocked {
|
|
222 |
my ($self, $objects) = @_;
|
|
223 |
|
|
224 |
$_->{stocked} = $_->part->get_stock(bin_id => $_->bin_id) for @$objects;
|
|
225 |
}
|
|
226 |
|
|
227 |
sub setup_list_action_bar {
|
|
228 |
my ($self) = @_;
|
|
229 |
|
|
230 |
for my $bar ($::request->layout->get('actionbar')) {
|
|
231 |
$bar->add(
|
|
232 |
action => [
|
|
233 |
t8('Update'),
|
|
234 |
submit => [ '#filter_form', { action => 'StockCountingReconciliation/list' } ],
|
|
235 |
accesskey => 'enter',
|
|
236 |
],
|
|
237 |
combobox => [
|
|
238 |
action => [
|
|
239 |
t8('Actions'),
|
|
240 |
],
|
|
241 |
action => [
|
|
242 |
t8('Reconcile'),
|
|
243 |
submit => [ '#form', { action => 'StockCountingReconciliation/reconcile', callback => $self->models->get_callback } ],
|
|
244 |
checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
|
|
245 |
confirm => t8('Do you really want the selected entries to be reconciled?'),
|
|
246 |
],
|
|
247 |
],
|
|
248 |
|
|
249 |
);
|
|
250 |
}
|
|
251 |
}
|
|
252 |
|
|
253 |
|
|
254 |
1;
|
Zwischeninventur: Controller für Abgleich