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