Anzeige und Import von übersetzten Artikeltexten und Bemerkungen
[kivitendo-erp.git] / SL / Controller / CsvImport / Part.pm
1 package SL::Controller::CsvImport::Part;
2
3 use strict;
4
5 use SL::Helper::Csv;
6
7 use SL::DB::Buchungsgruppe;
8 use SL::DB::Language;
9 use SL::DB::PartsGroup;
10 use SL::DB::PaymentTerm;
11 use SL::DB::PriceFactor;
12 use SL::DB::Translation;
13 use SL::DB::Unit;
14
15 use parent qw(SL::Controller::CsvImport::Base);
16
17 use Rose::Object::MakeMethods::Generic
18 (
19  scalar                  => [ qw(table) ],
20  'scalar --get_set_init' => [ qw(bg_by settings parts_by price_factors_by units_by payment_terms_by packing_types_by partsgroups_by all_languages
21                                  translation_columns) ],
22 );
23
24 sub init_class {
25   my ($self) = @_;
26   $self->class('SL::DB::Part');
27 }
28
29 sub init_bg_by {
30   my ($self) = @_;
31
32   my $all_bg = SL::DB::Manager::Buchungsgruppe->get_all;
33   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_bg } } ) } qw(id description) };
34 }
35
36 sub init_price_factors_by {
37   my ($self) = @_;
38
39   my $all_price_factors = SL::DB::Manager::PriceFactor->get_all;
40   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_price_factors } } ) } qw(id description) };
41 }
42
43 sub init_payment_terms_by {
44   my ($self) = @_;
45
46   my $all_payment_terms = SL::DB::Manager::PaymentTerm->get_all;
47   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_payment_terms } } ) } qw(id description) };
48 }
49
50 sub init_packing_types_by {
51   my ($self) = @_;
52
53   my $all_packing_types = SL::DB::Manager::PackingType->get_all;
54   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_packing_types } } ) } qw(id description) };
55 }
56
57 sub init_partsgroups_by {
58   my ($self) = @_;
59
60   my $all_partsgroups = SL::DB::Manager::PartsGroup->get_all;
61   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_partsgroups } } ) } qw(id partsgroup) };
62 }
63
64 sub init_units_by {
65   my ($self) = @_;
66
67   my $all_units = SL::DB::Manager::Unit->get_all;
68   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_units } } ) } qw(name) };
69 }
70
71 sub init_parts_by {
72   my ($self) = @_;
73
74   my $parts_by = { id         => { map { ( $_->id => $_ ) } grep { !$_->assembly } @{ $self->existing_objects } },
75                    partnumber => { part    => { },
76                                    service => { } } };
77
78   foreach my $part (@{ $self->existing_objects }) {
79     next if $part->assembly;
80     $parts_by->{partnumber}->{ $part->type }->{ $part->partnumber } = $part;
81   }
82
83   return $parts_by;
84 }
85
86 sub init_settings {
87   my ($self) = @_;
88
89   return { map { ( $_ => $self->controller->profile->get($_) ) } qw(apply_buchungsgruppe default_buchungsgruppe article_number_policy
90                                                                     sellprice_places sellprice_adjustment sellprice_adjustment_type
91                                                                     shoparticle_if_missing parts_type) };
92 }
93
94 sub init_all_languages {
95   my ($self) = @_;
96
97   return SL::DB::Manager::Language->get_all;
98 }
99
100 sub init_translation_columns {
101   my ($self) = @_;
102
103   return [ map { ("description_" . $_->article_code, "notes_" . $_->article_code) } (@{ $self->all_languages }) ];
104 }
105
106 sub check_objects {
107   my ($self) = @_;
108
109   return unless @{ $self->controller->data };
110
111   foreach my $entry (@{ $self->controller->data }) {
112     my $object   = $entry->{object};
113     my $raw_data = $entry->{raw_data};
114
115     next unless $self->check_buchungsgruppe($entry);
116     next unless $self->check_type($entry);
117     next unless $self->check_unit($entry);
118     next unless $self->check_price_factor($entry);
119     next unless $self->check_payment($entry);
120     next unless $self->check_packing_type($entry);
121     next unless $self->check_partsgroup($entry);
122     $self->check_existing($entry);
123     $self->handle_prices($entry) if $self->settings->{sellprice_adjustment};
124     $self->handle_shoparticle($entry);
125     $self->handle_translations($entry);
126     $self->set_various_fields($entry);
127   }
128
129   $self->add_columns(qw(type)) if $self->settings->{parts_type} eq 'mixed';
130   $self->add_columns(qw(buchungsgruppen_id unit));
131   $self->add_columns(map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw (price_factor payment packing_type partsgroup));
132   $self->add_columns(qw(shop)) if $self->settings->{shoparticle_if_missing};
133
134   map { $self->add_raw_data_columns($_) if exists $self->controller->data->[0]->{raw_data}->{$_} } @{ $self->translation_columns };
135 }
136
137 sub check_duplicates {
138   my ($self, %params) = @_;
139
140   my $normalizer = sub { my $name = $_[0]; $name =~ s/[\s,\.\-]//g; return $name; };
141   my $name_maker = sub { return $normalizer->($_[0]->description) };
142
143   my %by_name;
144   if ('check_db' eq $self->controller->profile->get('duplicates')) {
145     %by_name = map { ( $name_maker->($_) => 'db' ) } @{ $self->existing_objects };
146   }
147
148   foreach my $entry (@{ $self->controller->data }) {
149     next if @{ $entry->{errors} };
150
151     my $name = $name_maker->($entry->{object});
152
153     if (!$by_name{ $name }) {
154       $by_name{ $name } = 'csv';
155
156     } else {
157       push @{ $entry->{errors} }, $by_name{ $name } eq 'db' ? $::locale->text('Duplicate in database') : $::locale->text('Duplicate in CSV file');
158     }
159   }
160 }
161
162 sub check_buchungsgruppe {
163   my ($self, $entry) = @_;
164
165   my $object = $entry->{object};
166
167   # Check Buchungsgruppe
168
169   # Store and verify default ID.
170   my $default_id = $self->settings->{default_buchungsgruppe};
171   $default_id    = undef unless $self->bg_by->{id}->{ $default_id };
172
173   # 1. Use default ID if enforced.
174   $object->buchungsgruppen_id($default_id) if $default_id && ($self->settings->{apply_buchungsgruppe} eq 'all');
175
176   # 2. Use supplied ID if valid
177   $object->buchungsgruppen_id(undef) if $object->buchungsgruppen_id && !$self->bg_by->{id}->{ $object->buchungsgruppen_id };
178
179   # 3. Look up name if supplied.
180   if (!$object->buchungsgruppen_id) {
181     my $bg = $self->bg_by->{description}->{ $entry->{raw_data}->{buchungsgruppe} };
182     $object->buchungsgruppen_id($bg->id) if $bg;
183   }
184
185   # 4. Use default ID if not valid.
186   $object->buchungsgruppen_id($default_id) if !$object->buchungsgruppen_id && $default_id && ($self->settings->{apply_buchungsgruppe} eq 'missing');
187
188   return 1 if $object->buchungsgruppen_id;
189
190   push @{ $entry->{errors} }, $::locale->text('Error: Buchungsgruppe missing or invalid');
191   return 0;
192 }
193
194 sub check_existing {
195   my ($self, $entry) = @_;
196
197   my $object = $entry->{object};
198
199   my $entry->{part} = $self->parts_by->{partnumber}->{ $object->type }->{ $object->partnumber };
200
201   if ($self->settings->{article_number_policy} eq 'update_prices') {
202     if ($entry->{part}) {
203       map { $object->$_( $entry->{part}->$_ ) } qw(sellprice listprice lastcost);
204       $entry->{priceupdate} = 1;
205     }
206
207   } else {
208     $object->partnumber('####') if $entry->{part};
209   }
210 }
211
212 sub handle_prices {
213   my ($self, $entry) = @_;
214
215   foreach my $column (qw(sellprice listprice lastcost)) {
216     next unless $self->controller->headers->{used}->{ $column };
217
218     my $adjustment = $self->settings->{sellprice_adjustment};
219     my $value      = $entry->{object}->$column;
220
221     $value = $self->settings->{sellprice_adjustment_type} eq 'percent' ? $value * (100 + $adjustment) / 100 : $value + $adjustment;
222     $entry->{object}->$column($::form->round_amount($value, $self->settings->{sellprice_places}));
223   }
224 }
225
226 sub handle_shoparticle {
227   my ($self, $entry) = @_;
228
229   $entry->{object}->shop(1) if $self->settings->{shoparticle_if_missing} && !$self->controller->headers->{used}->{shop};
230 }
231
232 sub check_type {
233   my ($self, $entry) = @_;
234
235   my $bg = $self->bg_by->{id}->{ $entry->{object}->buchungsgruppen_id };
236   die "Program logic error" if !$bg;
237
238   my $type = $self->settings->{parts_type};
239   if ($type eq 'mixed') {
240     $type = $entry->{raw_data}->{type} =~ m/^p/i ? 'part'
241           : $entry->{raw_data}->{type} =~ m/^s/i ? 'service'
242           :                                        undef;
243   }
244
245   $entry->{object}->income_accno_id(  $bg->income_accno_id_0 );
246   $entry->{object}->expense_accno_id( $bg->expense_accno_id_0 );
247
248   if ($type eq 'part') {
249     $entry->{object}->inventory_accno_id( $bg->inventory_accno_id );
250
251   } elsif ($type ne 'service') {
252     push @{ $entry->{errors} }, $::locale->text('Error: Invalid part type');
253     return 0;
254   }
255
256   return 1;
257 }
258
259 sub check_price_factor {
260   my ($self, $entry) = @_;
261
262   my $object = $entry->{object};
263
264   # Check whether or not price factor ID is valid.
265   if ($object->price_factor_id && !$self->price_factors_by->{id}->{ $object->price_factor_id }) {
266     push @{ $entry->{errors} }, $::locale->text('Error: Invalid price factor');
267     return 0;
268   }
269
270   # Map name to ID if given.
271   if (!$object->price_factor_id && $entry->{raw_data}->{price_factor}) {
272     my $pf = $self->price_factors_by->{description}->{ $entry->{raw_data}->{price_factor} };
273
274     if (!$pf) {
275       push @{ $entry->{errors} }, $::locale->text('Error: Invalid price factor');
276       return 0;
277     }
278
279     $object->price_factor_id($pf->id);
280   }
281
282   return 1;
283 }
284
285 sub check_payment {
286   my ($self, $entry) = @_;
287
288   my $object = $entry->{object};
289
290   # Check whether or not payment ID is valid.
291   if ($object->payment_id && !$self->payment_terms_by->{id}->{ $object->payment_id }) {
292     push @{ $entry->{errors} }, $::locale->text('Error: Invalid payment terms');
293     return 0;
294   }
295
296   # Map name to ID if given.
297   if (!$object->payment_id && $entry->{raw_data}->{payment}) {
298     my $terms = $self->payment_terms_by->{description}->{ $entry->{raw_data}->{payment} };
299
300     if (!$terms) {
301       push @{ $entry->{errors} }, $::locale->text('Error: Invalid payment terms');
302       return 0;
303     }
304
305     $object->payment_id($terms->id);
306   }
307
308   return 1;
309 }
310
311 sub check_packing_type {
312   my ($self, $entry) = @_;
313
314   my $object = $entry->{object};
315
316   # Check whether or not packing type ID is valid.
317   if ($object->packing_type_id && !$self->packing_types_by->{id}->{ $object->packing_type_id }) {
318     push @{ $entry->{errors} }, $::locale->text('Error: Invalid packing type');
319     return 0;
320   }
321
322   # Map name to ID if given.
323   if (!$object->packing_type_id && $entry->{raw_data}->{packing_type}) {
324     my $type = $self->packing_types_by->{description}->{ $entry->{raw_data}->{packing_type} };
325
326     if (!$type) {
327       push @{ $entry->{errors} }, $::locale->text('Error: Invalid packing type');
328       return 0;
329     }
330
331     $object->packing_type_id($type->id);
332   }
333
334   return 1;
335 }
336
337 sub check_partsgroup {
338   my ($self, $entry) = @_;
339
340   my $object = $entry->{object};
341
342   # Check whether or not part group ID is valid.
343   if ($object->partsgroup_id && !$self->partsgroups_by->{id}->{ $object->partsgroup_id }) {
344     push @{ $entry->{errors} }, $::locale->text('Error: Invalid parts group');
345     return 0;
346   }
347
348   # Map name to ID if given.
349   if (!$object->partsgroup_id && $entry->{raw_data}->{partsgroup}) {
350     my $pg = $self->partsgroups_by->{partsgroup}->{ $entry->{raw_data}->{partsgroup} };
351
352     if (!$pg) {
353       push @{ $entry->{errors} }, $::locale->text('Error: Invalid parts group');
354       return 0;
355     }
356
357     $object->partsgroup_id($pg->id);
358   }
359
360   return 1;
361 }
362
363 sub check_unit {
364   my ($self, $entry) = @_;
365
366   my $object = $entry->{object};
367
368   # Check whether or unit is valid.
369   if (!$self->units_by->{name}->{ $object->unit }) {
370     push @{ $entry->{errors} }, $::locale->text('Error: Unit missing or invalid');
371     return 0;
372   }
373
374   return 1;
375 }
376
377 sub handle_translations {
378   my ($self, $entry) = @_;
379
380   my @translations;
381   foreach my $language (@{ $self->all_languages }) {
382     my ($desc, $notes) = @{ $entry->{raw_data} }{ "description_" . $language->article_code, "notes_" . $language->article_code };
383     next unless $desc || $notes;
384
385     push @translations, SL::DB::Translation->new(language_id     => $language->id,
386                                                  translation     => $desc,
387                                                  longdescription => $notes);
388   }
389
390   $entry->{object}->translations(\@translations);
391 }
392
393 sub set_various_fields {
394   my ($self, $entry) = @_;
395
396   $entry->{object}->priceupdate(DateTime->now_local);
397 }
398
399 sub init_profile {
400   my ($self) = @_;
401
402   my $profile = $self->SUPER::init_profile;
403   delete @{$profile}{qw(type priceupdate)};
404
405   return $profile;
406 }
407
408 sub save_objects {
409   my ($self, %params) = @_;
410
411   my $with_number    = [ grep { $_->{object}->partnumber ne '####' } @{ $self->controller->data } ];
412   my $without_number = [ grep { $_->{object}->partnumber eq '####' } @{ $self->controller->data } ];
413
414   map { $_->{object}->partnumber('') } @{ $without_number };
415
416   $self->SUPER::save_objects(data => $with_number);
417   $self->SUPER::save_objects(data => $without_number);
418 }
419
420 # TODO:
421 #  CVARs ins Profil rein
422
423 1;