1 package SL::Controller::CsvImport::Inventory;
7 use SL::Helper::DateTime;
10 use SL::DB::Inventory;
12 use SL::DB::Warehouse;
14 use SL::DB::TransferType;
17 use parent qw(SL::Controller::CsvImport::Base);
20 use Rose::Object::MakeMethods::Generic
22 'scalar --get_set_init' => [ qw(settings parts_by warehouses_by bins_by) ],
28 $self->class('SL::DB::Inventory');
31 sub set_profile_defaults {
37 my $profile = $self->SUPER::init_profile;
38 delete @{$profile}{qw(trans_id oe_id delivery_order_items_stock_id trans_type_id project_id)};
39 delete @{$profile}{qw(bestbefore)} if !$::instance_conf->get_show_bestbefore;
47 return { map { ( $_ => $self->controller->profile->get($_) ) } qw(warehouse apply_warehouse
49 comment apply_comment) };
55 my $all_parts = SL::DB::Manager::Part->get_all;
56 return { map { my $col = $_; ( $col =>
57 { map { ( $_->$col => $_ ) } grep { defined $_->$col } @{ $all_parts } } ) } qw(id partnumber ean description) };
60 sub init_warehouses_by {
63 my $all_warehouses = SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]]);
64 return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_warehouses } } ) } qw(id description) };
70 my $all_bins = SL::DB::Manager::Bin->get_all();
72 $bins_by->{_wh_id_and_id_ident()} = { map { ( _wh_id_and_id_maker($_->warehouse_id, $_->id) => $_ ) } @{ $all_bins } };
73 $bins_by->{_wh_id_and_description_ident()} = { map { ( _wh_id_and_description_maker($_->warehouse_id, $_->description) => $_ ) } @{ $all_bins } };
80 $self->controller->track_progress(phase => 'building data', progress => 0);
83 my $num_data = scalar @{ $self->controller->data };
84 foreach my $entry (@{ $self->controller->data }) {
85 $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
87 $self->check_warehouse($entry);
88 $self->check_bin($entry);
89 $self->check_part($entry);
90 $self->check_qty($entry) unless scalar @{ $entry->{errors} };
91 $self->handle_comment($entry);
92 $self->handle_employee($entry);
93 $self->handle_transfer_type($entry) unless scalar @{ $entry->{errors} };
94 $self->handle_shippingdate($entry);
99 $self->add_info_columns(qw(warehouse bin partnumber employee target_qty));
102 sub setup_displayable_columns {
105 $self->SUPER::setup_displayable_columns;
107 $self->add_displayable_columns({ name => 'bin', description => $::locale->text('Bin') },
108 { name => 'bin_id', description => $::locale->text('Bin (database ID)') },
109 { name => 'chargenumber', description => $::locale->text('Charge number') },
110 { name => 'comment', description => $::locale->text('Comment') },
111 { name => 'employee_id', description => $::locale->text('Employee (database ID)') },
112 { name => 'partnumber', description => $::locale->text('Part Number') },
113 { name => 'parts_id', description => $::locale->text('Part (database ID)') },
114 { name => 'qty', description => $::locale->text('qty (to transfer)') },
115 { name => 'shippingdate', description => $::locale->text('Shipping date') },
116 { name => 'target_qty', description => $::locale->text('Target Qty') },
117 { name => 'warehouse', description => $::locale->text('Warehouse') },
118 { name => 'warehouse_id', description => $::locale->text('Warehouse (database ID)') },
120 if ($::instance_conf->get_show_bestbefore) {
121 $self->add_displayable_columns({ name => 'bestbefore', description => $::locale->text('Best Before') });
125 sub check_warehouse {
126 my ($self, $entry) = @_;
128 my $object = $entry->{object};
130 $self->settings->{apply_warehouse} ||= ''; # avoid warnings if undefined
132 # If warehouse from front-end is enforced for all transfers, use this, if valid.
133 if ($self->settings->{apply_warehouse} eq 'all') {
134 $object->warehouse_id(undef);
135 my $wh = $self->warehouses_by->{description}->{ $self->settings->{warehouse} };
137 push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
141 $object->warehouse_id($wh->id);
144 # If warehouse from front-end is enforced for transfers with missing warehouse, use this, if valid.
145 if ( $self->settings->{apply_warehouse} eq 'missing'
146 && ! $object->warehouse_id
147 && ! $entry->{raw_data}->{warehouse} ) {
148 my $wh = $self->warehouses_by->{description}->{ $self->settings->{warehouse} };
150 push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
154 $object->warehouse_id($wh->id);
157 # Check whether or not warehouse ID is valid.
158 if ($object->warehouse_id && !$self->warehouses_by->{id}->{ $object->warehouse_id }) {
159 push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
163 # Map description to ID if given.
164 if (!$object->warehouse_id && $entry->{raw_data}->{warehouse}) {
165 my $wh = $self->warehouses_by->{description}->{ $entry->{raw_data}->{warehouse} };
167 push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
171 $object->warehouse_id($wh->id);
174 if ($object->warehouse_id) {
175 $entry->{info_data}->{warehouse} = $self->warehouses_by->{id}->{ $object->warehouse_id }->description;
177 push @{ $entry->{errors} }, $::locale->text('Error: Warehouse not found');
184 # Check bin for given warehouse, so check_warehouse must be called first.
186 my ($self, $entry) = @_;
188 my $object = $entry->{object};
190 $self->settings->{apply_bin} ||= ''; # avoid warnings if undefined
192 # If bin from front-end is enforced for all transfers, use this, if valid.
193 if ($self->settings->{apply_bin} eq 'all') {
194 $object->bin_id(undef);
195 my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $self->settings->{bin}) };
197 push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
201 $object->bin_id($bin->id);
204 # If bin from front-end is enforced for transfers with missing bin, use this, if valid.
205 if ( $self->settings->{apply_bin} eq 'missing'
207 && ! $entry->{raw_data}->{bin} ) {
208 my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $self->settings->{bin}) };
210 push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
214 $object->bin_id($bin->id);
217 # Check whether or not bin ID is valid.
218 if ($object->bin_id && !$self->bins_by->{_wh_id_and_id_ident()}->{ _wh_id_and_id_maker($object->warehouse_id, $object->bin_id) }) {
219 push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
223 # Map description to ID if given.
224 if (!$object->bin_id && $entry->{raw_data}->{bin} && $object->warehouse_id) {
225 my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $entry->{raw_data}->{bin}) };
227 push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
231 $object->bin_id($bin->id);
234 if ($object->bin_id) {
235 $entry->{info_data}->{bin} = $self->bins_by->{_wh_id_and_id_ident()}->{ _wh_id_and_id_maker($object->warehouse_id, $object->bin_id) }->description;
237 push @{ $entry->{errors} }, $::locale->text('Error: Bin not found');
245 my ($self, $entry) = @_;
247 my $object = $entry->{object};
249 # Check whether or not part ID is valid.
250 if ($object->parts_id && !$self->parts_by->{id}->{ $object->parts_id }) {
251 push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
255 # Map number to ID if given.
256 if (!$object->parts_id && $entry->{raw_data}->{partnumber}) {
257 my $part = $self->parts_by->{partnumber}->{ $entry->{raw_data}->{partnumber} };
259 push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
263 $object->parts_id($part->id);
266 if ($object->parts_id) {
267 $entry->{info_data}->{partnumber} = $self->parts_by->{id}->{ $object->parts_id }->partnumber;
269 push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
276 # This imports inventories when target_qty is given, transfers else.
277 # So we get the actual qty in stock and transfer the difference in case of
280 my ($self, $entry) = @_;
282 my $object = $entry->{object};
284 # parse qty (may be float values)
285 if (exists $entry->{raw_data}->{target_qty}) {
286 $entry->{raw_data}->{target_qty} = $::form->parse_amount(\%::myconfig, $entry->{raw_data}->{target_qty});
287 # $object->target_qty($entry->{raw_data}->{target_qty});
289 if (exists $entry->{raw_data}->{qty}) {
290 $entry->{raw_data}->{qty} = $::form->parse_amount(\%::myconfig, $entry->{raw_data}->{qty});
291 $object->qty($entry->{raw_data}->{qty});
293 if (! exists $entry->{raw_data}->{target_qty} && ! exists $entry->{raw_data}->{qty}) {
294 push @{ $entry->{errors} }, $::locale->text('Error: A quantity or a target quantity must be given.');
298 if (exists $entry->{raw_data}->{target_qty} && exists $entry->{raw_data}->{qty}) {
299 push @{ $entry->{errors} }, $::locale->text('Error: A quantity and a target quantity could not be given both.');
303 if (exists $entry->{raw_data}->{target_qty} && ($entry->{raw_data}->{target_qty} * 1) < 0) {
304 push @{ $entry->{errors} }, $::locale->text('Error: A negative target quantity is not allowed.');
308 # Actual quantity is read from stock or is the result of transfers for the
309 # same part, warehouse, bin, chargenumber and bestbefore date (if
310 # show_bestbefore is enabled) done before.
311 my $key = join '+', $object->parts_id, $object->warehouse_id, $object->bin_id, $object->chargenumber;
312 $key .= join '+', $key, $object->bestbefore if $::instance_conf->get_show_bestbefore;
314 if (!exists $self->{resulting_quantities}->{$key}) {
315 $self->{resulting_quantities}->{$key} = _get_stocked_qty($object);
317 my $actual_qty = $self->{resulting_quantities}->{$key};
319 if (exists $entry->{raw_data}->{target_qty}) {
320 my $target_qty = $entry->{raw_data}->{target_qty} * 1;
322 $object->qty($target_qty - $actual_qty);
323 $self->add_columns(qw(qty));
326 if ($object->qty == 0) {
327 push @{ $entry->{errors} }, $::locale->text('Error: Quantity to transfer is zero.');
331 # Check if resulting quantity is below zero.
332 if ( ($actual_qty + $object->qty) < 0 ) {
333 push @{ $entry->{errors} }, $::locale->text('Error: Transfer would result in a negative target quantity.');
337 $self->{resulting_quantities}->{$key} += $object->qty;
338 $entry->{info_data}->{target_qty} = $self->{resulting_quantities}->{$key};
344 my ($self, $entry) = @_;
346 my $object = $entry->{object};
348 # If comment from front-end is enforced for all transfers, use this, if valid.
349 if ($self->settings->{apply_comment} eq 'all') {
350 $object->comment($self->settings->{comment});
353 # If comment from front-end is enforced for transfers with missing comment, use this, if valid.
354 if ($self->settings->{apply_comment} eq 'missing' && ! $object->comment) {
355 $object->comment($self->settings->{comment});
361 sub handle_transfer_type {
362 my ($self, $entry) = @_;
364 my $object = $entry->{object};
366 my $transfer_type = SL::DB::Manager::TransferType->find_by(description => 'correction',
367 direction => ($object->qty > 0)? 'in': 'out');
368 $object->trans_type($transfer_type);
373 # ToDo: employee by name
374 sub handle_employee {
375 my ($self, $entry) = @_;
377 my $object = $entry->{object};
379 # employee from front end if not given
380 if (!$object->employee_id) {
381 $object->employee_id($self->controller->{employee_id})
384 # employee from login if not given
385 if (!$object->employee_id) {
386 $object->employee_id(SL::DB::Manager::Employee->current->id) if SL::DB::Manager::Employee->current;
389 if ($object->employee_id) {
390 $entry->{info_data}->{employee} = $object->employee->name;
395 sub handle_shippingdate {
396 my ($self, $entry) = @_;
398 my $object = $entry->{object};
400 if (!$object->shippingdate) {
401 $object->shippingdate(DateTime->today_local);
406 my ($self, %params) = @_;
408 my $data = $params{data} || $self->controller->data;
410 foreach my $entry (@{ $data }) {
411 my ($trans_id) = selectrow_query($::form,$entry->{object}->db->dbh, qq|SELECT nextval('id')|);
412 $entry->{object}->trans_id($trans_id);
415 $self->SUPER::save_objects(%params);
418 sub _get_stocked_qty {
421 my $bestbefore_filter = '';
422 my $bestbefore_val_cnt = 0;
423 if ($::instance_conf->get_show_bestbefore) {
424 $bestbefore_filter = ($object->bestbefore) ? 'AND bestbefore = ?' : 'AND bestbefore IS NULL';
425 $bestbefore_val_cnt = ($object->bestbefore) ? 1 : 0;
429 SELECT sum(qty) FROM inventory
430 WHERE parts_id = ? AND warehouse_id = ? AND bin_id = ? AND chargenumber = ? $bestbefore_filter
431 GROUP BY warehouse_id, bin_id, chargenumber
434 my @values = ($object->parts_id,
435 $object->warehouse_id,
437 $object->chargenumber);
438 push @values, $object->bestbefore if $bestbefore_val_cnt;
440 my ($stocked_qty) = selectrow_query($::form, $object->db->dbh, $query, @values);
445 sub _wh_id_and_description_ident {
446 return 'wh_id+description';
449 sub _wh_id_and_description_maker {
450 return join '+', $_[0], $_[1]
453 sub _wh_id_and_id_ident {
457 sub _wh_id_and_id_maker {
458 return join '+', $_[0], $_[1]