81b9fd31753555bd58757536e021b5fc0eae41aa
[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
15 use parent qw(SL::Controller::CsvImport::BaseMulti);
16
17
18 use Rose::Object::MakeMethods::Generic
19 (
20  'scalar --get_set_init' => [ qw(settings languages_by all_parts parts_by all_contacts contacts_by) ],
21 );
22
23
24 sub init_class {
25   my ($self) = @_;
26   $self->class(['SL::DB::Order', 'SL::DB::OrderItem']);
27 }
28
29
30 sub init_settings {
31   my ($self) = @_;
32
33   return { map { ( $_ => $self->controller->profile->get($_) ) } qw(order_column item_column) };
34 }
35
36
37 sub init_profile {
38   my ($self) = @_;
39
40   my $profile = $self->SUPER::init_profile;
41
42   foreach my $p (@{ $profile }) {
43     my $prof = $p->{profile};
44     if ($p->{row_ident} eq 'Order') {
45       # no need to handle
46       delete @{$prof}{qw(delivery_customer_id delivery_vendor_id proforma quotation amount netamount)};
47       # handable, but not handled by now
48     }
49     if ($p->{row_ident} eq 'OrderItem') {
50       delete @{$prof}{qw(trans_id)};
51     }
52   }
53
54   return $profile;
55 }
56
57
58 sub setup_displayable_columns {
59   my ($self) = @_;
60
61   $self->SUPER::setup_displayable_columns;
62
63   $self->add_displayable_columns('Order',
64                                  { name => 'datatype',         description => $::locale->text('Zeilenkennung')                  },
65                                  { name => 'verify_amount',    description => $::locale->text('Amount (for verification)')      },
66                                  { name => 'verify_netamount', description => $::locale->text('Net amount (for verification)')  },
67                                  { name => 'taxincluded',      description => $::locale->text('Tax Included')                   },
68                                  { name => 'customer',         description => $::locale->text('Customer (name)')                },
69                                  { name => 'customernumber',   description => $::locale->text('Customer Number')                },
70                                  { name => 'customer_id',      description => $::locale->text('Customer (database ID)')         },
71                                  { name => 'vendor',           description => $::locale->text('Vendor (name)')                  },
72                                  { name => 'vendornumber',     description => $::locale->text('Vendor Number')                  },
73                                  { name => 'vendor_id',        description => $::locale->text('Vendor (database ID)')           },
74                                  { name => 'language_id',      description => $::locale->text('Language (database ID)')         },
75                                  { name => 'language',         description => $::locale->text('Language (name)')                },
76                                  { name => 'payment_id',       description => $::locale->text('Payment terms (database ID)')    },
77                                  { name => 'payment',          description => $::locale->text('Payment terms (name)')           },
78                                  { name => 'taxzone_id',       description => $::locale->text('Steuersatz')                     },
79                                  { name => 'contact_id',       description => $::locale->text('Contact Person (database ID)')   },
80                                  { name => 'contact',          description => $::locale->text('Contact Person (name)')          },
81                                 );
82
83   $self->add_displayable_columns('OrderItem',
84                                  { name => 'parts_id',       description => $::locale->text('Part (database ID)')          },
85                                  { name => 'partnumber',     description => $::locale->text('Part Number')                 },
86                                 );
87 }
88
89
90 sub init_languages_by {
91   my ($self) = @_;
92
93   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_languages } } ) } qw(id description article_code) };
94 }
95
96 sub init_all_parts {
97   my ($self) = @_;
98
99   return SL::DB::Manager::Part->get_all;
100 }
101
102 sub init_parts_by {
103   my ($self) = @_;
104
105   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_parts } } ) } qw(id partnumber ean description) };
106 }
107
108 sub init_all_contacts {
109   my ($self) = @_;
110
111   return SL::DB::Manager::Contact->get_all;
112 }
113
114 sub init_contacts_by {
115   my ($self) = @_;
116
117   my $cby = { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_contacts } } ) } qw(cp_id cp_name) };
118
119   # by customer/vendor id  _and_  contact person id
120   $cby->{'cp_cv_id+cp_id'} = { map { ( $_->cp_cv_id . '+' . $_->cp_id => $_ ) } @{ $self->all_contacts } };
121
122   return $cby;
123 }
124
125 sub check_objects {
126   my ($self) = @_;
127
128   $self->controller->track_progress(phase => 'building data', progress => 0);
129
130   my $i;
131   my $num_data = scalar @{ $self->controller->data };
132   foreach my $entry (@{ $self->controller->data }) {
133     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
134
135     if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
136
137       my $vc_obj;
138       if (any { $entry->{raw_data}->{$_} } qw(customer customernumber customer_id)) {
139         $self->check_vc($entry, 'customer_id');
140         $vc_obj = SL::DB::Customer->new(id => $entry->{object}->customer_id)->load if $entry->{object}->customer_id;
141       } elsif (any { $entry->{raw_data}->{$_} } qw(vendor vendornumber vendor_id)) {
142         $self->check_vc($entry, 'vendor_id');
143         $vc_obj = SL::DB::Vendor->new(id => $entry->{object}->vendor_id)->load if $entry->{object}->vendor_id;
144       } else {
145         push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor missing');
146       }
147
148       $self->check_contact($entry);
149       $self->check_language($entry);
150       $self->check_payment($entry);
151
152       if ($vc_obj) {
153         # copy from customer if not given
154         foreach (qw(payment_id language_id taxzone_id)) {
155           $entry->{object}->$_($vc_obj->$_) unless $entry->{object}->$_;
156         }
157       }
158
159       # ToDo: salesman and emloyee by name
160       # salesman from customer or login if not given
161       if (!$entry->{object}->salesman) {
162         if ($vc_obj && $vc_obj->salesman_id) {
163           $entry->{object}->salesman(SL::DB::Manager::Employee->find_by(id => $vc_obj->salesman_id));
164         } else {
165           $entry->{object}->salesman(SL::DB::Manager::Employee->find_by(login => $::myconfig{login}));
166         }
167       }
168
169       # employee from login if not given
170       if (!$entry->{object}->employee_id) {
171         $entry->{object}->employee_id(SL::DB::Manager::Employee->find_by(login => $::myconfig{login})->id);
172       }
173
174     }
175   }
176
177   $self->add_info_columns($self->settings->{'order_column'},
178                           { header => $::locale->text('Customer/Vendor'), method => 'vc_name' });
179   $self->add_columns($self->settings->{'order_column'},
180                      map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw(business payment));
181
182
183   foreach my $entry (@{ $self->controller->data }) {
184     if ($entry->{raw_data}->{datatype} eq $self->settings->{'item_column'} && $entry->{object}->can('part')) {
185
186       next if !$self->check_part($entry);
187
188       my $part_obj = SL::DB::Part->new(id => $entry->{object}->parts_id)->load;
189
190       # copy from part if not given
191       $entry->{object}->description($part_obj->description) unless $entry->{object}->description;
192       $entry->{object}->longdescription($part_obj->notes)   unless $entry->{object}->longdescription;
193       $entry->{object}->unit($part_obj->unit)               unless $entry->{object}->unit;
194
195       # set to 0 if not given
196       $entry->{object}->discount(0)      unless $entry->{object}->discount;
197       $entry->{object}->ship(0)          unless $entry->{object}->ship;
198     }
199   }
200
201   $self->add_info_columns($self->settings->{'item_column'},
202                           { header => $::locale->text('Part Number'), method => 'partnumber' });
203
204   # add orderitems to order
205   my $order_entry;
206   my @orderitems;
207   foreach my $entry (@{ $self->controller->data }) {
208     # search first Order
209     if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
210
211       # new order entry: add collected orderitems to the last one
212       if (defined $order_entry) {
213         $order_entry->{object}->orderitems(@orderitems);
214         @orderitems = ();
215       }
216
217       $order_entry = $entry;
218
219     } elsif ( defined $order_entry && $entry->{raw_data}->{datatype} eq $self->settings->{'item_column'} ) {
220       # collect orderitems to add to order (if they have no errors)
221       # ( add_orderitems does not work here if we want to call
222       #   calculate_prices_and_taxes afterwards ...
223       #   so collect orderitems and add them at once)
224       if (scalar @{ $entry->{errors} } == 0) {
225         push @orderitems, $entry->{object};
226       }
227     }
228   }
229   # add last collected orderitems to last order
230   if ($order_entry) {
231     $order_entry->{object}->orderitems(@orderitems);
232   }
233
234   # calculate prices and taxes
235   foreach my $entry (@{ $self->controller->data }) {
236     next if @{ $entry->{errors} };
237
238     if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
239
240       $entry->{object}->calculate_prices_and_taxes;
241
242       $entry->{info_data}->{calc_amount}    = $entry->{object}->amount_as_number;
243       $entry->{info_data}->{calc_netamount} = $entry->{object}->netamount_as_number;
244     }
245   }
246
247   # If amounts are given, show calculated amounts as info and given amounts (verify_xxx).
248   # And throw an error if the differences are too big.
249   my $max_diff = 0.02;
250   my @to_verify = ( { column      => 'amount',
251                       raw_column  => 'verify_amount',
252                       info_header => 'Calc. Amount',
253                       info_method => 'calc_amount',
254                       err_msg     => 'Amounts differ too much',
255                     },
256                     { column      => 'netamount',
257                       raw_column  => 'verify_netamount',
258                       info_header => 'Calc. Net amount',
259                       info_method => 'calc_netamount',
260                       err_msg     => 'Net amounts differ too much',
261                     } );
262
263   foreach my $tv (@to_verify) {
264     if (exists $self->controller->data->[0]->{raw_data}->{ $tv->{raw_column} }) {
265       $self->add_raw_data_columns($self->settings->{'order_column'}, $tv->{raw_column});
266       $self->add_info_columns($self->settings->{'order_column'},
267                               { header => $::locale->text($tv->{info_header}), method => $tv->{info_method} });
268     }
269
270     # check differences
271     foreach my $entry (@{ $self->controller->data }) {
272       next if @{ $entry->{errors} };
273       if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
274         next if !$entry->{raw_data}->{ $tv->{raw_column} };
275         my $parsed_value = $::form->parse_amount(\%::myconfig, $entry->{raw_data}->{ $tv->{raw_column} });
276         if (abs($entry->{object}->${ \$tv->{column} } - $parsed_value) > $max_diff) {
277           push @{ $entry->{errors} }, $::locale->text($tv->{err_msg});
278         }
279       }
280     }
281   }
282
283   # If order has errors set error for orderitems as well
284   my $order_entry;
285   foreach my $entry (@{ $self->controller->data }) {
286     # Search first order
287     if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
288       $order_entry = $entry;
289     } elsif ( defined $order_entry
290               && $entry->{raw_data}->{datatype} eq $self->settings->{'item_column'}
291               && scalar @{ $order_entry->{errors} } > 0 ) {
292       push @{ $entry->{errors} }, $::locale->text('order not valid for this orderitem!');
293     }
294   }
295
296 }
297
298
299 sub check_language {
300   my ($self, $entry) = @_;
301
302   my $object = $entry->{object};
303
304   # Check whether or not language ID is valid.
305   if ($object->language_id && !$self->languages_by->{id}->{ $object->language_id }) {
306     push @{ $entry->{errors} }, $::locale->text('Error: Invalid language');
307     return 0;
308   }
309
310   # Map name to ID if given.
311   if (!$object->language_id && $entry->{raw_data}->{language}) {
312     my $language = $self->languages_by->{description}->{  $entry->{raw_data}->{language} }
313                 || $self->languages_by->{article_code}->{ $entry->{raw_data}->{language} };
314
315     if (!$language) {
316       push @{ $entry->{errors} }, $::locale->text('Error: Invalid language');
317       return 0;
318     }
319
320     $object->language_id($language->id);
321   }
322
323   if ($object->language_id) {
324     $entry->{info_data}->{language} = $self->languages_by->{id}->{ $object->language_id }->description;
325   }
326
327   return 1;
328 }
329
330 sub check_part {
331   my ($self, $entry) = @_;
332
333   my $object = $entry->{object};
334
335   # Check wether or non part ID is valid.
336   if ($object->parts_id && !$self->parts_by->{id}->{ $object->parts_id }) {
337     push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
338     return 0;
339   }
340
341   # Map number to ID if given.
342   if (!$object->parts_id && $entry->{raw_data}->{partnumber}) {
343     my $part = $self->parts_by->{partnumber}->{ $entry->{raw_data}->{partnumber} };
344     if (!$part) {
345       push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
346       return 0;
347     }
348
349     $object->parts_id($part->id);
350   }
351
352   if ($object->parts_id) {
353     $entry->{info_data}->{partnumber} = $self->parts_by->{id}->{ $object->parts_id }->partnumber;
354   } else {
355     push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
356     return 0;
357   }
358
359   return 1;
360 }
361
362 sub check_contact {
363   my ($self, $entry) = @_;
364
365   my $object = $entry->{object};
366
367   # Check wether or non contact ID is valid.
368   if ($object->cp_id && !$self->contacts_by->{cp_id}->{ $object->cp_id }) {
369     push @{ $entry->{errors} }, $::locale->text('Error: Invalid contact');
370     return 0;
371   }
372
373   # Map number to ID if given.
374   if (!$object->cp_id && $entry->{raw_data}->{contact}) {
375     my $cp = $self->contacts_by->{cp_name}->{ $entry->{raw_data}->{contact} };
376     if (!$cp) {
377       push @{ $entry->{errors} }, $::locale->text('Error: Invalid contact');
378       return 0;
379     }
380
381     $object->cp_id($cp->cp_id);
382   }
383
384   # Check if the contact belongs to this customer/vendor.
385   if ($object->cp_id && $object->customer_id && !$self->contacts_by->{'cp_cv_id+cp_id'}) {
386     push @{ $entry->{errors} }, $::locale->text('Error: Contact not found for this customer/vendor');
387     return 0;
388   }
389
390   if ($object->cp_id) {
391     $entry->{info_data}->{contact} = $self->contacts_by->{cp_id}->{ $object->cp_id }->cp_name;
392   }
393
394   return 1;
395 }
396
397 sub save_objects {
398   my ($self, %params) = @_;
399
400   # set order number and collect to save
401   my $objects_to_save;
402   foreach my $entry (@{ $self->controller->data }) {
403     next if @{ $entry->{errors} };
404
405     if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'} && !$entry->{object}->ordnumber) {
406       $entry->{object}->create_trans_number;
407     }
408
409     push @{ $objects_to_save }, $entry;
410   }
411
412   $self->SUPER::save_objects(data => $objects_to_save);
413 }
414
415
416 1;