]> wagnertech.de Git - mfinanz.git/blob - SL/Controller/Reclamation.pm
date error in mapping
[mfinanz.git] / SL / Controller / Reclamation.pm
1 package SL::Controller::Reclamation;
2
3 use strict;
4 use parent qw(SL::Controller::Base);
5
6 use SL::Helper::Flash qw(flash_later);
7 use SL::HTML::Util;
8 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
9 use SL::Presenter::ReclamationFilter qw(filter);
10 use SL::Locale::String qw(t8);
11 use SL::SessionFile::Random;
12 use SL::PriceSource;
13 use SL::ReportGenerator;
14 use SL::Controller::Helper::ReportGenerator;
15 use SL::Webdav;
16 use SL::File;
17 use SL::MIME;
18 use SL::Util qw(trim);
19 use SL::YAML;
20 use SL::DB::History;
21 use SL::DB::Reclamation;
22 use SL::DB::ReclamationItem;
23 use SL::DB::Default;
24 use SL::DB::Printer;
25 use SL::DB::Language;
26 use SL::DB::RecordLink;
27 use SL::DB::Shipto;
28 use SL::DB::Translation;
29 use SL::DB::ValidityToken;
30 use SL::DB::EmailJournal;
31 use SL::DB::Helper::RecordLink qw(set_record_link_conversions RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
32 use SL::DB::Helper::TypeDataProxy;
33 use SL::DB::Helper::Record qw(get_object_name_from_type get_class_from_type);
34
35 use SL::Helper::CreatePDF qw(:all);
36 use SL::Helper::PrintOptions;
37 use SL::Helper::UserPreferences::PositionsScrollbar;
38 use SL::Helper::UserPreferences::UpdatePositions;
39
40 use SL::Controller::Helper::GetModels;
41
42 use SL::DB::Order;
43 use SL::DB::DeliveryOrder;
44 use SL::DB::Invoice;
45 use SL::Model::Record;
46 use SL::DB::Order::TypeData qw(:types);
47 use SL::DB::DeliveryOrder::TypeData qw(:types);
48 use SL::DB::Reclamation::TypeData qw(:types);
49
50 use List::Util qw(first sum0);
51 use List::UtilsBy qw(sort_by uniq_by);
52 use List::MoreUtils qw(any none pairwise first_index);
53 use English qw(-no_match_vars);
54 use File::Spec;
55 use Cwd;
56 use Sort::Naturally;
57
58 use Rose::Object::MakeMethods::Generic
59 (
60  scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
61  'scalar --get_set_init' => [qw(
62     all_price_factors cv models p part_picker_classification_ids reclamation
63     search_cvpartnumber show_update_button type valid_types type_data
64  )],
65 );
66
67
68 # safety
69 __PACKAGE__->run_before('check_auth');
70
71 __PACKAGE__->run_before('recalc',
72                         only => [qw(
73                           save save_as_new print preview_pdf send_email
74                           save_and_show_email_dialog
75                           save_and_new_record
76                           save_and_credit_note
77                        )]);
78
79 __PACKAGE__->run_before('get_unalterable_data',
80                         only => [qw(
81                           save save_as_new print preview_pdf send_email
82                           save_and_show_email_dialog
83                           save_and_new_record
84                           save_and_credit_note
85                         )]);
86
87 #
88 # actions
89 #
90
91 # add a new reclamation
92 sub action_add {
93   my ($self) = @_;
94
95   $self->pre_render();
96
97   if (!$::form->{form_validity_token}) {
98     $::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_RECLAMATION_SAVE())->token;
99   }
100
101   $self->render(
102     'reclamation/form',
103     title => $self->type_data->text('add'),
104     %{$self->{template_args}},
105   );
106 }
107
108 sub action_add_from_record {
109   my ($self) = @_;
110   my $from_type = $::form->{from_type};
111   my $from_id   = $::form->{from_id};
112
113   die "No 'from_type' was given." unless ($from_type);
114   die "No 'from_id' was given."   unless ($from_id);
115
116   my %flags = ();
117   if (defined($::form->{from_item_ids})) {
118     my %use_item = map { $_ => 1 } @{$::form->{from_item_ids}};
119     $flags{item_filter} = sub {
120       my ($item) = @_;
121       return %use_item{$item->{RECORD_ITEM_ID()}};
122     }
123   }
124
125   my $record = SL::Model::Record->get_record($from_type, $from_id);
126   my $reclamation = SL::Model::Record->new_from_workflow($record, $self->type, %flags);
127   $self->reclamation($reclamation);
128   $self->reinit_after_new_reclamation();
129
130   if ($record->type eq SALES_RECLAMATION_TYPE()) { # check for direct delivery
131     # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
132     if ($::form->{use_shipto}) {
133       my $custom_shipto = $record->shipto->clone('SL::DB::Reclamation');
134       $self->reclamation->custom_shipto($custom_shipto) if $custom_shipto;
135     } else {
136       # remove any custom shipto if not wanted
137       $self->reclamation->custom_shipto(SL::DB::Shipto->new(module => 'RC', custom_variables => []));
138     }
139   }
140
141   $self->action_add;
142 }
143
144 sub action_add_from_email_journal {
145   my ($self) = @_;
146   die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
147
148   $self->action_add();
149 }
150
151 sub action_edit_with_email_journal_workflow {
152   my ($self) = @_;
153   die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
154   $::form->{workflow_email_journal_id}    = delete $::form->{email_journal_id};
155   $::form->{workflow_email_attachment_id} = delete $::form->{email_attachment_id};
156   $::form->{workflow_email_callback}      = delete $::form->{callback};
157
158   $self->action_edit();
159 }
160
161 # edit an existing reclamation
162 sub action_edit {
163   my ($self) = @_;
164   die "No 'id' was given." unless $::form->{id};
165
166   $self->load_reclamation();
167
168   $self->pre_render();
169   $self->render(
170     'reclamation/form',
171     title => $self->type_data->text('edit'),
172     %{$self->{template_args}},
173   );
174 }
175
176 # delete the reclamation
177 sub action_delete {
178   my ($self) = @_;
179
180
181   SL::Model::Record->delete($self->reclamation);
182   flash_later('info', $self->type_data->text('delete'));
183
184   my @redirect_params = (
185     action => 'add',
186     type   => $self->type,
187   );
188
189   $self->redirect_to(@redirect_params);
190 }
191
192 # save the reclamation
193 sub action_save {
194   my ($self) = @_;
195
196   $self->save();
197
198   flash_later('info', t8('The reclamation has been saved'));
199
200   my @redirect_params;
201   if ($::form->{back_to_caller}) {
202     @redirect_params = $::form->{callback} ? ($::form->{callback})
203                                            : (controller => 'LoginScreen', action => 'user_login');
204   } else {
205     @redirect_params = (
206       action => 'edit',
207       type   => $self->type,
208       id     => $self->reclamation->id,
209       callback => $::form->{callback},
210     );
211   }
212
213   $self->redirect_to(@redirect_params);
214 }
215
216 sub action_list {
217   my ($self) = @_;
218
219   $self->_setup_search_action_bar;
220   $self->prepare_report;
221   $self->report_generator_list_objects(
222     report => $self->{report},
223     objects => $self->models->get,
224     options => {
225       action_bar_additional_submit_values => {
226         type => $self->type,
227       },
228     },
229   );
230 }
231
232 # save the reclamation as new document an open it for edit
233 sub action_save_as_new {
234   my ($self) = @_;
235
236   my $reclamation = $self->reclamation;
237
238   if (!$reclamation->id) {
239     $self->js->flash('error', t8('This object has not been saved yet.'));
240     return $self->js->render();
241   }
242
243   my $saved_reclamation = SL::DB::Reclamation->new(id => $reclamation->id)->load;
244
245   # Create new record from current one
246   my $new_reclamation = SL::Model::Record->clone_for_save_as_new($saved_reclamation, $reclamation);
247   $self->reclamation($new_reclamation);
248
249   if (!$::form->{form_validity_token}) {
250     $::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_RECLAMATION_SAVE())->token;
251   }
252
253   # save
254   $self->action_save();
255 }
256
257 # print the reclamation
258 #
259 # This is called if "print" is pressed in the print dialog.
260 # If PDF creation was requested and succeeded, the pdf is offered for download
261 # via send_file (which uses ajax in this case).
262 sub action_print {
263   my ($self) = @_;
264
265   $self->save();
266
267   $self->js_reset_reclamation_and_item_ids_after_save;
268
269   my $format      = $::form->{print_options}->{format};
270   my $media       = $::form->{print_options}->{media};
271   my $formname    = $::form->{print_options}->{formname};
272   my $copies      = $::form->{print_options}->{copies};
273   my $groupitems  = $::form->{print_options}->{groupitems};
274   my $printer_id  = $::form->{print_options}->{printer_id};
275
276   # only pdf and opendocument by now
277   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
278     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
279   }
280
281   # only screen or printer by now
282   if (none { $media eq $_ } qw(screen printer)) {
283     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
284   }
285
286   # create a form for generate_attachment_filename
287   my $form   = Form->new;
288   $form->{record_number} = $self->reclamation->record_number;
289   $form->{type}          = $self->type;
290   $form->{format}        = $format;
291   $form->{formname}      = $formname;
292   $form->{language}      = '_' . $self->reclamation->language->template_code if $self->reclamation->language;
293   my $pdf_filename       = $form->generate_attachment_filename();
294
295   my $pdf;
296   my @errors = generate_pdf($self->reclamation, \$pdf, {
297                               format     => $format,
298                               formname   => $formname,
299                               language   => $self->reclamation->language,
300                               printer_id => $printer_id,
301                               groupitems => $groupitems,
302                             });
303   if (scalar @errors) {
304     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
305   }
306
307   if ($media eq 'screen') { # screen/download
308     $self->js->flash('info', t8('The PDF has been created'));
309     $self->send_file(
310       \$pdf,
311       type         => SL::MIME->mime_type_from_ext($pdf_filename),
312       name         => $pdf_filename,
313       js_no_render => 1,
314     );
315   } elsif ($media eq 'printer') { # printer
316     my $printer_id = $::form->{print_options}->{printer_id};
317     SL::DB::Printer->new(id => $printer_id)->load->print_document(
318       copies  => $copies,
319       content => $pdf,
320     );
321     $self->js->flash('info', t8('The PDF has been printed'));
322   }
323
324   my @warnings = store_pdf_to_webdav_and_filemanagement($self->reclamation, $pdf, $pdf_filename);
325   if (scalar @warnings) {
326     $self->js->flash('warning', $_) for @warnings;
327   }
328
329   $self->save_history('PRINTED');
330
331   $self->js
332     ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
333     ->render;
334 }
335
336 sub action_preview_pdf {
337   my ($self) = @_;
338
339   $self->save();
340
341   $self->js_reset_reclamation_and_item_ids_after_save;
342
343   my $format      = 'pdf';
344   my $media       = 'screen';
345   my $formname    = $self->type;
346
347   # only pdf
348   # create a form for generate_attachment_filename
349   my $form   = Form->new;
350   $form->{record_number} = $self->reclamation->record_number;
351   $form->{type}          = $self->type;
352   $form->{format}        = $format;
353   $form->{formname}      = $formname;
354   $form->{language}      = '_' . $self->reclamation->language->template_code if $self->reclamation->language;
355   my $pdf_filename       = $form->generate_attachment_filename();
356
357   my $pdf;
358   my @errors = generate_pdf($self->reclamation, \$pdf, {
359                              format     => $format,
360                              formname   => $formname,
361                              language   => $self->reclamation->language,
362                            });
363   if (scalar @errors) {
364     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
365   }
366   $self->save_history('PREVIEWED');
367   $self->js->flash('info', t8('The PDF has been previewed'));
368   # screen/download
369   $self->send_file(
370     \$pdf,
371     type         => SL::MIME->mime_type_from_ext($pdf_filename),
372     name         => $pdf_filename,
373     js_no_render => 0,
374   );
375 }
376
377 # open the email dialog
378 sub action_save_and_show_email_dialog {
379   my ($self) = @_;
380
381   $self->save();
382   $self->js_reset_reclamation_and_item_ids_after_save;
383
384   my $cv = $self->reclamation->customervendor
385     or return $self->js->flash('error',
386       $self->type_data->properties('is_customer') ?
387           t8('Cannot send E-mail without customer given')
388         : t8('Cannot send E-mail without vendor given')
389     )->render($self);
390
391   my $form = Form->new;
392   $form->{record_number}    = $self->reclamation->record_number;
393   $form->{cv_record_number} = $self->reclamation->cv_record_number;
394   $form->{formname}         = $self->type;
395   $form->{type}             = $self->type;
396   $form->{language}         = '_' . $self->reclamation->language->template_code if $self->reclamation->language;
397   $form->{language_id}      = $self->reclamation->language->id                  if $self->reclamation->language;
398   $form->{format}           = 'pdf';
399   $form->{cp_id}            = $self->reclamation->contact->cp_id if $self->reclamation->contact;
400
401   my $email_form;
402   $email_form->{to} =
403        ($self->reclamation->contact ? $self->reclamation->contact->cp_email : undef)
404     ||  $cv->email;
405   $email_form->{cc}  = $cv->cc;
406   $email_form->{bcc} = join ', ', grep $_, $cv->bcc;
407   # TODO: get addresses from shipto, if any
408   $email_form->{subject}             = $form->generate_email_subject();
409   $email_form->{attachment_filename} = $form->generate_attachment_filename();
410   $email_form->{message}             = $form->generate_email_body();
411   $email_form->{js_send_function}    = 'kivi.Reclamation.send_email()';
412
413   my %files = $self->get_files_for_email_dialog();
414
415   my @employees_with_email = grep {
416     my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
417     $user && !!trim($user->get_config_value('email'));
418   } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
419
420   my $dialog_html = $self->render(
421     'common/_send_email_dialog', { output => 0 },
422     email_form  => $email_form,
423     show_bcc    => $::auth->assert('email_bcc', 'may fail'),
424     FILES       => \%files,
425     is_customer => $self->type_data->properties('is_customer'),
426     ALL_EMPLOYEES => \@employees_with_email,
427     ALL_PARTNER_EMAIL_ADDRESSES => $cv->get_all_email_addresses(),
428   );
429
430   $self->js
431     ->run('kivi.Reclamation.show_email_dialog', $dialog_html)
432     ->reinit_widgets
433     ->render($self);
434 }
435
436 # send email
437 sub action_send_email {
438   my ($self) = @_;
439
440   eval {
441     $self->save();
442     1;
443   } or do {
444     $self->js->run('kivi.Reclamation.close_email_dialog');
445     die $EVAL_ERROR;
446   };
447
448   my @redirect_params = (
449     action => 'edit',
450     type   => $self->type,
451     id     => $self->reclamation->id,
452   );
453
454   # Set the error handler to reload the document and display errors later,
455   # because the document is already saved and saving can have some side effects
456   # such as generating a document number, project number or record links,
457   # which will be up to date when the document is reloaded.
458   # Hint: Do not use "die" here and try to catch exceptions in subroutine
459   # calls. You should use "$::form->error" which respects the error handler.
460   local $::form->{__ERROR_HANDLER} = sub {
461       flash_later('error', $_[0]);
462       $self->redirect_to(@redirect_params);
463       $::dispatcher->end_request;
464   };
465
466   # move $::form->{email_form} to $::form
467   my $email_form  = delete $::form->{email_form};
468
469   if ($email_form->{additional_to}) {
470     $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
471     delete $email_form->{additional_to};
472   }
473
474   my %field_names = (to => 'email');
475   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
476
477   # for Form::cleanup which may be called in Form::send_email
478   $::form->{cwd}    = getcwd();
479   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
480
481   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{$::form->{print_options}};
482   $::form->{media}  = 'email';
483
484   $::form->{attachment_policy} //= '';
485
486   # Is an old file version available?
487   my $attfile;
488   if ($::form->{attachment_policy} eq 'old_file') {
489     $attfile = SL::File->get_all(
490       object_id     => $self->reclamaiton->id,
491       object_type   => $self->type,
492       print_variant => $::form->{formname},
493     );
494   }
495
496   if (   $::form->{attachment_policy} ne 'no_file'
497     && !($::form->{attachment_policy} eq 'old_file' && $attfile)
498   ) {
499     my $pdf;
500     my @errors = generate_pdf(
501       $self->reclamation, \$pdf, {
502         media      => $::form->{media},
503         format     => $::form->{print_options}->{format},
504         formname   => $::form->{print_options}->{formname},
505         language   => $self->reclamation->language,
506         printer_id => $::form->{print_options}->{printer_id},
507         groupitems => $::form->{print_options}->{groupitems},
508       });
509     if (scalar @errors) {
510       $::form->error(t8('Generating the document failed: #1', $errors[0]));
511     }
512
513     my @warnings = store_pdf_to_webdav_and_filemanagement(
514       $self->reclamation, $pdf, $::form->{attachment_filename}
515     );
516     if (scalar @warnings) {
517       flash_later('warning', $_) for @warnings;
518     }
519
520     my $sfile = SL::SessionFile::Random->new(mode => "w");
521     $sfile->fh->print($pdf);
522     $sfile->fh->close;
523
524     $::form->{tmpfile} = $sfile->file_name;
525     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be
526                                            # called in Form::send_email
527   }
528
529   $::form->{id} = $self->reclamation->id; # this is used in SL::Mailer to
530                                           # create a linked record to the mail
531   $::form->send_email(\%::myconfig, 'pdf');
532
533   flash_later('info', t8('The email has been sent.'));
534   $self->save_history('MAILED');
535
536   # internal notes unless no email journal
537   unless ($::instance_conf->get_email_journal) {
538     my $intnotes = $self->reclamation->intnotes;
539     $intnotes   .= "\n\n" if $self->reclamation->intnotes;
540     $intnotes   .= t8('[email]')                                       . "\n";
541     $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(
542                                                DateTime->now_local,
543                                                precision => 'seconds') . "\n";
544     $intnotes   .= t8('To (email)') . ": " . $::form->{email}          . "\n";
545     $intnotes   .= t8('Cc')         . ": " . $::form->{cc}             . "\n"    if $::form->{cc};
546     $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}            . "\n"    if $::form->{bcc};
547     $intnotes   .= t8('Subject')    . ": " . $::form->{subject}        . "\n\n";
548     $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
549
550     $self->reclamation->update_attributes(intnotes => $intnotes);
551   }
552
553   $self->redirect_to(@redirect_params);
554 }
555
556 sub action_save_and_new_record {
557   my ($self) = @_;
558   my $to_type = $::form->{to_type};
559   my $to_controller = get_object_name_from_type($to_type);
560
561   $self->save();
562   flash_later('info', t8('The reclamation has been saved'));
563
564   my %additional_params = ();
565   if ($::form->{only_selected_item_positions}) { # ids can be unset before save
566     my $item_positions = $::form->{selected_item_positions} || [];
567     my @from_item_ids = map { $self->reclamation->items_sorted->[$_]->id } @$item_positions;
568     $additional_params{from_item_ids} = \@from_item_ids;
569   }
570
571   $self->redirect_to(
572     controller => $to_controller,
573     action     => 'add_from_record',
574     type       => $to_type,
575     from_id    => $self->reclamation->id,
576     from_type  => $self->reclamation->type,
577     email_journal_id    => $::form->{workflow_email_journal_id},
578     email_attachment_id => $::form->{workflow_email_attachment_id},
579     callback            => $::form->{workflow_email_callback},
580     %additional_params,
581   );
582 }
583
584 # save the reclamation and redirect to the frontend subroutine for a new
585 # credit_note
586 sub action_save_and_credit_note {
587   my ($self) = @_;
588
589   # always save
590   $self->save();
591
592   if (!$self->reclamation->is_sales) {
593     $self->js->flash('error', t8("Can't convert Purchase Reclamation to Credit Note"));
594     return $self->js->render();
595   }
596
597   flash_later('info', t8('The reclamation has been saved'));
598   $self->redirect_to(
599     controller => 'is.pl',
600     action     => 'credit_note_from_reclamation',
601     from_id    => $self->reclamation->id,
602     email_journal_id    => $::form->{workflow_email_journal_id},
603     email_attachment_id => $::form->{workflow_email_attachment_id},
604     callback            => $::form->{workflow_email_callback},
605   );
606 }
607
608 # set form elements in respect to a changed customer or vendor
609 #
610 # This action is called on an change of the customer/vendor picker.
611 sub action_customer_vendor_changed {
612   my ($self) = @_;
613
614   $self->reclamation(
615     SL::Model::Record->update_after_customer_vendor_change($self->reclamation));
616
617   $self->recalc();
618
619   if ( $self->reclamation->customervendor->contacts
620        && scalar @{ $self->reclamation->customervendor->contacts } > 0) {
621     $self->js->show('#cp_row');
622   } else {
623     $self->js->hide('#cp_row');
624   }
625
626   if ($self->reclamation->customervendor->shipto
627       && scalar @{ $self->reclamation->customervendor->shipto } > 0) {
628     $self->js->show('#shipto_selection');
629   } else {
630     $self->js->hide('#shipto_selection');
631   }
632
633   $self->js->val( '#reclamation_salesman_id', $self->reclamation->salesman_id) if $self->reclamation->is_sales;
634
635   $self->js
636     ->replaceWith('#reclamation_cp_id',            $self->build_contact_select)
637     ->replaceWith('#reclamation_shipto_id',        $self->build_shipto_select)
638     ->replaceWith('#shipto_inputs  ',              $self->build_shipto_inputs)
639     ->replaceWith('#business_info_row',            $self->build_business_info_row)
640     ->val(        '#reclamation_taxzone_id',       $self->reclamation->taxzone_id)
641     ->val(        '#reclamation_taxincluded',      $self->reclamation->taxincluded)
642     ->val(        '#reclamation_currency_id',      $self->reclamation->currency_id)
643     ->val(        '#reclamation_payment_id',       $self->reclamation->payment_id)
644     ->val(        '#reclamation_delivery_term_id', $self->reclamation->delivery_term_id)
645     ->val(        '#reclamation_intnotes',         $self->reclamation->intnotes)
646     ->val(        '#reclamation_language_id',      $self->reclamation->customervendor->language_id)
647     ->focus(      '#reclamation_' . $self->cv . '_id')
648     ->run('kivi.Reclamation.update_exchangerate');
649
650   $self->js_redisplay_amounts_and_taxes;
651   $self->js_redisplay_cvpartnumbers;
652   $self->js->render();
653 }
654
655 # called if a unit in an existing item row is changed
656 sub action_unit_changed {
657   my ($self) = @_;
658
659   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{reclamation_item_ids} };
660   my $item = $self->reclamation->items_sorted->[$idx];
661
662   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
663   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
664
665   $self->recalc();
666
667   $self->js
668     ->run('kivi.Reclamation.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
669   $self->js_redisplay_line_values;
670   $self->js_redisplay_amounts_and_taxes;
671   $self->js->render();
672 }
673
674 # add an item row for a new item entered in the input row
675 sub action_add_item {
676   my ($self) = @_;
677
678   delete $::form->{add_item}->{create_part_type};
679
680   my $form_attr = $::form->{add_item};
681
682   unless ($form_attr->{parts_id}) {
683     $self->js->flash('error', t8("No part was selected."));
684     return $self->js->render();
685   }
686
687
688   my $item = new_item($self->reclamation, $form_attr);
689
690   $self->reclamation->add_items($item);
691
692   $self->recalc();
693
694   $self->get_item_cvpartnumber($item);
695
696   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
697   my $row_as_html = $self->p->render('reclamation/tabs/basic_data/_row',
698                                      ITEM => $item,
699                                      ID   => $item_id,
700                                      SELF => $self,
701   );
702
703   if ($::form->{insert_before_item_id}) {
704     $self->js
705       ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
706   } else {
707     $self->js
708       ->append('#row_table_id', $row_as_html);
709   }
710
711   if ( $item->part->is_assortment ) {
712     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
713     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
714       my $attr = { parts_id => $assortment_item->parts_id,
715                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
716                    unit     => $assortment_item->unit,
717                    description => $assortment_item->part->description,
718                  };
719       my $item = new_item($self->reclamation, $attr);
720
721       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
722       $item->discount(1) unless $assortment_item->charge;
723
724       $self->reclamation->add_items( $item );
725       $self->recalc();
726       $self->get_item_cvpartnumber($item);
727       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
728       my $row_as_html = $self->p->render('reclamation/tabs/basic_data/_row',
729                                          ITEM => $item,
730                                          ID   => $item_id,
731                                          SELF => $self,
732       );
733       if ($::form->{insert_before_item_id}) {
734         $self->js
735           ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
736       } else {
737         $self->js
738           ->append('#row_table_id', $row_as_html);
739       }
740     };
741   };
742
743   $self->js
744     ->val('.add_item_input', '')
745     ->run('kivi.Reclamation.init_row_handlers')
746     ->run('kivi.Reclamation.renumber_positions')
747     ->focus('#add_item_parts_id_name');
748
749   $self->js->run('kivi.Reclamation.row_table_scroll_down') if !$::form->{insert_before_item_id};
750
751   $self->js_redisplay_amounts_and_taxes;
752   $self->js->render();
753 }
754
755 # add item rows for multiple items at once
756 sub action_add_multi_items {
757   my ($self) = @_;
758
759   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
760   unless (scalar(@form_attr)) {
761     $self->js->flash('error', t8("No part was selected."));
762     return $self->js->render();
763   }
764
765   my @items;
766   foreach my $attr (@form_attr) {
767     my $item = new_item($self->reclamation, $attr);
768     push @items, $item;
769     if ( $item->part->is_assortment ) {
770       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
771         my $attr = { parts_id => $assortment_item->parts_id,
772                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
773                      unit     => $assortment_item->unit,
774                      description => $assortment_item->part->description,
775                    };
776         my $item = new_item($self->reclamation, $attr);
777
778         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
779         $item->discount(1) unless $assortment_item->charge;
780         push @items, $item;
781       }
782     }
783   }
784   $self->reclamation->add_items(@items);
785
786   $self->recalc();
787
788   foreach my $item (@items) {
789     $self->get_item_cvpartnumber($item);
790     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
791     my $row_as_html = $self->p->render('reclamation/tabs/basic_data/_row',
792                                        ITEM => $item,
793                                        ID   => $item_id,
794                                        SELF => $self,
795     );
796
797     if ($::form->{insert_before_item_id}) {
798       $self->js
799         ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
800     } else {
801       $self->js
802         ->append('#row_table_id', $row_as_html);
803     }
804   }
805
806   $self->js
807     ->run('kivi.Part.close_picker_dialogs')
808     ->run('kivi.Reclamation.init_row_handlers')
809     ->run('kivi.Reclamation.renumber_positions')
810     ->focus('#add_item_parts_id_name');
811
812   $self->js->run('kivi.Reclamation.row_table_scroll_down') if !$::form->{insert_before_item_id};
813
814   $self->js_redisplay_amounts_and_taxes;
815   $self->js->render();
816 }
817
818 # recalculate all linetotals, amounts and taxes and redisplay them
819 sub action_recalc_amounts_and_taxes {
820   my ($self) = @_;
821
822   $self->recalc();
823
824   $self->js_redisplay_line_values;
825   $self->js_redisplay_amounts_and_taxes;
826   $self->js->render();
827 }
828
829 sub action_update_exchangerate {
830   my ($self) = @_;
831
832   my $data = {
833     is_standard   => $self->reclamation->currency_id == $::instance_conf->get_currency_id,
834     currency_name => $self->reclamation->currency->name,
835     exchangerate  => $self->reclamation->daily_exchangerate_as_null_number,
836   };
837
838   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
839 }
840
841 # redisplay item rows if they are sorted by an attribute
842 sub action_reorder_items {
843   my ($self) = @_;
844
845   my %sort_keys = (
846     partnumber   => sub { $_[0]->part->partnumber },
847     description  => sub { $_[0]->description },
848     reason       => sub { $_[0]->reason eq undef ? "" : $_[0]->reason->name },
849     reason_description_ext => sub { $_[0]->reason_description_ext },
850     reason_description_int => sub { $_[0]->reason_description_int },
851     qty          => sub { $_[0]->qty },
852     sellprice    => sub { $_[0]->sellprice },
853     discount     => sub { $_[0]->discount },
854     cvpartnumber => sub { $_[0]->{cvpartnumber} },
855   );
856
857   $self->get_item_cvpartnumber($_) for @{$self->reclamation->items_sorted};
858
859   my $method = $sort_keys{$::form->{order_by}};
860   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->reclamation->items_sorted };
861   if ($::form->{sort_dir}) {
862     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
863       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
864     } else {
865       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
866     }
867   } else {
868     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
869       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
870     } else {
871       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
872     }
873   }
874   $self->js
875     ->run('kivi.Reclamation.redisplay_items', \@to_sort)
876     ->render;
877 }
878
879 # show the popup to choose a price/discount source
880 sub action_price_popup {
881   my ($self) = @_;
882
883   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{reclamation_item_ids} };
884   my $item = $self->reclamation->items_sorted->[$idx];
885   if ($item->is_linked_to_record) {
886     $self->js->flash('error', t8("Can't change price of a linked item"));
887     return $self->js->render();
888   }
889
890   $self->render_price_dialog($item);
891 }
892
893 # save the reclamation in a session variable and redirect to the part controller
894 sub action_create_part {
895   my ($self) = @_;
896
897   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
898
899   my $callback     = $self->url_for(
900     action       => 'return_from_create_part',
901     type         => $self->type, # type is needed for check_auth on return
902     previousform => $previousform,
903   );
904
905   flash_later('info', t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.'));
906
907   my @redirect_params = (
908     controller => 'Part',
909     action     => 'add',
910     part_type  => $::form->{add_item}->{create_part_type},
911     callback   => $callback,
912     show_abort => 1,
913   );
914
915   $self->redirect_to(@redirect_params);
916 }
917
918 sub action_return_from_create_part {
919   my ($self) = @_;
920
921   $self->{created_part} = SL::DB::Part->new(
922     id => delete $::form->{new_parts_id}
923   )->load if $::form->{new_parts_id};
924
925   $::auth->restore_form_from_session(delete $::form->{previousform});
926   $self->reclamation($self->init_reclamation);
927   $self->reinit_after_new_reclamation();
928
929   if ($self->reclamation->id) {
930     $self->pre_render();
931     $self->render(
932       'reclamation/form',
933       title => $self->type_data->text('edit'),
934       %{$self->{template_args}},
935     );
936   } else {
937     $self->action_add;
938   }
939 }
940
941 # load the second row for one or more items
942 #
943 # This action gets the html code for all items second rows by rendering a template for
944 # the second row and sets the html code via client js.
945 sub action_load_second_rows {
946   my ($self) = @_;
947
948   foreach my $item_id (@{ $::form->{reclamation_item_ids} }) {
949     my $idx  = first_index { $_ eq $item_id } @{ $::form->{reclamation_item_ids} };
950     my $item = $self->reclamation->items_sorted->[$idx];
951
952     $self->js_load_second_row($item, $item_id, 0);
953   }
954
955   $self->js->run('kivi.Reclamation.init_row_handlers') if $self->reclamation->is_sales; # for lastcosts change-callback
956
957   $self->js->render();
958 }
959
960 # update description, notes and sellprice from master data
961 sub action_update_row_from_master_data {
962   my ($self) = @_;
963
964   foreach my $item_id (@{ $::form->{item_ids} }) {
965     my $idx   = first_index { $_ eq $item_id } @{ $::form->{reclamation_item_ids} };
966     my $item  = $self->reclamation->items_sorted->[$idx];
967
968     if ($item->is_linked_to_record) {
969       $self->js->flash_later('error', t8("Can't change data of a linked item. Part: " . $item->part->partnumber));
970       next;
971     }
972
973     my $texts = get_part_texts($item->part, $self->reclamation->language_id);
974
975     $item->description($texts->{description});
976     $item->longdescription($texts->{longdescription});
977
978     my ($price_src, undef) = SL::Model::Record->get_best_price_and_discount_source($self->reclamation, $item, ignore_given => 1);
979     $item->sellprice($price_src->price);
980     $item->active_price_source($price_src);
981
982     $self->js
983       ->run('kivi.Reclamation.update_sellprice', $item_id, $item->sellprice_as_number)
984       ->html('.row_entry:has(#item_' . $item_id
985              . ') [name = "partnumber"] a', $item->part->partnumber)
986       ->val ('.row_entry:has(#item_' . $item_id
987              . ') [name = "reclamation.reclamation_items[].description"]',
988              $item->description)
989       ->val ('.row_entry:has(#item_' . $item_id
990              . ') [name = "reclamation.reclamation_items[].longdescription"]',
991              $item->longdescription);
992
993     if ($self->search_cvpartnumber) {
994       $self->get_item_cvpartnumber($item);
995       $self->js->html('.row_entry:has(#item_' . $item_id
996                       . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
997     }
998   }
999
1000   $self->recalc();
1001   $self->js_redisplay_line_values;
1002   $self->js_redisplay_amounts_and_taxes;
1003
1004   $self->js->render();
1005 }
1006
1007 sub js_load_second_row {
1008   my ($self, $item, $item_id, $do_parse) = @_;
1009
1010   if ($do_parse) {
1011     # Parse values from form (they are formated while rendering (template)).
1012     # Workaround to pre-parse number-cvars (parse_custom_variable_values does
1013     # not parse number values). This parsing is not necessary at all, if we
1014     # assure that the second row/cvars are only loaded once.
1015     foreach my $var (@{ $item->cvars_by_config }) {
1016       if ($var->config->type eq 'number' && exists($var->{__unparsed_value})) {
1017         $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value}));
1018       }
1019     }
1020     $item->parse_custom_variable_values;
1021   }
1022
1023   my $row_as_html = $self->p->render('reclamation/tabs/basic_data/_second_row', ITEM => $item, TYPE => $self->type);
1024
1025   $self->js
1026     ->html('#second_row_' . $item_id, $row_as_html)
1027     ->data('#second_row_' . $item_id, 'loaded', 1);
1028 }
1029
1030 sub js_redisplay_line_values {
1031   my ($self) = @_;
1032
1033   my @data = map {[
1034        $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1035       ]} @{ $self->reclamation->items_sorted };
1036
1037   $self->js
1038     ->run('kivi.Reclamation.redisplay_line_values', $self->reclamation->is_sales, \@data);
1039 }
1040
1041 sub js_redisplay_amounts_and_taxes {
1042   my ($self) = @_;
1043
1044   if (scalar @{ $self->reclamation->taxes }) {
1045     $self->js->show('#taxincluded_row_id');
1046   } else {
1047     $self->js->hide('#taxincluded_row_id');
1048   }
1049
1050   if ($self->reclamation->taxincluded) {
1051     $self->js->hide('#subtotal_row_id');
1052   } else {
1053     $self->js->show('#subtotal_row_id');
1054   }
1055
1056   $self->js
1057     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->reclamation->netamount, -2))
1058     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->reclamation->amount,    -2))
1059     ->remove('.tax_row')
1060     ->insertBefore($self->build_tax_rows, '#amount_row_id');
1061 }
1062
1063 sub js_redisplay_cvpartnumbers {
1064   my ($self) = @_;
1065
1066   $self->get_item_cvpartnumber($_) for @{$self->reclamation->items_sorted};
1067
1068   my @data = map {[$_->{cvpartnumber}]} @{ $self->reclamation->items_sorted };
1069
1070   $self->js
1071     ->run('kivi.Reclamation.redisplay_cvpartnumbers', \@data);
1072 }
1073 sub js_reset_reclamation_and_item_ids_after_save {
1074   my ($self) = @_;
1075
1076   $self->js
1077     ->val('#id', $self->reclamation->id)
1078     ->val('#converted_from_record_type_ref', '')
1079     ->val('#converted_from_record_id',  '')
1080     ->val('#reclamation_record_number', $self->reclamation->record_number);
1081
1082   my $idx = 0;
1083   foreach my $form_item_id (@{ $::form->{reclamation_item_ids} }) {
1084     next if !$self->reclamation->items_sorted->[$idx]->id;
1085     next if $form_item_id !~ m{^new};
1086     $self->js
1087       ->val ('[name="reclamation_item_ids[+]"][value="' . $form_item_id . '"]',
1088              $self->reclamation->items_sorted->[$idx]->id)
1089       ->val ('#item_' . $form_item_id,
1090              $self->reclamation->items_sorted->[$idx]->id)
1091       ->attr('#item_' . $form_item_id, "id",
1092              'item_' . $self->reclamation->items_sorted->[$idx]->id);
1093   } continue {
1094     $idx++;
1095   }
1096   $self->js->val('[name="converted_from_record_item_type_refs[+]"]', '');
1097   $self->js->val('[name="converted_from_record_item_ids[+]"]', '');
1098 }
1099
1100 #
1101 # helpers
1102 #
1103
1104 sub init_valid_types {
1105   $_[0]->type_data->valid_types;
1106 }
1107
1108 sub init_type {
1109   my ($self) = @_;
1110
1111   my $type = $self->reclamation->record_type;
1112   if (none { $type eq $_ } @{$self->valid_types}) {
1113     die "Not a valid type for reclamation";
1114   }
1115
1116   $self->type($type);
1117 }
1118
1119 sub init_cv {
1120   my ($self) = @_;
1121
1122   $self->type_data->properties('customervendor');
1123 }
1124
1125 sub init_search_cvpartnumber {
1126   my ($self) = @_;
1127
1128   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1129   my $search_cvpartnumber;
1130   if ($self->type_data->properties('is_customer')) {
1131     $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber()
1132   } else {
1133     $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel();
1134   }
1135
1136   return $search_cvpartnumber;
1137 }
1138
1139 sub init_show_update_button {
1140   my ($self) = @_;
1141
1142   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1143 }
1144
1145 sub init_models {
1146   my ($self) = @_;
1147
1148   SL::Controller::Helper::GetModels->new(
1149     controller => $self,
1150     sorted => {
1151       _default  => {
1152         by  => 'record_number',
1153         dir => 0,
1154       },
1155       id                      => t8('ID'),
1156       record_number           => t8('Reclamation Number'),
1157       employee                => t8('Employee'),
1158       salesman                => t8('Salesman'),
1159       customer                => t8('Customer'),
1160       vendor                  => t8('Vendor'),
1161       contact                 => t8('Contact'),
1162       language                => t8('Language'),
1163       department              => t8('Department'),
1164       globalproject           => t8('Project Number'),
1165       cv_record_number        => ($self->type_data->properties('is_customer') ? t8('Customer Record Number') : t8('Vendor Record Number')),
1166       transaction_description => t8('Description'),
1167       notes                   => t8('Notes'),
1168       intnotes                => t8('Internal Notes'),
1169       shippingpoint           => t8('Shipping Point'),
1170       shipvia                 => t8('Ship via'),
1171       shipto_id               => t8('Shipping Address'),
1172       amount                  => t8('Total'),
1173       netamount               => t8('Subtotal'),
1174       delivery_term           => t8('Delivery Terms'),
1175       payment                 => t8('Payment Terms'),
1176       currency                => t8('Currency'),
1177       exchangerate            => t8('Exchangerate'),
1178       taxincluded             => t8('Tax Included'),
1179       taxzone                 => t8('Tax zone'),
1180       tax_point               => t8('Tax point'),
1181       reqdate                 => t8('Deadline'),
1182       transdate               => t8('Booking Date'),
1183       itime                   => t8('Creation Time'),
1184       mtime                   => t8('Last modification Time'),
1185       delivered               => t8('Delivered'),
1186       closed                  => t8('Closed'),
1187     },
1188     query => [
1189       SL::DB::Manager::Reclamation->type_filter($self->type),
1190       (salesman_id => SL::DB::Manager::Employee->current->id) x ($self->reclamation->is_sales  && !$::auth->assert('sales_all_edit', 1)),
1191       (employee_id => SL::DB::Manager::Employee->current->id) x ($self->reclamation->is_sales  && !$::auth->assert('sales_all_edit', 1)),
1192       (employee_id => SL::DB::Manager::Employee->current->id) x (!$self->reclamation->is_sales && !$::auth->assert('purchase_all_edit', 1)),
1193     ],
1194
1195     with_objects => [
1196         'customer',      'vendor',   'employee',   'salesman',
1197         'contact',       'language', 'department', 'globalproject',
1198         'delivery_term', 'payment',  'currency',   'taxzone',
1199       ],
1200     );
1201 }
1202
1203 sub init_p {
1204   SL::Presenter->get;
1205 }
1206
1207 sub init_reclamation {
1208   $_[0]->make_reclamation;
1209 }
1210
1211 sub init_all_price_factors {
1212   SL::DB::Manager::PriceFactor->get_all;
1213 }
1214
1215 sub init_part_picker_classification_ids {
1216   my ($self)    = @_;
1217
1218   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => $self->type_data->part_classification_query()) } ];
1219 }
1220
1221 sub check_auth {
1222   my ($self) = @_;
1223   $::auth->assert($self->type_data->rights('edit'));
1224 }
1225
1226 # build the selection box for contacts
1227 #
1228 # Needed, if customer/vendor changed.
1229 sub build_contact_select {
1230   my ($self) = @_;
1231
1232   select_tag('reclamation.contact_id', [ $self->reclamation->customervendor->contacts ],
1233     value_key  => 'cp_id',
1234     title_key  => 'full_name_dep',
1235     default    => $self->reclamation->contact_id,
1236     with_empty => 1,
1237     style      => 'width: 300px',
1238   );
1239 }
1240
1241 # build the selection box for shiptos
1242 #
1243 # Needed, if customer/vendor changed.
1244 sub build_shipto_select {
1245   my ($self) = @_;
1246
1247   select_tag('reclamation.shipto_id',
1248              [ {displayable_id => t8("No/individual shipping address"),
1249                 shipto_id => '',
1250                },
1251                $self->reclamation->customervendor->shipto
1252              ],
1253              value_key  => 'shipto_id',
1254              title_key  => 'displayable_id',
1255              default    => $self->reclamation->shipto_id,
1256              with_empty => 0,
1257              style      => 'width: 300px',
1258   );
1259 }
1260
1261 # build the inputs for the cusom shipto dialog
1262 #
1263 # Needed, if customer/vendor changed.
1264 sub build_shipto_inputs {
1265   my ($self) = @_;
1266
1267   my $content = $self->p->render('common/_ship_to_dialog',
1268                                  cv_obj      => $self->reclamation->customervendor,
1269                                  cs_obj      => $self->reclamation->custom_shipto,
1270                                  cvars       => $self->reclamation->custom_shipto->cvars_by_config,
1271                                  id_selector => '#reclamation_shipto_id');
1272
1273   div_tag($content, id => 'shipto_inputs');
1274 }
1275
1276 # render the info line for business
1277 #
1278 # Needed, if customer/vendor changed.
1279 sub build_business_info_row
1280 {
1281   $_[0]->p->render('reclamation/tabs/basic_data/_business_info_row', SELF => $_[0]);
1282 }
1283
1284 # build the rows for displaying taxes
1285 #
1286 # Called if amounts where recalculated and redisplayed.
1287 sub build_tax_rows {
1288   my ($self) = @_;
1289
1290   my $rows_as_html;
1291   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->reclamation->taxes }) {
1292     $rows_as_html .= $self->p->render(
1293                        'reclamation/tabs/basic_data/_tax_row',
1294                        SELF => $self,
1295                        TAX => $tax,
1296                        TAXINCLUDED => $self->reclamation->taxincluded,
1297                      );
1298   }
1299   return $rows_as_html;
1300 }
1301
1302 sub render_price_dialog {
1303   my ($self, $record_item) = @_;
1304
1305   my $price_source = SL::PriceSource->new(
1306                        record_item => $record_item,
1307                        record => $self->reclamation,
1308                      );
1309
1310   $self->js
1311     ->run(
1312       'kivi.io.price_chooser_dialog',
1313       t8('Available Prices'),
1314       $self->render(
1315         'reclamation/tabs/basic_data/_price_sources_dialog',
1316         { output => 0 },
1317         price_source => $price_source,
1318       ),
1319     )
1320     ->reinit_widgets;
1321
1322 #   if (@errors) {
1323 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1324 #     $self->js->show('#dialog_flash_error');
1325 #   }
1326
1327   $self->js->render;
1328 }
1329
1330 # load or create a new reclamation object
1331 #
1332 # And assign changes from the form to this object.
1333 # If the reclamation is loaded from db, check if items are deleted in the form,
1334 # remove them form the object and collect them for removing from db on saving.
1335 # Then create/update items from form (via make_item) and add them.
1336 sub make_reclamation {
1337   my ($self) = @_;
1338
1339   # add_items adds items to an reclamation with no items for saving, but they
1340   # cannot be retrieved via items until the reclamation is saved. Adding empty
1341   # items to new reclamation here solves this problem.
1342   my $reclamation;
1343   if ($::form->{id}) {
1344     $reclamation = SL::DB::Reclamation->new(id => $::form->{id})->load();
1345   } else {
1346     $reclamation = SL::DB::Reclamation->new(
1347                      record_type        => $::form->{type},
1348                      reclamation_items  => [],
1349                      currency_id => $::instance_conf->get_currency_id(),
1350                    );
1351     $reclamation = SL::Model::Record->update_after_new($reclamation)
1352   }
1353
1354   my $cv_id_method = $reclamation->type_data->properties('customervendor'). '_id';
1355   if (!$::form->{id} && $::form->{$cv_id_method}) {
1356     $reclamation->$cv_id_method($::form->{$cv_id_method});
1357     $reclamation = SL::Model::Record->update_after_customer_vendor_change($reclamation);
1358   }
1359
1360   # don't assign hashes as objects
1361   my $form_reclamation_items = delete $::form->{reclamation}->{reclamation_items};
1362
1363   $reclamation->assign_attributes(%{$::form->{reclamation}});
1364
1365   # restore form values
1366   $::form->{reclamation}->{reclamation_items} = $form_reclamation_items;
1367
1368   $self->setup_custom_shipto_from_form($reclamation, $::form);
1369
1370   # remove deleted items
1371   $self->item_ids_to_delete([]);
1372   foreach my $idx (reverse 0..$#{$reclamation->reclamation_items}) {
1373     my $item = $reclamation->reclamation_items->[$idx];
1374     if (none { $item->id == $_->{id} } @{$form_reclamation_items}) {
1375       splice @{$reclamation->reclamation_items}, $idx, 1;
1376       push @{$self->item_ids_to_delete}, $item->id;
1377     }
1378   }
1379
1380   my @items;
1381   my $pos = 1;
1382   foreach my $form_attr (@{$form_reclamation_items}) {
1383     my $item = make_item($reclamation, $form_attr);
1384     $item->position($pos);
1385     push @items, $item;
1386     $pos++;
1387   }
1388   $reclamation->add_items(grep {!$_->id} @items);
1389
1390   return $reclamation;
1391 }
1392
1393 # create or update items from form
1394 #
1395 # Make item objects from form values. For items already existing read from db.
1396 # Create a new item else. And assign attributes.
1397 sub make_item {
1398   my ($record, $attr) = @_;
1399
1400   my $item;
1401   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1402
1403   my $is_new = !$item;
1404
1405   # add_custom_variables adds cvars to an reclamation_item with no cvars for
1406   # saving, but they cannot be retrieved via custom_variables until the
1407   # reclamation/reclamation_item is saved. Adding empty custom_variables to new
1408   # reclamationitem here solves this problem.
1409   $item ||= SL::DB::ReclamationItem->new(custom_variables => []);
1410
1411   $item->assign_attributes(%$attr);
1412
1413   if ($is_new) {
1414     my $texts = get_part_texts($item->part, $record->language_id);
1415     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
1416     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
1417     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1418   }
1419
1420   return $item;
1421 }
1422
1423 sub load_reclamation {
1424   my ($self) = @_;
1425
1426   return if !$::form->{id};
1427
1428   $self->reclamation(SL::DB::Reclamation->new(id => $::form->{id})->load);
1429
1430   $self->reinit_after_new_reclamation();
1431
1432   return $self->reclamation;
1433 }
1434
1435 # create a new item
1436 #
1437 # This is used to add one item
1438 sub new_item {
1439   my ($record, $attr) = @_;
1440
1441   my $item = SL::DB::ReclamationItem->new;
1442
1443   # Remove attributes where the user left or set the inputs empty.
1444   # So these attributes will be undefined and we can distinguish them
1445   # from zero later on.
1446   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1447     delete $attr->{$_} if $attr->{$_} eq '';
1448   }
1449
1450   $item->assign_attributes(%$attr);
1451
1452   my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1453   $item->qty(1.0)          if !$item->qty;
1454   $item->unit($part->unit) if !$item->unit;
1455
1456   my ($price_src, $discount_src) = SL::Model::Record->get_best_price_and_discount_source($record, $item, ignore_given => 0);
1457
1458   my %new_attr;
1459   $new_attr{part}                   = $part;
1460   $new_attr{description}            = $part->description     if ! $item->description;
1461   $new_attr{qty}                    = 1.0                    if ! $item->qty;
1462   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
1463   $new_attr{sellprice}              = $price_src->price;
1464   $new_attr{discount}               = $discount_src->discount;
1465   $new_attr{active_price_source}    = $price_src;
1466   $new_attr{active_discount_source} = $discount_src;
1467   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
1468   $new_attr{project_id}             = $record->globalproject_id;
1469   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
1470
1471   # add_custom_variables adds cvars to an reclamationitem with no cvars for saving, but
1472   # they cannot be retrieved via custom_variables until the reclamation/reclamationitem is
1473   # saved. Adding empty custom_variables to new reclamationitem here solves this problem.
1474   $new_attr{custom_variables} = [];
1475
1476   my $texts = get_part_texts($part, $record->language_id,
1477                 description => $new_attr{description},
1478                 longdescription => $new_attr{longdescription},
1479               );
1480
1481   $item->assign_attributes(%new_attr, %{ $texts });
1482
1483   $item->reclamation($record);
1484   return $item;
1485 }
1486
1487 # setup custom shipto from form
1488 #
1489 # The dialog returns form variables starting with 'shipto' and cvars starting
1490 # with 'shiptocvar_'.
1491 # Mark it to be deleted if a shipto from master data is selected
1492 # (i.e. reclamation has a shipto).
1493 # Else, update or create a new custom shipto. If the fields are empty, it
1494 # will not be saved on save.
1495 sub setup_custom_shipto_from_form {
1496   my ($self, $reclamation, $form) = @_;
1497
1498   if ($reclamation->shipto) {
1499     $self->is_custom_shipto_to_delete(1);
1500   } else {
1501     my $custom_shipto =    $reclamation->custom_shipto
1502                         || $reclamation->custom_shipto(
1503                              SL::DB::Shipto->new(module => 'RC', custom_variables => [])
1504                            );
1505
1506     my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
1507     my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
1508
1509     $custom_shipto->assign_attributes(%$shipto_attrs);
1510     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
1511   }
1512 }
1513
1514 # recalculate prices and taxes
1515 #
1516 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1517 sub recalc {
1518   my ($self) = @_;
1519
1520   my %pat = $self->reclamation->calculate_prices_and_taxes();
1521
1522   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->reclamation->items_sorted}, @{$pat{items}};
1523 }
1524
1525 # get data for saving, printing, ..., that is not changed in the form
1526 #
1527 # Only cvars for now.
1528 sub get_unalterable_data {
1529   my ($self) = @_;
1530
1531   foreach my $item (@{ $self->reclamation->items }) {
1532     # autovivify all cvars that are not in the form (cvars_by_config can do it).
1533     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1534     foreach my $var (@{ $item->cvars_by_config }) {
1535       if ($var->config->type eq 'number' && exists($var->{__unparsed_value})) {
1536         $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value}));
1537       }
1538     }
1539     $item->parse_custom_variable_values;
1540   }
1541 }
1542
1543 # save the reclamation
1544 #
1545 # And delete items that are deleted in the form.
1546 sub save {
1547   my ($self) = @_;
1548
1549   set_record_link_conversions($self->reclamation,
1550     delete $::form->{RECORD_TYPE_REF()}
1551       => delete $::form->{RECORD_ID()},
1552     delete $::form->{RECORD_ITEM_TYPE_REF()}
1553       => delete $::form->{RECORD_ITEM_ID()},
1554   );
1555
1556   my $items_to_delete  = scalar @{ $self->item_ids_to_delete || [] }
1557                        ? SL::DB::Manager::ReclamationItem->get_all(where => [id => $self->item_ids_to_delete])
1558                        : undef;
1559
1560   SL::Model::Record->save($self->reclamation,
1561                           with_validity_token  => { scope => SL::DB::ValidityToken::SCOPE_RECLAMATION_SAVE(), token => $::form->{form_validity_token} },
1562                           delete_custom_shipto => $self->reclamation->custom_shipto && ($self->is_custom_shipto_to_delete || $self->reclamation->custom_shipto->is_empty),
1563                           items_to_delete      => $items_to_delete,
1564   );
1565
1566   if ($::form->{email_journal_id}) {
1567     my $email_journal = SL::DB::EmailJournal->new(
1568       id => delete $::form->{email_journal_id}
1569     )->load;
1570     $email_journal->link_to_record_with_attachment(
1571       $self->reclamation,
1572       delete $::form->{email_attachment_id}
1573     );
1574   }
1575
1576   delete $::form->{form_validity_token};
1577 }
1578
1579 sub reinit_after_new_reclamation {
1580   my ($self) = @_;
1581
1582   # change form type
1583   $::form->{type} = $self->reclamation->type;
1584   $self->type($self->init_type);
1585   $self->type_data($self->init_type_data);
1586   $self->cv($self->init_cv);
1587   $self->check_auth;
1588
1589   $self->setup_custom_shipto_from_form($self->reclamation, $::form);
1590
1591   foreach my $item (@{$self->reclamation->items_sorted}) {
1592     # set item ids to new fake id, to identify them as new items
1593     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1594
1595     # trigger rendering values for second row as hidden, because they
1596     # are loaded only on demand. So we need to keep the values from the
1597     # source.
1598     $item->{render_second_row} = 1;
1599   }
1600
1601   $self->get_unalterable_data();
1602   $self->recalc();
1603 }
1604
1605 sub pre_render {
1606   my ($self) = @_;
1607
1608   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
1609   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
1610   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
1611   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted();
1612   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(
1613                                           where => [ or => [
1614                                                         id => $self->reclamation->employee_id,
1615                                                         deleted => 0 ] ],
1616                                                      sort_by => 'name');
1617   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(
1618                                           where => [ or => [
1619                                                         id => $self->reclamation->salesman_id,
1620                                                         deleted => 0 ] ],
1621                                           sort_by => 'name');
1622   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(
1623                                           where => [ or => [
1624                                                         id => $self->reclamation->payment_id,
1625                                                         obsolete => 0 ] ]);
1626   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1627   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
1628
1629   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
1630
1631   my $print_form = Form->new('');
1632   $print_form->{type}        = $self->type;
1633   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
1634   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
1635     form => $print_form,
1636     options => {dialog_name_prefix => 'print_options.',
1637                 show_headers       => 1,
1638                 no_queue           => 1,
1639                 no_postscript      => 1,
1640                 no_opendocument    => 0,
1641                 no_html            => 1},
1642   );
1643   foreach my $item (@{$self->reclamation->reclamation_items}) {
1644     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->reclamation);
1645     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
1646     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1647   }
1648
1649   if ($self->reclamation->record_number && $::instance_conf->get_webdav) {
1650     my $webdav = SL::Webdav->new(
1651       type     => $self->type,
1652       number   => $self->reclamation->record_number,
1653     );
1654     my @all_objects = $webdav->get_all_objects;
1655     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1656                                                     type => t8('File'),
1657                                                     link => File::Spec->catfile($_->full_filedescriptor),
1658                                                 } } @all_objects;
1659   }
1660
1661   $self->get_item_cvpartnumber($_) for @{$self->reclamation->items_sorted};
1662
1663   $::request->{layout}->use_javascript("${_}.js") for
1664     qw(kivi.SalesPurchase kivi.Reclamation kivi.File
1665        calculate_qty kivi.Validator follow_up
1666        show_history
1667       );
1668   $self->_setup_edit_action_bar;
1669 }
1670
1671 sub prepare_report {
1672   my ($self)         = @_;
1673
1674   my $report         =  SL::ReportGenerator->new(\%::myconfig, $::form);
1675   $report->{title}   =  t8('Sales Reclamations');
1676   if ($self->type eq PURCHASE_RECLAMATION_TYPE()){
1677     $report->{title} = t8('Purchase Reclamations');
1678   }
1679
1680   $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
1681   $self->models->add_additional_url_params(type => $self->type);
1682   $self->models->finalize; # for filter laundering
1683
1684   my $callback    = $self->models->get_callback;
1685
1686   $self->{report} = $report;
1687
1688   # TODO: shipto_id is not linked to custom_shipto
1689   my @columns_order = qw(
1690     id
1691     record_number
1692     employee
1693     salesman
1694     customer
1695     vendor
1696     contact
1697     language
1698     department
1699     globalproject
1700     cv_record_number
1701     transaction_description
1702     notes
1703     intnotes
1704     shippingpoint
1705     shipvia
1706     amount
1707     netamount
1708     delivery_term
1709     payment
1710     currency
1711     exchangerate
1712     taxincluded
1713     taxzone
1714     tax_point
1715     reqdate
1716     transdate
1717     itime
1718     mtime
1719     delivered
1720     closed
1721   );
1722
1723   my @default_columns = qw(
1724     record_number
1725     employee
1726     department
1727     globalproject
1728     cv_record_number
1729     transaction_description
1730     amount
1731     reqdate
1732     transdate
1733     itime
1734     mtime
1735     delivered
1736     closed
1737   );
1738
1739   my %column_defs = (
1740     id => {
1741       obj_link => sub {$self->url_for(action => 'edit', id => $_[0]->id, type => $self->type, callback => $callback)},
1742       sub      => sub { $_[0]->id },
1743     },
1744     record_number => {
1745       obj_link => sub {$self->url_for(action => 'edit', id => $_[0]->id, type => $self->type, callback => $callback)},
1746       sub      => sub { $_[0]->record_number },
1747     },
1748     employee => {
1749       sub      => sub { $_[0]->employee ? $_[0]->employee->name : '' },
1750     },
1751     salesman => {
1752       sub      => sub { $_[0]->salesman ? $_[0]->salesman->name : '' },
1753     },
1754     language => {
1755       sub      => sub { $_[0]->language ? $_[0]->language->article_code : '' },
1756     },
1757     department => {
1758       sub      => sub { $_[0]->department ? $_[0]->department->description : '' },
1759     },
1760     globalproject => {
1761       obj_link => sub { $_[0]->globalproject_id ?
1762        $self->url_for(
1763           controller => "controller.pl",
1764           action => 'Project/edit',
1765           id => $_[0]->globalproject_id,
1766           callback => $callback
1767         ) : '' },
1768       sub      => sub { !$_[0]->globalproject ? '' : $_[0]->globalproject->projectnumber },
1769     },
1770     cv_record_number => {
1771       sub      => sub { $_[0]->cv_record_number },
1772     },
1773     transaction_description => {
1774       sub      => sub { $_[0]->transaction_description },
1775     },
1776     notes => {
1777       sub      => sub { $_[0]->notes },
1778     },
1779     intnotes => {
1780       sub      => sub { $_[0]->intnotes },
1781     },
1782     shippingpoint => {
1783       sub      => sub { $_[0]->shippingpoint },
1784     },
1785     shipvia => {
1786       sub      => sub { $_[0]->shipvia },
1787     },
1788     # TODO: custom ship to is not safed in reclamation
1789     #shipto_id => {
1790     #  sub      => sub { $_[0]->shipto ? $_[0]->shipto->shiptoname : '' },
1791     #},
1792     amount  => {
1793       sub      => sub { $_[0]->amount_as_number },
1794     },
1795     netamount  => {
1796       sub      => sub { $_[0]->netamount_as_number },
1797     },
1798     delivery_term => {
1799       obj_link => sub { $_[0]->delivery_term_id ?
1800        $self->url_for(
1801           controller => "controller.pl",
1802           action => 'DeliveryTerm/edit',
1803           id => $_[0]->delivery_term_id,
1804           callback => $callback
1805         ) : '' },
1806       sub      => sub { $_[0]->delivery_term ? $_[0]->delivery_term->description : '' },
1807     },
1808     payment => {
1809       obj_link => sub { $_[0]->payment_id ?
1810        $self->url_for(
1811           controller => "controller.pl",
1812           action => 'PaymentTerm/edit',
1813           id => $_[0]->payment_id,
1814           callback => $callback
1815         ) : '' },
1816       sub      => sub { $_[0]->payment ? $_[0]->payment->description : '' },
1817     },
1818     currency => {
1819       sub      => sub { $_[0]->currency ? $_[0]->currency->name : '' },
1820     },
1821     exchangerate  => {
1822       sub      => sub { $_[0]->exchangerate ? $_[0]->exchangerate_as_number : '' },
1823     },
1824     taxincluded => {
1825       sub      => sub { $_[0]->taxincluded ? t8('Yes') : t8('No') },
1826     },
1827     taxzone => {
1828       obj_link => sub { $_[0]->taxzone_id ?
1829        $self->url_for(
1830           controller => "controller.pl",
1831           action => 'Taxzones/edit',
1832           id => $_[0]->taxzone_id,
1833           callback => $callback
1834         ) : '' },
1835       sub      => sub { $_[0]->taxzone ? $_[0]->taxzone->description : '' },
1836     },
1837     tax_point  => {
1838       sub      => sub { $_[0]->tax_point ? ($_[0]->tax_point)->to_kivitendo(precision => 'day') : '' },
1839     },
1840     reqdate  => {
1841       sub      => sub { $_[0]->reqdate ? ($_[0]->reqdate)->to_kivitendo(precision => 'day') : '' },
1842     },
1843     transdate  => {
1844       sub      => sub { $_[0]->transdate ? ($_[0]->transdate)->to_kivitendo(precision => 'day') : '' },
1845     },
1846     itime      => {
1847       sub      => sub { $_[0]->itime->to_kivitendo(precision => 'minute') }
1848     },
1849     mtime      => {
1850       sub      => sub { $_[0]->mtime ? $_[0]->mtime->to_kivitendo(precision => 'minute') : '' }
1851     },
1852     delivered => {
1853       sub      => sub { $_[0]->delivered ? t8('Yes') : t8('No') },
1854     },
1855     closed => {
1856       sub      => sub { $_[0]->closed ? t8('Yes') : t8('No') },
1857     },
1858   );
1859   if ($self->type_data->properties('is_customer')) {
1860     $column_defs{customer} = ({
1861       raw_data => sub { $_[0]->customervendor->presenter->customer(display => 'table-cell', callback => $callback) },
1862       sub      => sub { $_[0]->customervendor->name },
1863     });
1864     $column_defs{contact} = ({
1865       obj_link => sub { $self->url_for(
1866           controller => "controller.pl",
1867           action => 'CustomerVendor/edit',
1868           db => 'customer',
1869           id => $_[0]->customer_id
1870         ) . '#contacts'
1871       },
1872       sub      => sub { $_[0]->contact ? $_[0]->contact->cp_name : '' },
1873     });
1874   } else {
1875     $column_defs{vendor} = ({
1876       raw_data => sub { $_[0]->customervendor->presenter->vendor(display => 'table-cell', callback => $callback) },
1877       sub      => sub { $_[0]->customervendor->name },
1878     });
1879     $column_defs{contact} = ({
1880       obj_link => sub { $self->url_for(
1881           controller => "controller.pl",
1882           action => 'CustomerVendor/edit',
1883           db => 'vendor',
1884           id => $_[0]->vendor_id
1885         ) . "#contacts"
1886       },
1887       sub      => sub { $_[0]->contact ? $_[0]->contact->cp_name : '' },
1888     });
1889   }
1890   $column_defs{$_}->{text} ||= t8( $self->models->get_sort_spec->{$_}->{title} || $_ ) for keys %column_defs;
1891
1892   unless ($::form->{active_in_report}) {
1893     $::form->{active_in_report}->{$_} = 1 foreach @default_columns;
1894   }
1895   $self->models->add_additional_url_params(
1896     active_in_report => $::form->{active_in_report});
1897   map { $column_defs{$_}->{visible} = $::form->{active_in_report}->{"$_"} }
1898     keys %column_defs;
1899
1900   ## add cvars TODO: Add own cvars
1901   #my %cvar_column_defs = map {
1902   #  my $cfg = $_;
1903   #  (('cvar_' . $cfg->name) => {
1904   #    sub     => sub { my $var = $_[0]->cvar_by_name($cfg->name); $var ? $var->value_as_text : '' },
1905   #    text    => $cfg->description,
1906   #    visible => $self->include_cvars->{ $cfg->name } ? 1 : 0,
1907   #  })
1908   #} @{ $self->includeable_cvar_configs };
1909
1910   #push @columns, map { 'cvar_' . $_->name } @{ $self->includeable_cvar_configs };
1911   #%column_defs = (%column_defs, %cvar_column_defs);
1912
1913   #my @cvar_column_form_names = ('_include_cvars_from_form', map { "include_cvars_" . $_->name } @{ $self->includeable_cvar_configs });
1914
1915   # make all sortable
1916   my @sortable = keys %column_defs;
1917
1918   my $filter_html = SL::Presenter::ReclamationFilter::filter(
1919     $::form->{filter}, $self->type, active_in_report => $::form->{active_in_report}
1920   );
1921
1922   $report->set_options(
1923     std_column_visibility => 1,
1924     controller_class      => 'Reclamation',
1925     output_format         => 'HTML',
1926     raw_top_info_text     => $self->render(
1927      'reclamation/_report_top',
1928      { output => 0 },
1929      FILTER_HTML => $filter_html,
1930     ),
1931     raw_bottom_info_text  => $self->render(
1932      'reclamation/_report_bottom',
1933      { output => 0 },
1934      models => $self->models
1935     ),
1936     title                 => $self->type_data->text('list'),
1937     allow_pdf_export      => 1,
1938     allow_csv_export      => 1,
1939   );
1940   $report->set_columns(%column_defs);
1941   $report->set_column_order(@columns_order);
1942   #$report->set_export_options(qw(list filter), @cvar_column_form_names); TODO: for cvars
1943   $report->set_export_options(qw(list filter active_in_report));
1944   $report->set_options_from_form;
1945   $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
1946 }
1947
1948 sub _setup_edit_action_bar {
1949   my ($self, %params) = @_;
1950
1951   for my $bar ($::request->layout->get('actionbar')) {
1952     $bar->add(
1953       combobox => [
1954         action => [
1955           t8('Save'),
1956           call      => [ 'kivi.Reclamation.save', {
1957               action             => 'save',
1958               warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
1959               warn_on_reqdate    => $::instance_conf->get_reclamation_warn_no_reqdate,
1960             }],
1961           checks    => [
1962             ['kivi.validate_form','#reclamation_form'],
1963           ],
1964         ],
1965         action => [
1966           t8('Save and Close'),
1967           call      => [ 'kivi.Reclamation.save', {
1968               action             => 'save',
1969               warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
1970               warn_on_reqdate    => $::instance_conf->get_reclamation_warn_no_reqdate,
1971               form_params        => [
1972                 { name => 'back_to_caller', value => 1 },
1973               ],
1974             }],
1975           checks    => [
1976             ['kivi.validate_form','#reclamation_form'],
1977           ],
1978         ],
1979         action => [
1980           t8('Save as new'),
1981           call      => [ 'kivi.Reclamation.save', {
1982               action             => 'save_as_new',
1983               warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
1984             }],
1985           disabled  => !$self->reclamation->id ? t8('This object has not been saved yet.') : undef,
1986         ],
1987       ], # end of combobox "Save"
1988
1989       combobox => [
1990         action => [
1991           t8('Workflow'),
1992         ],
1993         action => [
1994           t8('Save and Sales Reclamation'),
1995           call      => [ 'kivi.Reclamation.save', {
1996               action             => 'save_and_new_record',
1997               warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
1998               warn_on_reqdate    => $::instance_conf->get_reclamation_warn_no_reqdate,
1999               form_params        => [
2000                 { name => 'to_type', value => SALES_RECLAMATION_TYPE() },
2001               ],
2002             }],
2003           only_if  => $self->type_data->show_menu('save_and_sales_reclamation'),
2004         ],
2005         action => [
2006           t8('Save and Purchase Reclamation'),
2007           call      => [ 'kivi.Reclamation.purchase_reclamation_check_for_direct_delivery', {
2008               action             => 'save_and_new_record',
2009               warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
2010               warn_on_reqdate    => $::instance_conf->get_reclamation_warn_no_reqdate,
2011               form_params        => [
2012                 { name => 'to_type', value => PURCHASE_RECLAMATION_TYPE() },
2013               ],
2014             }
2015           ],
2016           only_if  => $self->type_data->show_menu('save_and_purchase_reclamation'),
2017         ],
2018         action => [
2019           t8('Save and Order'),
2020           call      => [ 'kivi.Reclamation.save', {
2021               action             => 'save_and_new_record',
2022               warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
2023               warn_on_reqdate    => $::instance_conf->get_reclamation_warn_no_reqdate,
2024               form_params        => [
2025                 { name => 'to_type',
2026                   value => $self->reclamation->is_sales ? SALES_ORDER_TYPE()
2027                                                         : PURCHASE_ORDER_TYPE() },
2028               ],
2029             }],
2030         ],
2031         action => [
2032           t8('Save and RMA Delivery Order'),
2033           call      => [ 'kivi.Reclamation.save', {
2034               action             => 'save_and_new_record',
2035               warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
2036               warn_on_reqdate    => $::instance_conf->get_reclamation_warn_no_reqdate,
2037               form_params        => [
2038                 { name => 'to_type', value => RMA_DELIVERY_ORDER_TYPE() },
2039               ],
2040             }],
2041           only_if  => $self->type_data->show_menu('save_and_rma_delivery_order'),
2042         ],
2043         action => [
2044           t8('Save and Supplier Delivery Order'),
2045           call      => [ 'kivi.Reclamation.save', {
2046               action             => 'save_and_new_record',
2047               warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
2048               warn_on_reqdate    => $::instance_conf->get_reclamation_warn_no_reqdate,
2049               form_params        => [
2050                 { name => 'to_type', value => SUPPLIER_DELIVERY_ORDER_TYPE() },
2051               ],
2052             }],
2053           only_if  => $self->type_data->show_menu('save_and_supplier_delivery_order'),
2054         ],
2055         action => [
2056           t8('Save and Credit Note'),
2057           call      => [ 'kivi.Reclamation.save', {
2058               action             => 'save_and_credit_note',
2059               warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
2060               warn_on_reqdate    => $::instance_conf->get_reclamation_warn_no_reqdate,
2061               form_params        => [
2062                 { name => 'to_type', value => 'credit_note' },
2063               ],
2064             }],
2065           only_if  => $self->type_data->show_menu('save_and_credit_note'),
2066         ],
2067       ], # end of combobox "Workflow"
2068
2069       combobox => [
2070         action => [
2071           t8('Export'),
2072         ],
2073         action => [
2074           t8('Save and preview PDF'),
2075           call      => [ 'kivi.Reclamation.save', {
2076               action             => 'preview_pdf',
2077               warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
2078               warn_on_reqdate    => $::instance_conf->get_reclamation_warn_no_reqdate,
2079             }],
2080         ],
2081         action => [
2082           t8('Save and print'),
2083           call => [
2084             'kivi.Reclamation.show_print_options',
2085             $::instance_conf->get_reclamation_warn_duplicate_parts,
2086             $::instance_conf->get_reclamation_warn_no_reqdate,
2087           ],
2088         ],
2089         action => [
2090           t8('Save and E-mail'),
2091           id   => 'save_and_email_action',
2092           call      => [ 'kivi.Reclamation.save', {
2093               action             => 'save_and_show_email_dialog',
2094               warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
2095               warn_on_reqdate    => $::instance_conf->get_reclamation_warn_no_reqdate,
2096             }],
2097           disabled => !$self->reclamation->id ? t8('This object has not been saved yet.') : undef,
2098         ],
2099         action => [
2100           t8('Download attachments of all parts'),
2101           call     => [ 'kivi.File.downloadReclamationitemsFiles', $::form->{type}, $::form->{id} ],
2102           disabled => !$self->reclamation->id ? t8('This object has not been saved yet.') : undef,
2103           only_if  => $::instance_conf->get_doc_storage,
2104         ],
2105       ], # end of combobox "Export"
2106
2107       action => [
2108         t8('Delete'),
2109         call     => [ 'kivi.Reclamation.delete_reclamation' ],
2110         confirm  => t8('Do you really want to delete this object?'),
2111         disabled => !$self->reclamation->id ? t8('This object has not been saved yet.') : undef,
2112         only_if  => $self->type_data->show_menu('delete'),
2113       ],
2114
2115       combobox => [
2116         action => [
2117           t8('more')
2118         ],
2119         action => [
2120           t8('Follow-Up'),
2121           call     => [ 'kivi.Reclamation.follow_up_window' ],
2122           disabled => !$self->reclamation->id ? t8('This object has not been saved yet.') : undef,
2123           only_if  => $::auth->assert('productivity', 1),
2124         ],
2125         action => [
2126           t8('History'),
2127           call     => [ 'set_history_window', $self->reclamation->id, 'id' ],
2128           disabled => !$self->reclamation->id ? t8('This record has not been saved yet.') : undef,
2129         ],
2130       ], # end of combobox "more"
2131     );
2132   }
2133 }
2134
2135 sub _setup_search_action_bar {
2136   my ($self, %params) = @_;
2137
2138   for my $bar ($::request->layout->get('actionbar')) {
2139     $bar->add(
2140       action => [
2141         t8('Update'),
2142         submit    => [ '#search_form', { action => 'Reclamation/list', type => $self->type } ],
2143         accesskey => 'enter',
2144       ],
2145       link => [
2146         t8('Add'),
2147         link => $self->url_for(action => 'add', type => $self->type),
2148       ],
2149     );
2150   }
2151 }
2152
2153 sub generate_pdf {
2154   my ($reclamation, $pdf_ref, $params) = @_;
2155
2156   my @errors = ();
2157
2158   my $print_form = Form->new('');
2159   $print_form->{type}        = $reclamation->type;
2160   $print_form->{formname}    = $params->{formname} || $reclamation->type;
2161   $print_form->{format}      = $params->{format}   || 'pdf';
2162   $print_form->{media}       = $params->{media}    || 'file';
2163   $print_form->{groupitems}  = $params->{groupitems};
2164   $print_form->{printer_id}  = $params->{printer_id};
2165   $print_form->{language_id} = $params->{language} ? $params->{language}->id : undef;
2166   $print_form->{media}       = 'file'       if $print_form->{media} eq 'screen';
2167
2168   $reclamation->language($params->{language});
2169
2170   # Make reclamation available in template
2171   $print_form->{reclamation} = $reclamation;
2172
2173   my $template_ext;
2174   my $template_type;
2175   my $variable_content_types;
2176   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
2177     $template_ext  = 'odt';
2178     $template_type = 'OpenDocument';
2179
2180     # add variables for printing with the built-in parser
2181     $reclamation->flatten_to_form($print_form, format_amounts => 1);
2182     $reclamation->add_legacy_template_arrays($print_form);
2183
2184     $variable_content_types = {
2185       longdescription => 'html',
2186       notes           => 'html',
2187       $::form->get_variable_content_types_for_cvars,
2188     }
2189   }
2190
2191   # search for the template
2192   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
2193     name        => $print_form->{formname},
2194     extension   => $template_ext,
2195     email       => $print_form->{media} eq 'email',
2196     language    => $params->{language},
2197     printer_id  => $print_form->{printer_id},
2198   );
2199
2200   if (!defined $template_file) {
2201     push @errors, t8(
2202       'Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.',
2203       join ', ',
2204       map { "'$_'"} @template_files
2205     );
2206   }
2207
2208   return @errors if scalar @errors;
2209
2210   $print_form->throw_on_error(sub {
2211     eval {
2212       $print_form->prepare_for_printing;
2213
2214       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
2215         format        => $print_form->{format},
2216         template_type => $template_type,
2217         template      => $template_file,
2218         variables     => $print_form,
2219         variable_content_types => $variable_content_types,
2220       );
2221       1;
2222     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
2223   });
2224
2225   return @errors;
2226 }
2227
2228 sub get_files_for_email_dialog {
2229   my ($self) = @_;
2230
2231   my %files = map { ($_ => []) } qw(versions files cv_files project_files part_files);
2232
2233   return %files if !$::instance_conf->get_doc_storage;
2234
2235   if ($self->reclamation->id) {
2236     $files{versions} = [
2237       SL::File->get_all_versions(
2238         object_id => $self->reclamation->id,
2239         object_type => $self->reclamation->type,
2240         file_type => 'document')
2241     ];
2242     $files{files} = [
2243       SL::File->get_all(
2244         object_id => $self->reclamation->id,
2245         object_type => $self->reclamation->type,
2246         file_type => 'attachment')
2247     ];
2248     $files{cv_files} = [
2249       SL::File->get_all(
2250         object_id => $self->reclamation->customervendor->id,
2251         object_type => $self->cv,
2252         file_type => 'attachment')
2253     ];
2254     $files{project_files} = [
2255       SL::File->get_all(
2256         object_id => $self->reclamation->globalproject_id,
2257         object_type => 'project',
2258         file_type => 'attachment')
2259     ];
2260   }
2261
2262   my @parts =
2263     uniq_by { $_->{id} }
2264     map {
2265       +{ id         => $_->part->id,
2266          partnumber => $_->part->partnumber }
2267     } @{$self->reclamation->items_sorted};
2268
2269   foreach my $part (@parts) {
2270     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
2271     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
2272   }
2273
2274   foreach my $key (keys %files) {
2275     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
2276   }
2277
2278   return %files;
2279 }
2280
2281 sub get_item_cvpartnumber {
2282   my ($self, $item) = @_;
2283
2284   return if !$self->search_cvpartnumber;
2285   return if !$self->reclamation->customervendor;
2286
2287   if (!$self->reclamation->is_sales) {
2288     my @mms = grep { $_->make eq $self->reclamation->customervendor->id } @{$item->part->makemodels};
2289     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
2290   } elsif ($self->reclamation->is_sales) {
2291     my @cps = grep { $_->customer_id eq $self->reclamation->customervendor->id } @{$item->part->customerprices};
2292     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
2293   }
2294 }
2295
2296 sub get_part_texts {
2297   my ($part_or_id, $language_or_id, %defaults) = @_;
2298
2299   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
2300   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
2301   my $texts       = {
2302     description     => $defaults{description}     // $part->description,
2303     longdescription => $defaults{longdescription} // $part->notes,
2304   };
2305
2306   return $texts unless $language_id;
2307
2308   my $translation = SL::DB::Manager::Translation->get_first(
2309     where => [
2310       parts_id    => $part->id,
2311       language_id => $language_id,
2312     ]);
2313
2314   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
2315   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
2316
2317   return $texts;
2318 }
2319
2320 sub save_history {
2321   my ($self, $addition) = @_;
2322
2323   SL::DB::History->new(
2324     trans_id    => $self->reclamation->id,
2325     employee_id => SL::DB::Manager::Employee->current->id,
2326     what_done   => $self->reclamation->type,
2327     snumbers    => 'record_number_' . $self->reclamation->record_number,
2328     addition    => $addition,
2329   )->save;
2330 }
2331
2332 sub store_pdf_to_webdav_and_filemanagement {
2333   my($reclamation, $content, $filename) = @_;
2334
2335   my @errors;
2336
2337   # copy file to webdav folder
2338   if ($reclamation->record_number && $::instance_conf->get_webdav_documents) {
2339     my $webdav = SL::Webdav->new(
2340       type     => $reclamation->type,
2341       number   => $reclamation->record_number,
2342     );
2343     my $webdav_file = SL::Webdav::File->new(
2344       webdav   => $webdav,
2345       filename => $filename,
2346     );
2347     eval {
2348       $webdav_file->store(data => \$content);
2349       1;
2350     } or do {
2351       push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
2352     };
2353   }
2354   if ($reclamation->id && $::instance_conf->get_doc_storage) {
2355     eval {
2356       SL::File->save(object_id     => $reclamation->id,
2357                      object_type   => $reclamation->type,
2358                      mime_type     => 'application/pdf',
2359                      source        => 'created',
2360                      file_type     => 'document',
2361                      file_name     => $filename,
2362                      file_contents => $content);
2363       1;
2364     } or do {
2365       push @errors, t8('Storing PDF in storage backend failed: #1', $@);
2366     };
2367   }
2368
2369   return @errors;
2370 }
2371
2372 sub init_type_data {
2373   my ($self) = @_;
2374   SL::DB::Helper::TypeDataProxy->new('SL::DB::Reclamation', $self->reclamation->record_type);
2375 }
2376
2377 1;
2378
2379 __END__
2380
2381 =encoding utf-8
2382
2383 =head1 NAME
2384
2385 SL::Controller::Reclamation - controller for reclamations
2386
2387 =head1 SYNOPSIS
2388
2389 This is a new form to enter reclamations, written with the use
2390 of controller and java script techniques.
2391
2392 The aim is to provide the user a good experience and a fast workflow.
2393
2394 =head2 Key Features
2395
2396 =over 4
2397
2398 =item *
2399
2400 One input row, so that input happens every time at the same place.
2401
2402 =item *
2403
2404 Use of pickers where possible.
2405
2406 =item *
2407
2408 Possibility to enter more than one item at once.
2409
2410 =item *
2411
2412 Item list in a scrollable area, so that the workflow buttons stay at
2413 the bottom.
2414
2415 =item *
2416
2417 Ordering item rows with drag and drop is possible. Sorting item rows is
2418 possible (by partnumber, description, reason, qty, sellprice
2419 and discount for now).
2420
2421 =item *
2422
2423 No C<update> is necessary. All entries and calculations are managed
2424 with ajax-calls and the page only reloads on C<save>.
2425
2426 =item *
2427
2428 User can see changes immediately, because of the use of java script
2429 and ajax.
2430
2431 =item *
2432
2433 Parts that are linked though RecordLinks are protected against price editing.
2434
2435 =back
2436
2437 =head1 CODE
2438
2439 =head2 Layout
2440
2441 =over 4
2442
2443 =item * C<SL/Controller/Reclamation.pm>
2444
2445 the controller
2446
2447 =item * C<template/webpages/reclamation/form.html>
2448
2449 main form
2450
2451 =item * C<template/webpages/reclamation/tabs/basic_data.html>
2452
2453 Main tab for basic_data.
2454
2455 This is the only tab here for now. "webdav", "documents", "attachements" and
2456 "linked records" tabs are reused from generic code.
2457
2458 =over 4
2459
2460 =item * C<template/webpages/reclamation/tabs/basic_data/_business_info_row.html>
2461
2462 For displaying information on business type
2463
2464 =item * C<template/webpages/reclamation/tabs/basic_data/_item_input.html>
2465
2466 The input line for items
2467
2468 =item * C<template/webpages/reclamation/tabs/basic_data/_row.html>
2469
2470 One row for already entered items
2471
2472 =item * C<template/webpages/reclamation/tabs/basic_data/_second_row.html>
2473
2474 Foldable second row for already entered items with more fields
2475
2476 =item * C<template/webpages/reclamation/tabs/basic_data/_tax_row.html>
2477
2478 Displaying tax information
2479
2480 =item * C<template/webpages/reclamation/tabs/basic_data/_price_sources_dialog.html>
2481
2482 Dialog for selecting price and discount sources
2483
2484 =back
2485
2486 =item * C<js/kivi.Reclamation.js>
2487
2488 java script functions
2489
2490 =back
2491
2492 =head1 KNOWN BUGS AND CAVEATS
2493
2494 =over 4
2495
2496 =item *
2497
2498 Table header is not sticky in the scrolling area.
2499
2500 =item *
2501
2502 Sorting does not include C<position>, neither does reordering.
2503
2504 This behavior was implemented intentionally. But we can discuss, which behavior
2505 should be implemented.
2506
2507 =back
2508
2509 =head1 To discuss / Nice to have
2510
2511 =over 4
2512
2513 =item *
2514
2515 Possibility to select PriceSources in input row?
2516
2517 =item *
2518
2519 This controller uses a (changed) copy of the template for the PriceSource
2520 dialog. Maybe there could be used one code source.
2521
2522 =item *
2523
2524 A warning when leaving the page without saving unchanged inputs.
2525
2526 =back
2527
2528 =head1 AUTHOR
2529
2530 Tamino Steinert E<lt>tamino.steinert@tamino.stE<gt>
2531
2532 =cut