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