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');
 
  34   my $profile = $self->SUPER::init_profile;
 
  35   delete @{$profile}{qw(trans_id oe_id delivery_order_items_stock_id bestbefore trans_type_id project_id)};
 
  43   return { map { ( $_ => $self->controller->profile->get($_) ) } qw(warehouse apply_warehouse
 
  45                                                                     comment   apply_comment) };
 
  51   my $all_parts = SL::DB::Manager::Part->get_all;
 
  52   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_parts } } ) } qw(id partnumber ean description) };
 
  55 sub init_warehouses_by {
 
  58   my $all_warehouses = SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]]);
 
  59   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_warehouses } } ) } qw(id description) };
 
  65   my $all_bins = SL::DB::Manager::Bin->get_all();
 
  67   $bins_by->{_wh_id_and_id_ident()}          = { map { ( _wh_id_and_id_maker($_->warehouse_id, $_->id)                   => $_ ) } @{ $all_bins } };
 
  68   $bins_by->{_wh_id_and_description_ident()} = { map { ( _wh_id_and_description_maker($_->warehouse_id, $_->description) => $_ ) } @{ $all_bins } };
 
  76   $self->controller->track_progress(phase => 'building data', progress => 0);
 
  79   my $num_data = scalar @{ $self->controller->data };
 
  80   foreach my $entry (@{ $self->controller->data }) {
 
  81     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
 
  83     $self->check_warehouse($entry);
 
  84     $self->check_bin($entry);
 
  85     $self->check_part($entry);
 
  86     $self->check_qty($entry)            unless scalar @{ $entry->{errors} };
 
  87     $self->handle_comment($entry);
 
  88     $self->handle_employee($entry);
 
  89     $self->handle_transfer_type($entry) unless scalar @{ $entry->{errors} };
 
  90     $self->handle_shippingdate($entry);
 
  95   $self->add_info_columns(qw(warehouse bin partnumber employee target_qty));
 
  98 sub setup_displayable_columns {
 
 101   $self->SUPER::setup_displayable_columns;
 
 103   $self->add_displayable_columns({ name => 'bin',          description => $::locale->text('Bin')                     },
 
 104                                  { name => 'bin_id',       description => $::locale->text('Bin (database ID)')       },
 
 105                                  { name => 'chargenumber', description => $::locale->text('Charge number')           },
 
 106                                  { name => 'comment',      description => $::locale->text('Comment')                 },
 
 107                                  { name => 'employee_id',  description => $::locale->text('Employee (database ID)')  },
 
 108                                  { name => 'partnumber',   description => $::locale->text('Part Number')             },
 
 109                                  { name => 'parts_id',     description => $::locale->text('Part (database ID)')      },
 
 110                                  { name => 'qty',          description => $::locale->text('qty (to transfer)')       },
 
 111                                  { name => 'shippingdate', description => $::locale->text('Shipping date')           },
 
 112                                  { name => 'target_qty',   description => $::locale->text('Target Qty')              },
 
 113                                  { name => 'warehouse',    description => $::locale->text('Warehouse')               },
 
 114                                  { name => 'warehouse_id', description => $::locale->text('Warehouse (database ID)') },
 
 118 sub check_warehouse {
 
 119   my ($self, $entry) = @_;
 
 121   my $object = $entry->{object};
 
 123   # If warehouse from front-end is enforced for all transfers, use this, if valid.
 
 124   if ($self->settings->{apply_warehouse} eq 'all') {
 
 125     $object->warehouse_id(undef);
 
 126     my $wh = $self->warehouses_by->{description}->{ $self->settings->{warehouse} };
 
 128       push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
 
 132     $object->warehouse_id($wh->id);
 
 135   # If warehouse from front-end is enforced for transfers with missing warehouse, use this, if valid.
 
 136   if (    $self->settings->{apply_warehouse} eq 'missing'
 
 137        && ! $object->warehouse_id
 
 138        && ! $entry->{raw_data}->{warehouse} ) {
 
 139     my $wh = $self->warehouses_by->{description}->{ $self->settings->{warehouse} };
 
 141       push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
 
 145     $object->warehouse_id($wh->id);
 
 148   # Check wether or not warehouse ID is valid.
 
 149   if ($object->warehouse_id && !$self->warehouses_by->{id}->{ $object->warehouse_id }) {
 
 150     push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
 
 154   # Map description to ID if given.
 
 155   if (!$object->warehouse_id && $entry->{raw_data}->{warehouse}) {
 
 156     my $wh = $self->warehouses_by->{description}->{ $entry->{raw_data}->{warehouse} };
 
 158       push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
 
 162     $object->warehouse_id($wh->id);
 
 165   if ($object->warehouse_id) {
 
 166     $entry->{info_data}->{warehouse} = $self->warehouses_by->{id}->{ $object->warehouse_id }->description;
 
 168     push @{ $entry->{errors} }, $::locale->text('Error: Warehouse not found');
 
 175 # Check bin for given warehouse, so check_warehouse must be called first.
 
 177   my ($self, $entry) = @_;
 
 179   my $object = $entry->{object};
 
 181   # If bin from front-end is enforced for all transfers, use this, if valid.
 
 182   if ($self->settings->{apply_bin} eq 'all') {
 
 183     $object->bin_id(undef);
 
 184     my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $self->settings->{bin}) };
 
 186       push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
 
 190     $object->bin_id($bin->id);
 
 193   # If bin from front-end is enforced for transfers with missing bin, use this, if valid.
 
 194   if (    $self->settings->{apply_bin} eq 'missing'
 
 196        && ! $entry->{raw_data}->{bin} ) {
 
 197     my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $self->settings->{bin}) };
 
 199       push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
 
 203     $object->bin_id($bin->id);
 
 206   # Check wether or not bin ID is valid.
 
 207   if ($object->bin_id && !$self->bins_by->{_wh_id_and_id_ident()}->{ _wh_id_and_id_maker($object->warehouse_id, $object->bin_id) }) {
 
 208     push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
 
 212   # Map description to ID if given.
 
 213   if (!$object->bin_id && $entry->{raw_data}->{bin}) {
 
 214     my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $entry->{raw_data}->{bin}) };
 
 216       push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
 
 220     $object->bin_id($bin->id);
 
 223   if ($object->bin_id) {
 
 224     $entry->{info_data}->{bin} = $self->bins_by->{_wh_id_and_id_ident()}->{ _wh_id_and_id_maker($object->warehouse_id, $object->bin_id) }->description;
 
 226     push @{ $entry->{errors} }, $::locale->text('Error: Bin not found');
 
 234   my ($self, $entry) = @_;
 
 236   my $object = $entry->{object};
 
 238   # Check wether or non part ID is valid.
 
 239   if ($object->parts_id && !$self->parts_by->{id}->{ $object->parts_id }) {
 
 240     push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
 
 244   # Map number to ID if given.
 
 245   if (!$object->parts_id && $entry->{raw_data}->{partnumber}) {
 
 246     my $part = $self->parts_by->{partnumber}->{ $entry->{raw_data}->{partnumber} };
 
 248       push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
 
 252     $object->parts_id($part->id);
 
 255   if ($object->parts_id) {
 
 256     $entry->{info_data}->{partnumber} = $self->parts_by->{id}->{ $object->parts_id }->partnumber;
 
 258     push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
 
 265 # This imports inventories when target_qty is given, transfers else.
 
 266 # So we get the actual qty in stock and transfer the difference in case of
 
 269   my ($self, $entry) = @_;
 
 271   my $object = $entry->{object};
 
 273   if (! exists $entry->{raw_data}->{target_qty} && ! exists $entry->{raw_data}->{qty}) {
 
 274     push @{ $entry->{errors} }, $::locale->text('Error: A quantity or a target quantity must be given.');
 
 278   if (exists $entry->{raw_data}->{target_qty} && exists $entry->{raw_data}->{qty}) {
 
 279     push @{ $entry->{errors} }, $::locale->text('Error: A quantity and a target quantity could not be given both.');
 
 283   if (exists $entry->{raw_data}->{target_qty} && ($entry->{raw_data}->{target_qty} * 1) < 0) {
 
 284     push @{ $entry->{errors} }, $::locale->text('Error: A negative target quantity is not allowed.');
 
 288   # Actual quantity is read from stock or is the result of transfers for the
 
 289   # same part, warehouse, bin and chargenumber done before.
 
 290   my $key = join '+', $object->parts_id, $object->warehouse_id, $object->bin_id, $object->chargenumber;
 
 291   if (!exists $self->{resulting_quantities}->{$key}) {
 
 293       SELECT sum(qty) FROM inventory
 
 294         WHERE parts_id = ? AND warehouse_id = ? AND bin_id = ? AND chargenumber = ?
 
 295         GROUP BY warehouse_id, bin_id, chargenumber
 
 298     my ($stocked_qty) = selectrow_query($::form, $::form->get_standard_dbh, $query,
 
 299                                         $object->parts_id, $object->warehouse_id, $object->bin_id, $object->chargenumber);
 
 300     $self->{resulting_quantities}->{$key} = $stocked_qty;
 
 302   my $actual_qty = $self->{resulting_quantities}->{$key};
 
 304   if (exists $entry->{raw_data}->{target_qty}) {
 
 305     my $target_qty = $entry->{raw_data}->{target_qty} * 1;
 
 307     $object->qty($target_qty - $actual_qty);
 
 308     $self->add_columns(qw(qty));
 
 311   if ($object->qty == 0) {
 
 312     push @{ $entry->{errors} }, $::locale->text('Error: Quantity to transfer is zero.');
 
 316   # Check if resulting quantity is below zero.
 
 317   if ( ($actual_qty + $object->qty) < 0 ) {
 
 318     push @{ $entry->{errors} }, $::locale->text('Error: Transfer would result in a negative target quantity.');
 
 322   $self->{resulting_quantities}->{$key} += $object->qty;
 
 323   $entry->{info_data}->{target_qty} = $self->{resulting_quantities}->{$key};
 
 329   my ($self, $entry) = @_;
 
 331   my $object = $entry->{object};
 
 333   # If comment from front-end is enforced for all transfers, use this, if valid.
 
 334   if ($self->settings->{apply_comment} eq 'all') {
 
 335     $object->comment($self->settings->{comment});
 
 338   # If comment from front-end is enforced for transfers with missing comment, use this, if valid.
 
 339   if ($self->settings->{apply_comment} eq 'missing' && ! $object->comment) {
 
 340     $object->comment($self->settings->{comment});
 
 346 sub handle_transfer_type  {
 
 347   my ($self, $entry) = @_;
 
 349   my $object = $entry->{object};
 
 351   my $transfer_type = SL::DB::Manager::TransferType->find_by(description => 'correction',
 
 352                                                              direction   => ($object->qty > 0)? 'in': 'out');
 
 353   $object->trans_type($transfer_type);
 
 358 # ToDo: employee by name
 
 359 sub handle_employee {
 
 360   my ($self, $entry) = @_;
 
 362   my $object = $entry->{object};
 
 364   # employee from front end if not given
 
 365   if (!$object->employee_id) {
 
 366     $object->employee_id($self->controller->{employee_id})
 
 369   # employee from login if not given
 
 370   if (!$object->employee_id) {
 
 371     $object->employee_id(SL::DB::Manager::Employee->find_by(login => $::myconfig{login})->id);
 
 374   if ($object->employee_id) {
 
 375     $entry->{info_data}->{employee} = $object->employee->name;
 
 380 sub handle_shippingdate {
 
 381   my ($self, $entry) = @_;
 
 383   my $object = $entry->{object};
 
 385   if (!$object->shippingdate) {
 
 386     $object->shippingdate(DateTime->today_local);
 
 391   my ($self, %params) = @_;
 
 393   my $data = $params{data} || $self->controller->data;
 
 395   foreach my $entry (@{ $data }) {
 
 396     my ($trans_id) = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT nextval('id')|);
 
 397     $entry->{object}->trans_id($trans_id);
 
 400   $self->SUPER::save_objects(%params);
 
 403 sub _wh_id_and_description_ident {
 
 404   return 'wh_id+description';
 
 407 sub _wh_id_and_description_maker {
 
 408     return join '+', $_[0], $_[1]
 
 411 sub _wh_id_and_id_ident {
 
 415 sub _wh_id_and_id_maker {
 
 416     return join '+', $_[0], $_[1]