Alle Kontakte und Lieferadressen für entspr. Kunden/Lieferanten behandeln.
[kivitendo-erp.git] / SL / Controller / CsvImport / Order.pm
1 package SL::Controller::CsvImport::Order;
2
3
4 use strict;
5
6 use List::MoreUtils qw(any);
7
8 use SL::Helper::Csv;
9 use SL::DB::Order;
10 use SL::DB::OrderItem;
11 use SL::DB::Part;
12 use SL::DB::PaymentTerm;
13 use SL::DB::Contact;
14 use SL::DB::Department;
15 use SL::DB::Project;
16 use SL::DB::Shipto;
17 use SL::DB::TaxZone;
18 use SL::TransNumber;
19
20 use parent qw(SL::Controller::CsvImport::BaseMulti);
21
22
23 use Rose::Object::MakeMethods::Generic
24 (
25  'scalar --get_set_init' => [ qw(settings languages_by all_parts parts_by all_contacts contacts_by all_departments departments_by all_projects projects_by all_ct_shiptos ct_shiptos_by all_taxzones taxzones_by) ],
26 );
27
28
29 sub init_class {
30   my ($self) = @_;
31   $self->class(['SL::DB::Order', 'SL::DB::OrderItem']);
32 }
33
34
35 sub init_settings {
36   my ($self) = @_;
37
38   return { map { ( $_ => $self->controller->profile->get($_) ) } qw(order_column item_column) };
39 }
40
41
42 sub init_profile {
43   my ($self) = @_;
44
45   my $profile = $self->SUPER::init_profile;
46
47   foreach my $p (@{ $profile }) {
48     my $prof = $p->{profile};
49     if ($p->{row_ident} eq 'Order') {
50       # no need to handle
51       delete @{$prof}{qw(delivery_customer_id delivery_vendor_id proforma quotation amount netamount)};
52       # handable, but not handled by now
53     }
54     if ($p->{row_ident} eq 'OrderItem') {
55       delete @{$prof}{qw(trans_id)};
56     }
57   }
58
59   return $profile;
60 }
61
62
63 sub setup_displayable_columns {
64   my ($self) = @_;
65
66   $self->SUPER::setup_displayable_columns;
67
68   $self->add_displayable_columns('Order',
69                                  { name => 'datatype',         description => $::locale->text('Zeilenkennung')                  },
70                                  { name => 'verify_amount',    description => $::locale->text('Amount (for verification)')      },
71                                  { name => 'verify_netamount', description => $::locale->text('Net amount (for verification)')  },
72                                  { name => 'taxincluded',      description => $::locale->text('Tax Included')                   },
73                                  { name => 'customer',         description => $::locale->text('Customer (name)')                },
74                                  { name => 'customernumber',   description => $::locale->text('Customer Number')                },
75                                  { name => 'customer_id',      description => $::locale->text('Customer (database ID)')         },
76                                  { name => 'vendor',           description => $::locale->text('Vendor (name)')                  },
77                                  { name => 'vendornumber',     description => $::locale->text('Vendor Number')                  },
78                                  { name => 'vendor_id',        description => $::locale->text('Vendor (database ID)')           },
79                                  { name => 'language_id',      description => $::locale->text('Language (database ID)')         },
80                                  { name => 'language',         description => $::locale->text('Language (name)')                },
81                                  { name => 'payment_id',       description => $::locale->text('Payment terms (database ID)')    },
82                                  { name => 'payment',          description => $::locale->text('Payment terms (name)')           },
83                                  { name => 'taxzone_id',       description => $::locale->text('Steuersatz (database ID')        },
84                                  { name => 'taxzone',          description => $::locale->text('Steuersatz (description)')       },
85                                  { name => 'cp_id',            description => $::locale->text('Contact Person (database ID)')   },
86                                  { name => 'contact',          description => $::locale->text('Contact Person (name)')          },
87                                  { name => 'department_id',    description => $::locale->text('Department (database ID)')       },
88                                  { name => 'department',       description => $::locale->text('Department (description)')       },
89                                  { name => 'globalproject_id', description => $::locale->text('Document Project (database ID)') },
90                                  { name => 'globalprojectnumber', description => $::locale->text('Document Project (number)')   },
91                                  { name => 'globalproject',    description => $::locale->text('Document Project (description)') },
92                                  { name => 'shipto_id',        description => $::locale->text('Ship to (database ID)')          },
93                                 );
94
95   $self->add_displayable_columns('OrderItem',
96                                  { name => 'parts_id',      description => $::locale->text('Part (database ID)')    },
97                                  { name => 'partnumber',    description => $::locale->text('Part Number')           },
98                                  { name => 'project_id',    description => $::locale->text('Project (database ID)') },
99                                  { name => 'projectnumber', description => $::locale->text('Project (number)')      },
100                                  { name => 'project',       description => $::locale->text('Project (description)') },
101                                 );
102 }
103
104
105 sub init_languages_by {
106   my ($self) = @_;
107
108   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_languages } } ) } qw(id description article_code) };
109 }
110
111 sub init_all_parts {
112   my ($self) = @_;
113
114   return SL::DB::Manager::Part->get_all;
115 }
116
117 sub init_parts_by {
118   my ($self) = @_;
119
120   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_parts } } ) } qw(id partnumber ean description) };
121 }
122
123 sub init_all_contacts {
124   my ($self) = @_;
125
126   return SL::DB::Manager::Contact->get_all;
127 }
128
129 sub init_contacts_by {
130   my ($self) = @_;
131
132   my $cby;
133
134   # by customer/vendor id  _and_  contact person id
135   $cby->{'cp_cv_id+cp_id'}   = { map { ( $_->cp_cv_id . '+' . $_->cp_id   => $_ ) } @{ $self->all_contacts } };
136   # by customer/vendor id  _and_  contact person name
137   $cby->{'cp_cv_id+cp_name'} = { map { ( $_->cp_cv_id . '+' . $_->cp_name => $_ ) } @{ $self->all_contacts } };
138
139
140   return $cby;
141 }
142
143 sub init_all_departments {
144   my ($self) = @_;
145
146   return SL::DB::Manager::Department->get_all;
147 }
148
149 sub init_departments_by {
150   my ($self) = @_;
151
152   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_departments } } ) } qw(id description) };
153 }
154
155 sub init_all_projects {
156   my ($self) = @_;
157
158   return SL::DB::Manager::Project->get_all;
159 }
160
161 sub init_projects_by {
162   my ($self) = @_;
163
164   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_projects } } ) } qw(id projectnumber description) };
165 }
166
167 sub init_all_ct_shiptos {
168   my ($self) = @_;
169
170   return SL::DB::Manager::Shipto->get_all(query => [module => 'CT']);
171 }
172
173 sub init_ct_shiptos_by {
174   my ($self) = @_;
175
176   my $sby;
177
178   # by trans_id  _and_  shipto_id
179   $sby->{'trans_id+shipto_id'} = { map { ( $_->trans_id . '+' . $_->shipto_id => $_ ) } @{ $self->all_ct_shiptos } };
180
181   return $sby;
182 }
183
184 sub init_all_taxzones {
185   my ($self) = @_;
186
187   return SL::DB::Manager::TaxZone->get_all;
188 }
189
190 sub init_taxzones_by {
191   my ($self) = @_;
192
193   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_taxzones } } ) } qw(id description) };
194 }
195
196 sub check_objects {
197   my ($self) = @_;
198
199   $self->controller->track_progress(phase => 'building data', progress => 0);
200
201   my $i;
202   my $num_data = scalar @{ $self->controller->data };
203   foreach my $entry (@{ $self->controller->data }) {
204     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
205
206     if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
207
208       my $vc_obj;
209       if (any { $entry->{raw_data}->{$_} } qw(customer customernumber customer_id)) {
210         $self->check_vc($entry, 'customer_id');
211         $vc_obj = SL::DB::Customer->new(id => $entry->{object}->customer_id)->load if $entry->{object}->customer_id;
212       } elsif (any { $entry->{raw_data}->{$_} } qw(vendor vendornumber vendor_id)) {
213         $self->check_vc($entry, 'vendor_id');
214         $vc_obj = SL::DB::Vendor->new(id => $entry->{object}->vendor_id)->load if $entry->{object}->vendor_id;
215       } else {
216         push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor missing');
217       }
218
219       $self->check_contact($entry);
220       $self->check_language($entry);
221       $self->check_payment($entry);
222       $self->check_department($entry);
223       $self->check_project($entry, global => 1);
224       $self->check_ct_shipto($entry);
225       $self->check_taxzone($entry);
226
227       if ($vc_obj) {
228         # copy from customer if not given
229         foreach (qw(payment_id language_id taxzone_id)) {
230           $entry->{object}->$_($vc_obj->$_) unless $entry->{object}->$_;
231         }
232       }
233
234       # ToDo: salesman and emloyee by name
235       # salesman from customer or login if not given
236       if (!$entry->{object}->salesman) {
237         if ($vc_obj && $vc_obj->salesman_id) {
238           $entry->{object}->salesman(SL::DB::Manager::Employee->find_by(id => $vc_obj->salesman_id));
239         } else {
240           $entry->{object}->salesman(SL::DB::Manager::Employee->find_by(login => $::myconfig{login}));
241         }
242       }
243
244       # employee from login if not given
245       if (!$entry->{object}->employee_id) {
246         $entry->{object}->employee_id(SL::DB::Manager::Employee->find_by(login => $::myconfig{login})->id);
247       }
248
249     }
250   }
251
252   $self->add_info_columns($self->settings->{'order_column'},
253                           { header => $::locale->text('Customer/Vendor'), method => 'vc_name' });
254   # Todo: access via ->[0] ok? Better: search first order column and use this
255   $self->add_columns($self->settings->{'order_column'},
256                      map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw(payment language department globalproject taxzone cp));
257   $self->add_columns($self->settings->{'order_column'}, 'globalproject_id') if exists $self->controller->data->[0]->{raw_data}->{globalprojectnumber};
258   $self->add_columns($self->settings->{'order_column'}, 'cp_id')            if exists $self->controller->data->[0]->{raw_data}->{contact};
259
260   foreach my $entry (@{ $self->controller->data }) {
261     if ($entry->{raw_data}->{datatype} eq $self->settings->{'item_column'} && $entry->{object}->can('part')) {
262
263       next if !$self->check_part($entry);
264
265       my $part_obj = SL::DB::Part->new(id => $entry->{object}->parts_id)->load;
266
267       # copy from part if not given
268       $entry->{object}->description($part_obj->description) unless $entry->{object}->description;
269       $entry->{object}->longdescription($part_obj->notes)   unless $entry->{object}->longdescription;
270       $entry->{object}->unit($part_obj->unit)               unless $entry->{object}->unit;
271
272       # set to 0 if not given
273       $entry->{object}->discount(0)      unless $entry->{object}->discount;
274       $entry->{object}->ship(0)          unless $entry->{object}->ship;
275
276       $self->check_project($entry, global => 0);
277     }
278   }
279
280   $self->add_info_columns($self->settings->{'item_column'},
281                           { header => $::locale->text('Part Number'), method => 'partnumber' });
282   # Todo: access via ->[1] ok? Better: search first item column and use this
283   $self->add_columns($self->settings->{'item_column'},
284                      map { "${_}_id" } grep { exists $self->controller->data->[1]->{raw_data}->{$_} } qw(project));
285   $self->add_columns($self->settings->{'item_column'}, 'project_id') if exists $self->controller->data->[1]->{raw_data}->{projectnumber};
286
287   # add orderitems to order
288   my $order_entry;
289   my @orderitems;
290   foreach my $entry (@{ $self->controller->data }) {
291     # search first Order
292     if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
293
294       # new order entry: add collected orderitems to the last one
295       if (defined $order_entry) {
296         $order_entry->{object}->orderitems(@orderitems);
297         @orderitems = ();
298       }
299
300       $order_entry = $entry;
301
302     } elsif ( defined $order_entry && $entry->{raw_data}->{datatype} eq $self->settings->{'item_column'} ) {
303       # collect orderitems to add to order (if they have no errors)
304       # ( add_orderitems does not work here if we want to call
305       #   calculate_prices_and_taxes afterwards ...
306       #   so collect orderitems and add them at once)
307       if (scalar @{ $entry->{errors} } == 0) {
308         push @orderitems, $entry->{object};
309       }
310     }
311   }
312   # add last collected orderitems to last order
313   if ($order_entry) {
314     $order_entry->{object}->orderitems(@orderitems);
315   }
316
317   # calculate prices and taxes
318   foreach my $entry (@{ $self->controller->data }) {
319     next if @{ $entry->{errors} };
320
321     if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
322
323       $entry->{object}->calculate_prices_and_taxes;
324
325       $entry->{info_data}->{calc_amount}    = $entry->{object}->amount_as_number;
326       $entry->{info_data}->{calc_netamount} = $entry->{object}->netamount_as_number;
327     }
328   }
329
330   # If amounts are given, show calculated amounts as info and given amounts (verify_xxx).
331   # And throw an error if the differences are too big.
332   my $max_diff = 0.02;
333   my @to_verify = ( { column      => 'amount',
334                       raw_column  => 'verify_amount',
335                       info_header => 'Calc. Amount',
336                       info_method => 'calc_amount',
337                       err_msg     => 'Amounts differ too much',
338                     },
339                     { column      => 'netamount',
340                       raw_column  => 'verify_netamount',
341                       info_header => 'Calc. Net amount',
342                       info_method => 'calc_netamount',
343                       err_msg     => 'Net amounts differ too much',
344                     } );
345
346   foreach my $tv (@to_verify) {
347     # Todo: access via ->[0] ok? Better: search first order column and use this
348     if (exists $self->controller->data->[0]->{raw_data}->{ $tv->{raw_column} }) {
349       $self->add_raw_data_columns($self->settings->{'order_column'}, $tv->{raw_column});
350       $self->add_info_columns($self->settings->{'order_column'},
351                               { header => $::locale->text($tv->{info_header}), method => $tv->{info_method} });
352     }
353
354     # check differences
355     foreach my $entry (@{ $self->controller->data }) {
356       next if @{ $entry->{errors} };
357       if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
358         next if !$entry->{raw_data}->{ $tv->{raw_column} };
359         my $parsed_value = $::form->parse_amount(\%::myconfig, $entry->{raw_data}->{ $tv->{raw_column} });
360         if (abs($entry->{object}->${ \$tv->{column} } - $parsed_value) > $max_diff) {
361           push @{ $entry->{errors} }, $::locale->text($tv->{err_msg});
362         }
363       }
364     }
365   }
366
367   # If order has errors set error for orderitems as well
368   my $order_entry;
369   foreach my $entry (@{ $self->controller->data }) {
370     # Search first order
371     if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
372       $order_entry = $entry;
373     } elsif ( defined $order_entry
374               && $entry->{raw_data}->{datatype} eq $self->settings->{'item_column'}
375               && scalar @{ $order_entry->{errors} } > 0 ) {
376       push @{ $entry->{errors} }, $::locale->text('order not valid for this orderitem!');
377     }
378   }
379
380 }
381
382
383 sub check_language {
384   my ($self, $entry) = @_;
385
386   my $object = $entry->{object};
387
388   # Check whether or not language ID is valid.
389   if ($object->language_id && !$self->languages_by->{id}->{ $object->language_id }) {
390     push @{ $entry->{errors} }, $::locale->text('Error: Invalid language');
391     return 0;
392   }
393
394   # Map name to ID if given.
395   if (!$object->language_id && $entry->{raw_data}->{language}) {
396     my $language = $self->languages_by->{description}->{  $entry->{raw_data}->{language} }
397                 || $self->languages_by->{article_code}->{ $entry->{raw_data}->{language} };
398
399     if (!$language) {
400       push @{ $entry->{errors} }, $::locale->text('Error: Invalid language');
401       return 0;
402     }
403
404     $object->language_id($language->id);
405   }
406
407   if ($object->language_id) {
408     $entry->{info_data}->{language} = $self->languages_by->{id}->{ $object->language_id }->description;
409   }
410
411   return 1;
412 }
413
414 sub check_part {
415   my ($self, $entry) = @_;
416
417   my $object = $entry->{object};
418
419   # Check wether or non part ID is valid.
420   if ($object->parts_id && !$self->parts_by->{id}->{ $object->parts_id }) {
421     push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
422     return 0;
423   }
424
425   # Map number to ID if given.
426   if (!$object->parts_id && $entry->{raw_data}->{partnumber}) {
427     my $part = $self->parts_by->{partnumber}->{ $entry->{raw_data}->{partnumber} };
428     if (!$part) {
429       push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
430       return 0;
431     }
432
433     $object->parts_id($part->id);
434   }
435
436   if ($object->parts_id) {
437     $entry->{info_data}->{partnumber} = $self->parts_by->{id}->{ $object->parts_id }->partnumber;
438   } else {
439     push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
440     return 0;
441   }
442
443   return 1;
444 }
445
446 sub check_contact {
447   my ($self, $entry) = @_;
448
449   my $object = $entry->{object};
450
451   my $cp_cv_id = $object->customer_id || $object->vendor_id;
452   return 0 unless $cp_cv_id;
453
454   # Check wether or not contact ID is valid.
455   if ($object->cp_id && !$self->contacts_by->{'cp_cv_id+cp_id'}->{ $cp_cv_id . '+' . $object->cp_id }) {
456     push @{ $entry->{errors} }, $::locale->text('Error: Invalid contact');
457     return 0;
458   }
459
460   # Map name to ID if given.
461   if (!$object->cp_id && $entry->{raw_data}->{contact}) {
462     my $cp = $self->contacts_by->{'cp_cv_id+cp_name'}->{ $cp_cv_id . '+' . $entry->{raw_data}->{contact} };
463     if (!$cp) {
464       push @{ $entry->{errors} }, $::locale->text('Error: Invalid contact');
465       return 0;
466     }
467
468     $object->cp_id($cp->cp_id);
469   }
470
471   if ($object->cp_id) {
472     $entry->{info_data}->{contact} = $self->contacts_by->{'cp_cv_id+cp_id'}->{ $cp_cv_id . '+' . $object->cp_id }->cp_name;
473   }
474
475   return 1;
476 }
477
478 sub check_department {
479   my ($self, $entry) = @_;
480
481   my $object = $entry->{object};
482
483   # Check wether or not department ID is valid.
484   if ($object->department_id && !$self->departments_by->{id}->{ $object->department_id }) {
485     push @{ $entry->{errors} }, $::locale->text('Error: Invalid department');
486     return 0;
487   }
488
489   # Map description to ID if given.
490   if (!$object->department_id && $entry->{raw_data}->{department}) {
491     my $dep = $self->departments_by->{description}->{ $entry->{raw_data}->{department} };
492     if (!$dep) {
493       push @{ $entry->{errors} }, $::locale->text('Error: Invalid department');
494       return 0;
495     }
496
497     $object->department_id($dep->id);
498   }
499
500   return 1;
501 }
502
503 sub check_project {
504   my ($self, $entry, %params) = @_;
505
506   my $id_column          = ($params{global} ? 'global' : '') . 'project_id';
507   my $number_column      = ($params{global} ? 'global' : '') . 'projectnumber';
508   my $description_column = ($params{global} ? 'global' : '') . 'project';
509
510   my $object = $entry->{object};
511
512   # Check wether or not projetc ID is valid.
513   if ($object->$id_column && !$self->projects_by->{id}->{ $object->$id_column }) {
514     push @{ $entry->{errors} }, $::locale->text('Error: Invalid project');
515     return 0;
516   }
517
518   # Map number to ID if given.
519   if (!$object->$id_column && $entry->{raw_data}->{$number_column}) {
520     my $proj = $self->projects_by->{projectnumber}->{ $entry->{raw_data}->{$number_column} };
521     if (!$proj) {
522       push @{ $entry->{errors} }, $::locale->text('Error: Invalid project');
523       return 0;
524     }
525
526     $object->$id_column($proj->id);
527   }
528
529   # Map description to ID if given.
530   if (!$object->$id_column && $entry->{raw_data}->{$description_column}) {
531     my $proj = $self->projects_by->{description}->{ $entry->{raw_data}->{$description_column} };
532     if (!$proj) {
533       push @{ $entry->{errors} }, $::locale->text('Error: Invalid project');
534       return 0;
535     }
536
537     $object->$id_column($proj->id);
538   }
539
540   return 1;
541 }
542
543 sub check_ct_shipto {
544   my ($self, $entry) = @_;
545
546   my $object = $entry->{object};
547
548   my $trans_id = $object->customer_id || $object->vendor_id;
549   return 0 unless $trans_id;
550
551   # Check wether or not shipto ID is valid.
552   if ($object->shipto_id && !$self->ct_shiptos_by->{'trans_id+shipto_id'}->{ $trans_id . '+' . $object->shipto_id }) {
553     push @{ $entry->{errors} }, $::locale->text('Error: Invalid shipto');
554     return 0;
555   }
556
557   return 1;
558 }
559
560 sub check_taxzone {
561   my ($self, $entry) = @_;
562
563   my $object = $entry->{object};
564
565   # Check wether or not taxzone ID is valid.
566   if ($object->taxzone_id && !$self->taxzones_by->{id}->{ $object->taxzone_id }) {
567     push @{ $entry->{errors} }, $::locale->text('Error: Invalid taxzone');
568     return 0;
569   }
570
571   # Map description to ID if given.
572   if (!$object->taxzone_id && $entry->{raw_data}->{taxzone}) {
573     my $taxzone = $self->taxzones_by->{description}->{ $entry->{raw_data}->{taxzone} };
574     if (!$taxzone) {
575       push @{ $entry->{errors} }, $::locale->text('Error: Invalid taxzone');
576       return 0;
577     }
578
579     $object->taxzone_id($taxzone->id);
580   }
581
582   return 1;
583 }
584
585
586 sub save_objects {
587   my ($self, %params) = @_;
588
589   # set order number and collect to save
590   my $objects_to_save;
591   foreach my $entry (@{ $self->controller->data }) {
592     next if @{ $entry->{errors} };
593
594     if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'} && !$entry->{object}->ordnumber) {
595       my $number = SL::TransNumber->new(type        => 'sales_order',
596                                         save        => 1);
597       $entry->{object}->ordnumber($number->create_unique());
598     }
599
600     push @{ $objects_to_save }, $entry;
601   }
602
603   $self->SUPER::save_objects(data => $objects_to_save);
604 }
605
606
607 1;