c1406d05963c1ab4331ff7e6adb9734449756315
[kivitendo-erp.git] / SL / Controller / CsvImport / Inventory.pm
1 package SL::Controller::CsvImport::Inventory;
2
3
4 use strict;
5
6 use SL::Helper::Csv;
7 use SL::Helper::DateTime;
8
9 use SL::DBUtils;
10 use SL::DB::Inventory;
11 use SL::DB::Part;
12 use SL::DB::Warehouse;
13 use SL::DB::Bin;
14 use SL::DB::TransferType;
15 use SL::DB::Employee;
16
17 use parent qw(SL::Controller::CsvImport::Base);
18
19
20 use Rose::Object::MakeMethods::Generic
21 (
22  'scalar --get_set_init' => [ qw(settings parts_by warehouses_by bins_by) ],
23 );
24
25
26 sub init_class {
27   my ($self) = @_;
28   $self->class('SL::DB::Inventory');
29 }
30
31 sub set_profile_defaults {
32 };
33
34 sub init_profile {
35   my ($self) = @_;
36
37   my $profile = $self->SUPER::init_profile;
38   delete @{$profile}{qw(trans_id oe_id delivery_order_items_stock_id bestbefore trans_type_id project_id)};
39
40   return $profile;
41 }
42
43 sub init_settings {
44   my ($self) = @_;
45
46   return { map { ( $_ => $self->controller->profile->get($_) ) } qw(warehouse apply_warehouse
47                                                                     bin       apply_bin
48                                                                     comment   apply_comment) };
49 }
50
51 sub init_parts_by {
52   my ($self) = @_;
53
54   my $all_parts = SL::DB::Manager::Part->get_all;
55   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_parts } } ) } qw(id partnumber ean description) };
56 }
57
58 sub init_warehouses_by {
59   my ($self) = @_;
60
61   my $all_warehouses = SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]]);
62   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_warehouses } } ) } qw(id description) };
63 }
64
65 sub init_bins_by {
66   my ($self) = @_;
67
68   my $all_bins = SL::DB::Manager::Bin->get_all();
69   my $bins_by;
70   $bins_by->{_wh_id_and_id_ident()}          = { map { ( _wh_id_and_id_maker($_->warehouse_id, $_->id)                   => $_ ) } @{ $all_bins } };
71   $bins_by->{_wh_id_and_description_ident()} = { map { ( _wh_id_and_description_maker($_->warehouse_id, $_->description) => $_ ) } @{ $all_bins } };
72
73   return $bins_by;
74 }
75
76 sub check_objects {
77   my ($self) = @_;
78
79   $self->controller->track_progress(phase => 'building data', progress => 0);
80
81   my $i;
82   my $num_data = scalar @{ $self->controller->data };
83   foreach my $entry (@{ $self->controller->data }) {
84     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
85
86     $self->check_warehouse($entry);
87     $self->check_bin($entry);
88     $self->check_part($entry);
89     $self->check_qty($entry)            unless scalar @{ $entry->{errors} };
90     $self->handle_comment($entry);
91     $self->handle_employee($entry);
92     $self->handle_transfer_type($entry) unless scalar @{ $entry->{errors} };
93     $self->handle_shippingdate($entry);
94   } continue {
95     $i++;
96   }
97
98   $self->add_info_columns(qw(warehouse bin partnumber employee target_qty));
99 }
100
101 sub setup_displayable_columns {
102   my ($self) = @_;
103
104   $self->SUPER::setup_displayable_columns;
105
106   $self->add_displayable_columns({ name => 'bin',          description => $::locale->text('Bin')                     },
107                                  { name => 'bin_id',       description => $::locale->text('Bin (database ID)')       },
108                                  { name => 'chargenumber', description => $::locale->text('Charge number')           },
109                                  { name => 'comment',      description => $::locale->text('Comment')                 },
110                                  { name => 'employee_id',  description => $::locale->text('Employee (database ID)')  },
111                                  { name => 'partnumber',   description => $::locale->text('Part Number')             },
112                                  { name => 'parts_id',     description => $::locale->text('Part (database ID)')      },
113                                  { name => 'qty',          description => $::locale->text('qty (to transfer)')       },
114                                  { name => 'shippingdate', description => $::locale->text('Shipping date')           },
115                                  { name => 'target_qty',   description => $::locale->text('Target Qty')              },
116                                  { name => 'warehouse',    description => $::locale->text('Warehouse')               },
117                                  { name => 'warehouse_id', description => $::locale->text('Warehouse (database ID)') },
118                                 );
119 }
120
121 sub check_warehouse {
122   my ($self, $entry) = @_;
123
124   my $object = $entry->{object};
125
126   # If warehouse from front-end is enforced for all transfers, use this, if valid.
127   if ($self->settings->{apply_warehouse} eq 'all') {
128     $object->warehouse_id(undef);
129     my $wh = $self->warehouses_by->{description}->{ $self->settings->{warehouse} };
130     if (!$wh) {
131       push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
132       return 0;
133     }
134
135     $object->warehouse_id($wh->id);
136   }
137
138   # If warehouse from front-end is enforced for transfers with missing warehouse, use this, if valid.
139   if (    $self->settings->{apply_warehouse} eq 'missing'
140        && ! $object->warehouse_id
141        && ! $entry->{raw_data}->{warehouse} ) {
142     my $wh = $self->warehouses_by->{description}->{ $self->settings->{warehouse} };
143     if (!$wh) {
144       push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
145       return 0;
146     }
147
148     $object->warehouse_id($wh->id);
149   }
150
151   # Check wether or not warehouse ID is valid.
152   if ($object->warehouse_id && !$self->warehouses_by->{id}->{ $object->warehouse_id }) {
153     push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
154     return 0;
155   }
156
157   # Map description to ID if given.
158   if (!$object->warehouse_id && $entry->{raw_data}->{warehouse}) {
159     my $wh = $self->warehouses_by->{description}->{ $entry->{raw_data}->{warehouse} };
160     if (!$wh) {
161       push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
162       return 0;
163     }
164
165     $object->warehouse_id($wh->id);
166   }
167
168   if ($object->warehouse_id) {
169     $entry->{info_data}->{warehouse} = $self->warehouses_by->{id}->{ $object->warehouse_id }->description;
170   } else {
171     push @{ $entry->{errors} }, $::locale->text('Error: Warehouse not found');
172     return 0;
173   }
174
175   return 1;
176 }
177
178 # Check bin for given warehouse, so check_warehouse must be called first.
179 sub check_bin {
180   my ($self, $entry) = @_;
181
182   my $object = $entry->{object};
183
184   # If bin from front-end is enforced for all transfers, use this, if valid.
185   if ($self->settings->{apply_bin} eq 'all') {
186     $object->bin_id(undef);
187     my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $self->settings->{bin}) };
188     if (!$bin) {
189       push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
190       return 0;
191     }
192
193     $object->bin_id($bin->id);
194   }
195
196   # If bin from front-end is enforced for transfers with missing bin, use this, if valid.
197   if (    $self->settings->{apply_bin} eq 'missing'
198        && ! $object->bin_id
199        && ! $entry->{raw_data}->{bin} ) {
200     my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $self->settings->{bin}) };
201     if (!$bin) {
202       push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
203       return 0;
204     }
205
206     $object->bin_id($bin->id);
207   }
208
209   # Check wether or not bin ID is valid.
210   if ($object->bin_id && !$self->bins_by->{_wh_id_and_id_ident()}->{ _wh_id_and_id_maker($object->warehouse_id, $object->bin_id) }) {
211     push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
212     return 0;
213   }
214
215   # Map description to ID if given.
216   if (!$object->bin_id && $entry->{raw_data}->{bin}) {
217     my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $entry->{raw_data}->{bin}) };
218     if (!$bin) {
219       push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
220       return 0;
221     }
222
223     $object->bin_id($bin->id);
224   }
225
226   if ($object->bin_id) {
227     $entry->{info_data}->{bin} = $self->bins_by->{_wh_id_and_id_ident()}->{ _wh_id_and_id_maker($object->warehouse_id, $object->bin_id) }->description;
228   } else {
229     push @{ $entry->{errors} }, $::locale->text('Error: Bin not found');
230     return 0;
231   }
232
233   return 1;
234 }
235
236 sub check_part {
237   my ($self, $entry) = @_;
238
239   my $object = $entry->{object};
240
241   # Check wether or non part ID is valid.
242   if ($object->parts_id && !$self->parts_by->{id}->{ $object->parts_id }) {
243     push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
244     return 0;
245   }
246
247   # Map number to ID if given.
248   if (!$object->parts_id && $entry->{raw_data}->{partnumber}) {
249     my $part = $self->parts_by->{partnumber}->{ $entry->{raw_data}->{partnumber} };
250     if (!$part) {
251       push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
252       return 0;
253     }
254
255     $object->parts_id($part->id);
256   }
257
258   if ($object->parts_id) {
259     $entry->{info_data}->{partnumber} = $self->parts_by->{id}->{ $object->parts_id }->partnumber;
260   } else {
261     push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
262     return 0;
263   }
264
265   return 1;
266 }
267
268 # This imports inventories when target_qty is given, transfers else.
269 # So we get the actual qty in stock and transfer the difference in case of
270 # a given target_qty
271 sub check_qty{
272   my ($self, $entry) = @_;
273
274   my $object = $entry->{object};
275
276   if (! exists $entry->{raw_data}->{target_qty} && ! exists $entry->{raw_data}->{qty}) {
277     push @{ $entry->{errors} }, $::locale->text('Error: A quantity or a target quantity must be given.');
278     return 0;
279   }
280
281   if (exists $entry->{raw_data}->{target_qty} && exists $entry->{raw_data}->{qty}) {
282     push @{ $entry->{errors} }, $::locale->text('Error: A quantity and a target quantity could not be given both.');
283     return 0;
284   }
285
286   if (exists $entry->{raw_data}->{target_qty} && ($entry->{raw_data}->{target_qty} * 1) < 0) {
287     push @{ $entry->{errors} }, $::locale->text('Error: A negative target quantity is not allowed.');
288     return 0;
289   }
290
291   # Actual quantity is read from stock or is the result of transfers for the
292   # same part, warehouse, bin and chargenumber done before.
293   my $key = join '+', $object->parts_id, $object->warehouse_id, $object->bin_id, $object->chargenumber;
294   if (!exists $self->{resulting_quantities}->{$key}) {
295     my $query = <<SQL;
296       SELECT sum(qty) FROM inventory
297         WHERE parts_id = ? AND warehouse_id = ? AND bin_id = ? AND chargenumber = ?
298         GROUP BY warehouse_id, bin_id, chargenumber
299 SQL
300
301     my ($stocked_qty) = selectrow_query($::form, $::form->get_standard_dbh, $query,
302                                         $object->parts_id, $object->warehouse_id, $object->bin_id, $object->chargenumber);
303     $self->{resulting_quantities}->{$key} = $stocked_qty;
304   }
305   my $actual_qty = $self->{resulting_quantities}->{$key};
306
307   if (exists $entry->{raw_data}->{target_qty}) {
308     my $target_qty = $entry->{raw_data}->{target_qty} * 1;
309
310     $object->qty($target_qty - $actual_qty);
311     $self->add_columns(qw(qty));
312   }
313
314   if ($object->qty == 0) {
315     push @{ $entry->{errors} }, $::locale->text('Error: Quantity to transfer is zero.');
316     return 0;
317   }
318
319   # Check if resulting quantity is below zero.
320   if ( ($actual_qty + $object->qty) < 0 ) {
321     push @{ $entry->{errors} }, $::locale->text('Error: Transfer would result in a negative target quantity.');
322     return 0;
323   }
324
325   $self->{resulting_quantities}->{$key} += $object->qty;
326   $entry->{info_data}->{target_qty} = $self->{resulting_quantities}->{$key};
327
328   return 1;
329 }
330
331 sub handle_comment {
332   my ($self, $entry) = @_;
333
334   my $object = $entry->{object};
335
336   # If comment from front-end is enforced for all transfers, use this, if valid.
337   if ($self->settings->{apply_comment} eq 'all') {
338     $object->comment($self->settings->{comment});
339   }
340
341   # If comment from front-end is enforced for transfers with missing comment, use this, if valid.
342   if ($self->settings->{apply_comment} eq 'missing' && ! $object->comment) {
343     $object->comment($self->settings->{comment});
344   }
345
346   return;
347 }
348
349 sub handle_transfer_type  {
350   my ($self, $entry) = @_;
351
352   my $object = $entry->{object};
353
354   my $transfer_type = SL::DB::Manager::TransferType->find_by(description => 'correction',
355                                                              direction   => ($object->qty > 0)? 'in': 'out');
356   $object->trans_type($transfer_type);
357
358   return;
359 }
360
361 # ToDo: employee by name
362 sub handle_employee {
363   my ($self, $entry) = @_;
364
365   my $object = $entry->{object};
366
367   # employee from front end if not given
368   if (!$object->employee_id) {
369     $object->employee_id($self->controller->{employee_id})
370   }
371
372   # employee from login if not given
373   if (!$object->employee_id) {
374     $object->employee_id(SL::DB::Manager::Employee->find_by(login => $::myconfig{login})->id);
375   }
376
377   if ($object->employee_id) {
378     $entry->{info_data}->{employee} = $object->employee->name;
379   }
380
381 }
382
383 sub handle_shippingdate {
384   my ($self, $entry) = @_;
385
386   my $object = $entry->{object};
387
388   if (!$object->shippingdate) {
389     $object->shippingdate(DateTime->today_local);
390   }
391 }
392
393 sub save_objects {
394   my ($self, %params) = @_;
395
396   my $data = $params{data} || $self->controller->data;
397
398   foreach my $entry (@{ $data }) {
399     my ($trans_id) = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT nextval('id')|);
400     $entry->{object}->trans_id($trans_id);
401   }
402
403   $self->SUPER::save_objects(%params);
404 }
405
406 sub _wh_id_and_description_ident {
407   return 'wh_id+description';
408 }
409
410 sub _wh_id_and_description_maker {
411     return join '+', $_[0], $_[1]
412 }
413
414 sub _wh_id_and_id_ident {
415   return 'wh_id+id';
416 }
417
418 sub _wh_id_and_id_maker {
419     return join '+', $_[0], $_[1]
420 }
421
422 1;