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