trailing whitespaces entfernt
[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 init_profile {
32   my ($self) = @_;
33
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)};
36
37   return $profile;
38 }
39
40 sub init_settings {
41   my ($self) = @_;
42
43   return { map { ( $_ => $self->controller->profile->get($_) ) } qw(warehouse apply_warehouse
44                                                                     bin       apply_bin
45                                                                     comment   apply_comment) };
46 }
47
48 sub init_parts_by {
49   my ($self) = @_;
50
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) };
53 }
54
55 sub init_warehouses_by {
56   my ($self) = @_;
57
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) };
60 }
61
62 sub init_bins_by {
63   my ($self) = @_;
64
65   my $all_bins = SL::DB::Manager::Bin->get_all();
66   my $bins_by;
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 } };
69
70   return $bins_by;
71 }
72
73 sub check_objects {
74   my ($self) = @_;
75
76   $self->controller->track_progress(phase => 'building data', progress => 0);
77
78   my $i;
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;
82
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);
91   } continue {
92     $i++;
93   }
94
95   $self->add_info_columns(qw(warehouse bin partnumber employee target_qty));
96 }
97
98 sub setup_displayable_columns {
99   my ($self) = @_;
100
101   $self->SUPER::setup_displayable_columns;
102
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)') },
115                                 );
116 }
117
118 sub check_warehouse {
119   my ($self, $entry) = @_;
120
121   my $object = $entry->{object};
122
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} };
127     if (!$wh) {
128       push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
129       return 0;
130     }
131
132     $object->warehouse_id($wh->id);
133   }
134
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} };
140     if (!$wh) {
141       push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
142       return 0;
143     }
144
145     $object->warehouse_id($wh->id);
146   }
147
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');
151     return 0;
152   }
153
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} };
157     if (!$wh) {
158       push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
159       return 0;
160     }
161
162     $object->warehouse_id($wh->id);
163   }
164
165   if ($object->warehouse_id) {
166     $entry->{info_data}->{warehouse} = $self->warehouses_by->{id}->{ $object->warehouse_id }->description;
167   } else {
168     push @{ $entry->{errors} }, $::locale->text('Error: Warehouse not found');
169     return 0;
170   }
171
172   return 1;
173 }
174
175 # Check bin for given warehouse, so check_warehouse must be called first.
176 sub check_bin {
177   my ($self, $entry) = @_;
178
179   my $object = $entry->{object};
180
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}) };
185     if (!$bin) {
186       push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
187       return 0;
188     }
189
190     $object->bin_id($bin->id);
191   }
192
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'
195        && ! $object->bin_id
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}) };
198     if (!$bin) {
199       push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
200       return 0;
201     }
202
203     $object->bin_id($bin->id);
204   }
205
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');
209     return 0;
210   }
211
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}) };
215     if (!$bin) {
216       push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
217       return 0;
218     }
219
220     $object->bin_id($bin->id);
221   }
222
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;
225   } else {
226     push @{ $entry->{errors} }, $::locale->text('Error: Bin not found');
227     return 0;
228   }
229
230   return 1;
231 }
232
233 sub check_part {
234   my ($self, $entry) = @_;
235
236   my $object = $entry->{object};
237
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');
241     return 0;
242   }
243
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} };
247     if (!$part) {
248       push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
249       return 0;
250     }
251
252     $object->parts_id($part->id);
253   }
254
255   if ($object->parts_id) {
256     $entry->{info_data}->{partnumber} = $self->parts_by->{id}->{ $object->parts_id }->partnumber;
257   } else {
258     push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
259     return 0;
260   }
261
262   return 1;
263 }
264
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
267 # a given target_qty
268 sub check_qty{
269   my ($self, $entry) = @_;
270
271   my $object = $entry->{object};
272
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.');
275     return 0;
276   }
277
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.');
280     return 0;
281   }
282
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.');
285     return 0;
286   }
287
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}) {
292     my $query = <<SQL;
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
296 SQL
297
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;
301   }
302   my $actual_qty = $self->{resulting_quantities}->{$key};
303
304   if (exists $entry->{raw_data}->{target_qty}) {
305     my $target_qty = $entry->{raw_data}->{target_qty} * 1;
306
307     $object->qty($target_qty - $actual_qty);
308     $self->add_columns(qw(qty));
309   }
310
311   if ($object->qty == 0) {
312     push @{ $entry->{errors} }, $::locale->text('Error: Quantity to transfer is zero.');
313     return 0;
314   }
315
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.');
319     return 0;
320   }
321
322   $self->{resulting_quantities}->{$key} += $object->qty;
323   $entry->{info_data}->{target_qty} = $self->{resulting_quantities}->{$key};
324
325   return 1;
326 }
327
328 sub handle_comment {
329   my ($self, $entry) = @_;
330
331   my $object = $entry->{object};
332
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});
336   }
337
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});
341   }
342
343   return;
344 }
345
346 sub handle_transfer_type  {
347   my ($self, $entry) = @_;
348
349   my $object = $entry->{object};
350
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);
354
355   return;
356 }
357
358 # ToDo: employee by name
359 sub handle_employee {
360   my ($self, $entry) = @_;
361
362   my $object = $entry->{object};
363
364   # employee from front end if not given
365   if (!$object->employee_id) {
366     $object->employee_id($self->controller->{employee_id})
367   }
368
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);
372   }
373
374   if ($object->employee_id) {
375     $entry->{info_data}->{employee} = $object->employee->name;
376   }
377
378 }
379
380 sub handle_shippingdate {
381   my ($self, $entry) = @_;
382
383   my $object = $entry->{object};
384
385   if (!$object->shippingdate) {
386     $object->shippingdate(DateTime->today_local);
387   }
388 }
389
390 sub save_objects {
391   my ($self, %params) = @_;
392
393   my $data = $params{data} || $self->controller->data;
394
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);
398   }
399
400   $self->SUPER::save_objects(%params);
401 }
402
403 sub _wh_id_and_description_ident {
404   return 'wh_id+description';
405 }
406
407 sub _wh_id_and_description_maker {
408     return join '+', $_[0], $_[1]
409 }
410
411 sub _wh_id_and_id_ident {
412   return 'wh_id+id';
413 }
414
415 sub _wh_id_and_id_maker {
416     return join '+', $_[0], $_[1]
417 }
418
419 1;