1 package SL::Controller::Reclamation;
4 use parent qw(SL::Controller::Base);
6 use SL::Helper::Flash qw(flash_later);
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;
13 use SL::ReportGenerator;
14 use SL::Controller::Helper::ReportGenerator;
18 use SL::Util qw(trim);
21 use SL::DB::Reclamation;
22 use SL::DB::ReclamationItem;
26 use SL::DB::RecordLink;
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);
35 use SL::Helper::CreatePDF qw(:all);
36 use SL::Helper::PrintOptions;
37 use SL::Helper::UserPreferences::PositionsScrollbar;
38 use SL::Helper::UserPreferences::UpdatePositions;
40 use SL::Controller::Helper::GetModels;
43 use SL::DB::DeliveryOrder;
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);
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);
58 use Rose::Object::MakeMethods::Generic
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
69 __PACKAGE__->run_before('check_auth');
71 __PACKAGE__->run_before('recalc',
73 save save_as_new print preview_pdf send_email
74 save_and_show_email_dialog
79 __PACKAGE__->run_before('get_unalterable_data',
81 save save_as_new print preview_pdf send_email
82 save_and_show_email_dialog
91 # add a new reclamation
97 if (!$::form->{form_validity_token}) {
98 $::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_RECLAMATION_SAVE())->token;
103 title => $self->type_data->text('add'),
104 %{$self->{template_args}},
108 sub action_add_from_record {
110 my $from_type = $::form->{from_type};
111 my $from_id = $::form->{from_id};
113 die "No 'from_type' was given." unless ($from_type);
114 die "No 'from_id' was given." unless ($from_id);
117 if (defined($::form->{from_item_ids})) {
118 my %use_item = map { $_ => 1 } @{$::form->{from_item_ids}};
119 $flags{item_filter} = sub {
121 return %use_item{$item->{RECORD_ITEM_ID()}};
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();
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;
136 # remove any custom shipto if not wanted
137 $self->reclamation->custom_shipto(SL::DB::Shipto->new(module => 'RC', custom_variables => []));
144 sub action_add_from_email_journal {
146 die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
151 sub action_edit_with_email_journal_workflow {
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};
158 $self->action_edit();
161 # edit an existing reclamation
164 die "No 'id' was given." unless $::form->{id};
166 $self->load_reclamation();
171 title => $self->type_data->text('edit'),
172 %{$self->{template_args}},
176 # delete the reclamation
181 SL::Model::Record->delete($self->reclamation);
182 flash_later('info', $self->type_data->text('delete'));
184 my @redirect_params = (
189 $self->redirect_to(@redirect_params);
192 # save the reclamation
198 flash_later('info', t8('The reclamation has been saved'));
201 if ($::form->{back_to_caller}) {
202 @redirect_params = $::form->{callback} ? ($::form->{callback})
203 : (controller => 'LoginScreen', action => 'user_login');
208 id => $self->reclamation->id,
209 callback => $::form->{callback},
213 $self->redirect_to(@redirect_params);
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,
225 action_bar_additional_submit_values => {
232 # save the reclamation as new document an open it for edit
233 sub action_save_as_new {
236 my $reclamation = $self->reclamation;
238 if (!$reclamation->id) {
239 $self->js->flash('error', t8('This object has not been saved yet.'));
240 return $self->js->render();
243 my $saved_reclamation = SL::DB::Reclamation->new(id => $reclamation->id)->load;
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);
249 if (!$::form->{form_validity_token}) {
250 $::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_RECLAMATION_SAVE())->token;
254 $self->action_save();
257 # print the reclamation
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).
267 $self->js_reset_reclamation_and_item_ids_after_save;
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};
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;
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;
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();
296 my @errors = generate_pdf($self->reclamation, \$pdf, {
298 formname => $formname,
299 language => $self->reclamation->language,
300 printer_id => $printer_id,
301 groupitems => $groupitems,
303 if (scalar @errors) {
304 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
307 if ($media eq 'screen') { # screen/download
308 $self->js->flash('info', t8('The PDF has been created'));
311 type => SL::MIME->mime_type_from_ext($pdf_filename),
312 name => $pdf_filename,
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(
321 $self->js->flash('info', t8('The PDF has been printed'));
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;
329 $self->save_history('PRINTED');
332 ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
336 sub action_preview_pdf {
341 $self->js_reset_reclamation_and_item_ids_after_save;
344 my $media = 'screen';
345 my $formname = $self->type;
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();
358 my @errors = generate_pdf($self->reclamation, \$pdf, {
360 formname => $formname,
361 language => $self->reclamation->language,
363 if (scalar @errors) {
364 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
366 $self->save_history('PREVIEWED');
367 $self->js->flash('info', t8('The PDF has been previewed'));
371 type => SL::MIME->mime_type_from_ext($pdf_filename),
372 name => $pdf_filename,
377 # open the email dialog
378 sub action_save_and_show_email_dialog {
382 $self->js_reset_reclamation_and_item_ids_after_save;
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')
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;
403 ($self->reclamation->contact ? $self->reclamation->contact->cp_email : undef)
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()';
413 my %files = $self->get_files_for_email_dialog();
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 ]) };
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'),
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(),
431 ->run('kivi.Reclamation.show_email_dialog', $dialog_html)
437 sub action_send_email {
444 $self->js->run('kivi.Reclamation.close_email_dialog');
448 my @redirect_params = (
451 id => $self->reclamation->id,
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;
466 # move $::form->{email_form} to $::form
467 my $email_form = delete $::form->{email_form};
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};
474 my %field_names = (to => 'email');
475 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
477 # for Form::cleanup which may be called in Form::send_email
478 $::form->{cwd} = getcwd();
479 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
481 $::form->{$_} = $::form->{print_options}->{$_} for keys %{$::form->{print_options}};
482 $::form->{media} = 'email';
484 $::form->{attachment_policy} //= '';
486 # Is an old file version available?
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},
496 if ( $::form->{attachment_policy} ne 'no_file'
497 && !($::form->{attachment_policy} eq 'old_file' && $attfile)
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},
509 if (scalar @errors) {
510 $::form->error(t8('Generating the document failed: #1', $errors[0]));
513 my @warnings = store_pdf_to_webdav_and_filemanagement(
514 $self->reclamation, $pdf, $::form->{attachment_filename}
516 if (scalar @warnings) {
517 flash_later('warning', $_) for @warnings;
520 my $sfile = SL::SessionFile::Random->new(mode => "w");
521 $sfile->fh->print($pdf);
524 $::form->{tmpfile} = $sfile->file_name;
525 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be
526 # called in Form::send_email
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');
533 flash_later('info', t8('The email has been sent.'));
534 $self->save_history('MAILED');
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(
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});
550 $self->reclamation->update_attributes(intnotes => $intnotes);
553 $self->redirect_to(@redirect_params);
556 sub action_save_and_new_record {
558 my $to_type = $::form->{to_type};
559 my $to_controller = get_object_name_from_type($to_type);
562 flash_later('info', t8('The reclamation has been saved'));
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;
572 controller => $to_controller,
573 action => 'add_from_record',
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},
584 # save the reclamation and redirect to the frontend subroutine for a new
586 sub action_save_and_credit_note {
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();
597 flash_later('info', t8('The reclamation has been saved'));
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},
608 # set form elements in respect to a changed customer or vendor
610 # This action is called on an change of the customer/vendor picker.
611 sub action_customer_vendor_changed {
615 SL::Model::Record->update_after_customer_vendor_change($self->reclamation));
619 if ( $self->reclamation->customervendor->contacts
620 && scalar @{ $self->reclamation->customervendor->contacts } > 0) {
621 $self->js->show('#cp_row');
623 $self->js->hide('#cp_row');
626 if ($self->reclamation->customervendor->shipto
627 && scalar @{ $self->reclamation->customervendor->shipto } > 0) {
628 $self->js->show('#shipto_selection');
630 $self->js->hide('#shipto_selection');
633 $self->js->val( '#reclamation_salesman_id', $self->reclamation->salesman_id) if $self->reclamation->is_sales;
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');
650 $self->js_redisplay_amounts_and_taxes;
651 $self->js_redisplay_cvpartnumbers;
655 # called if a unit in an existing item row is changed
656 sub action_unit_changed {
659 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{reclamation_item_ids} };
660 my $item = $self->reclamation->items_sorted->[$idx];
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));
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;
674 # add an item row for a new item entered in the input row
675 sub action_add_item {
678 delete $::form->{add_item}->{create_part_type};
680 my $form_attr = $::form->{add_item};
682 unless ($form_attr->{parts_id}) {
683 $self->js->flash('error', t8("No part was selected."));
684 return $self->js->render();
688 my $item = new_item($self->reclamation, $form_attr);
690 $self->reclamation->add_items($item);
694 $self->get_item_cvpartnumber($item);
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',
703 if ($::form->{insert_before_item_id}) {
705 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
708 ->append('#row_table_id', $row_as_html);
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,
719 my $item = new_item($self->reclamation, $attr);
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;
724 $self->reclamation->add_items( $item );
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',
733 if ($::form->{insert_before_item_id}) {
735 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
738 ->append('#row_table_id', $row_as_html);
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');
749 $self->js->run('kivi.Reclamation.row_table_scroll_down') if !$::form->{insert_before_item_id};
751 $self->js_redisplay_amounts_and_taxes;
755 # add item rows for multiple items at once
756 sub action_add_multi_items {
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();
766 foreach my $attr (@form_attr) {
767 my $item = new_item($self->reclamation, $attr);
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,
776 my $item = new_item($self->reclamation, $attr);
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;
784 $self->reclamation->add_items(@items);
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',
797 if ($::form->{insert_before_item_id}) {
799 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
802 ->append('#row_table_id', $row_as_html);
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');
812 $self->js->run('kivi.Reclamation.row_table_scroll_down') if !$::form->{insert_before_item_id};
814 $self->js_redisplay_amounts_and_taxes;
818 # recalculate all linetotals, amounts and taxes and redisplay them
819 sub action_recalc_amounts_and_taxes {
824 $self->js_redisplay_line_values;
825 $self->js_redisplay_amounts_and_taxes;
829 sub action_update_exchangerate {
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,
838 $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
841 # redisplay item rows if they are sorted by an attribute
842 sub action_reorder_items {
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} },
857 $self->get_item_cvpartnumber($_) for @{$self->reclamation->items_sorted};
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;
865 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
868 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
869 @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
871 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
875 ->run('kivi.Reclamation.redisplay_items', \@to_sort)
879 # show the popup to choose a price/discount source
880 sub action_price_popup {
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();
890 $self->render_price_dialog($item);
893 # save the reclamation in a session variable and redirect to the part controller
894 sub action_create_part {
897 my $previousform = $::auth->save_form_in_session(non_scalars => 1);
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,
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.'));
907 my @redirect_params = (
908 controller => 'Part',
910 part_type => $::form->{add_item}->{create_part_type},
911 callback => $callback,
915 $self->redirect_to(@redirect_params);
918 sub action_return_from_create_part {
921 $self->{created_part} = SL::DB::Part->new(
922 id => delete $::form->{new_parts_id}
923 )->load if $::form->{new_parts_id};
925 $::auth->restore_form_from_session(delete $::form->{previousform});
926 $self->reclamation($self->init_reclamation);
927 $self->reinit_after_new_reclamation();
929 if ($self->reclamation->id) {
933 title => $self->type_data->text('edit'),
934 %{$self->{template_args}},
941 # load the second row for one or more items
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 {
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];
952 $self->js_load_second_row($item, $item_id, 0);
955 $self->js->run('kivi.Reclamation.init_row_handlers') if $self->reclamation->is_sales; # for lastcosts change-callback
960 # update description, notes and sellprice from master data
961 sub action_update_row_from_master_data {
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];
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));
973 my $texts = get_part_texts($item->part, $self->reclamation->language_id);
975 $item->description($texts->{description});
976 $item->longdescription($texts->{longdescription});
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);
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"]',
989 ->val ('.row_entry:has(#item_' . $item_id
990 . ') [name = "reclamation.reclamation_items[].longdescription"]',
991 $item->longdescription);
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});
1001 $self->js_redisplay_line_values;
1002 $self->js_redisplay_amounts_and_taxes;
1004 $self->js->render();
1007 sub js_load_second_row {
1008 my ($self, $item, $item_id, $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}));
1020 $item->parse_custom_variable_values;
1023 my $row_as_html = $self->p->render('reclamation/tabs/basic_data/_second_row', ITEM => $item, TYPE => $self->type);
1026 ->html('#second_row_' . $item_id, $row_as_html)
1027 ->data('#second_row_' . $item_id, 'loaded', 1);
1030 sub js_redisplay_line_values {
1034 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1035 ]} @{ $self->reclamation->items_sorted };
1038 ->run('kivi.Reclamation.redisplay_line_values', $self->reclamation->is_sales, \@data);
1041 sub js_redisplay_amounts_and_taxes {
1044 if (scalar @{ $self->reclamation->taxes }) {
1045 $self->js->show('#taxincluded_row_id');
1047 $self->js->hide('#taxincluded_row_id');
1050 if ($self->reclamation->taxincluded) {
1051 $self->js->hide('#subtotal_row_id');
1053 $self->js->show('#subtotal_row_id');
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');
1063 sub js_redisplay_cvpartnumbers {
1066 $self->get_item_cvpartnumber($_) for @{$self->reclamation->items_sorted};
1068 my @data = map {[$_->{cvpartnumber}]} @{ $self->reclamation->items_sorted };
1071 ->run('kivi.Reclamation.redisplay_cvpartnumbers', \@data);
1073 sub js_reset_reclamation_and_item_ids_after_save {
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);
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};
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);
1096 $self->js->val('[name="converted_from_record_item_type_refs[+]"]', '');
1097 $self->js->val('[name="converted_from_record_item_ids[+]"]', '');
1104 sub init_valid_types {
1105 $_[0]->type_data->valid_types;
1111 my $type = $self->reclamation->record_type;
1112 if (none { $type eq $_ } @{$self->valid_types}) {
1113 die "Not a valid type for reclamation";
1122 $self->type_data->properties('customervendor');
1125 sub init_search_cvpartnumber {
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()
1133 $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel();
1136 return $search_cvpartnumber;
1139 sub init_show_update_button {
1142 !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1148 SL::Controller::Helper::GetModels->new(
1149 controller => $self,
1152 by => 'record_number',
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'),
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)),
1196 'customer', 'vendor', 'employee', 'salesman',
1197 'contact', 'language', 'department', 'globalproject',
1198 'delivery_term', 'payment', 'currency', 'taxzone',
1207 sub init_reclamation {
1208 $_[0]->make_reclamation;
1211 sub init_all_price_factors {
1212 SL::DB::Manager::PriceFactor->get_all;
1215 sub init_part_picker_classification_ids {
1218 return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => $self->type_data->part_classification_query()) } ];
1223 $::auth->assert($self->type_data->rights('edit'));
1226 # build the selection box for contacts
1228 # Needed, if customer/vendor changed.
1229 sub build_contact_select {
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,
1237 style => 'width: 300px',
1241 # build the selection box for shiptos
1243 # Needed, if customer/vendor changed.
1244 sub build_shipto_select {
1247 select_tag('reclamation.shipto_id',
1248 [ {displayable_id => t8("No/individual shipping address"),
1251 $self->reclamation->customervendor->shipto
1253 value_key => 'shipto_id',
1254 title_key => 'displayable_id',
1255 default => $self->reclamation->shipto_id,
1257 style => 'width: 300px',
1261 # build the inputs for the cusom shipto dialog
1263 # Needed, if customer/vendor changed.
1264 sub build_shipto_inputs {
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');
1273 div_tag($content, id => 'shipto_inputs');
1276 # render the info line for business
1278 # Needed, if customer/vendor changed.
1279 sub build_business_info_row
1281 $_[0]->p->render('reclamation/tabs/basic_data/_business_info_row', SELF => $_[0]);
1284 # build the rows for displaying taxes
1286 # Called if amounts where recalculated and redisplayed.
1287 sub build_tax_rows {
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',
1296 TAXINCLUDED => $self->reclamation->taxincluded,
1299 return $rows_as_html;
1302 sub render_price_dialog {
1303 my ($self, $record_item) = @_;
1305 my $price_source = SL::PriceSource->new(
1306 record_item => $record_item,
1307 record => $self->reclamation,
1312 'kivi.io.price_chooser_dialog',
1313 t8('Available Prices'),
1315 'reclamation/tabs/basic_data/_price_sources_dialog',
1317 price_source => $price_source,
1323 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1324 # $self->js->show('#dialog_flash_error');
1330 # load or create a new reclamation object
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 {
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.
1343 if ($::form->{id}) {
1344 $reclamation = SL::DB::Reclamation->new(id => $::form->{id})->load();
1346 $reclamation = SL::DB::Reclamation->new(
1347 record_type => $::form->{type},
1348 reclamation_items => [],
1349 currency_id => $::instance_conf->get_currency_id(),
1351 $reclamation = SL::Model::Record->update_after_new($reclamation)
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);
1360 # don't assign hashes as objects
1361 my $form_reclamation_items = delete $::form->{reclamation}->{reclamation_items};
1363 $reclamation->assign_attributes(%{$::form->{reclamation}});
1365 # restore form values
1366 $::form->{reclamation}->{reclamation_items} = $form_reclamation_items;
1368 $self->setup_custom_shipto_from_form($reclamation, $::form);
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;
1382 foreach my $form_attr (@{$form_reclamation_items}) {
1383 my $item = make_item($reclamation, $form_attr);
1384 $item->position($pos);
1388 $reclamation->add_items(grep {!$_->id} @items);
1390 return $reclamation;
1393 # create or update items from form
1395 # Make item objects from form values. For items already existing read from db.
1396 # Create a new item else. And assign attributes.
1398 my ($record, $attr) = @_;
1401 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1403 my $is_new = !$item;
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 => []);
1411 $item->assign_attributes(%$attr);
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};
1423 sub load_reclamation {
1426 return if !$::form->{id};
1428 $self->reclamation(SL::DB::Reclamation->new(id => $::form->{id})->load);
1430 $self->reinit_after_new_reclamation();
1432 return $self->reclamation;
1437 # This is used to add one item
1439 my ($record, $attr) = @_;
1441 my $item = SL::DB::ReclamationItem->new;
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 '';
1450 $item->assign_attributes(%$attr);
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;
1456 my ($price_src, $discount_src) = SL::Model::Record->get_best_price_and_discount_source($record, $item, ignore_given => 0);
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;
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} = [];
1476 my $texts = get_part_texts($part, $record->language_id,
1477 description => $new_attr{description},
1478 longdescription => $new_attr{longdescription},
1481 $item->assign_attributes(%new_attr, %{ $texts });
1483 $item->reclamation($record);
1487 # setup custom shipto from form
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) = @_;
1498 if ($reclamation->shipto) {
1499 $self->is_custom_shipto_to_delete(1);
1501 my $custom_shipto = $reclamation->custom_shipto
1502 || $reclamation->custom_shipto(
1503 SL::DB::Shipto->new(module => 'RC', custom_variables => [])
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};
1509 $custom_shipto->assign_attributes(%$shipto_attrs);
1510 $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
1514 # recalculate prices and taxes
1516 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1520 my %pat = $self->reclamation->calculate_prices_and_taxes();
1522 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->reclamation->items_sorted}, @{$pat{items}};
1525 # get data for saving, printing, ..., that is not changed in the form
1527 # Only cvars for now.
1528 sub get_unalterable_data {
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}));
1539 $item->parse_custom_variable_values;
1543 # save the reclamation
1545 # And delete items that are deleted in the form.
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()},
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])
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,
1566 if ($::form->{email_journal_id}) {
1567 my $email_journal = SL::DB::EmailJournal->new(
1568 id => delete $::form->{email_journal_id}
1570 $email_journal->link_to_record_with_attachment(
1572 delete $::form->{email_attachment_id}
1576 delete $::form->{form_validity_token};
1579 sub reinit_after_new_reclamation {
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);
1589 $self->setup_custom_shipto_from_form($self->reclamation, $::form);
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);
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
1598 $item->{render_second_row} = 1;
1601 $self->get_unalterable_data();
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(
1614 id => $self->reclamation->employee_id,
1617 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(
1619 id => $self->reclamation->salesman_id,
1622 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(
1624 id => $self->reclamation->payment_id,
1626 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1627 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1629 $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
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.',
1640 no_opendocument => 0,
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));
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,
1654 my @all_objects = $webdav->get_all_objects;
1655 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1657 link => File::Spec->catfile($_->full_filedescriptor),
1661 $self->get_item_cvpartnumber($_) for @{$self->reclamation->items_sorted};
1663 $::request->{layout}->use_javascript("${_}.js") for
1664 qw(kivi.SalesPurchase kivi.Reclamation kivi.File
1665 calculate_qty kivi.Validator follow_up
1668 $self->_setup_edit_action_bar;
1671 sub prepare_report {
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');
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
1684 my $callback = $self->models->get_callback;
1686 $self->{report} = $report;
1688 # TODO: shipto_id is not linked to custom_shipto
1689 my @columns_order = qw(
1701 transaction_description
1723 my @default_columns = qw(
1729 transaction_description
1741 obj_link => sub {$self->url_for(action => 'edit', id => $_[0]->id, type => $self->type, callback => $callback)},
1742 sub => sub { $_[0]->id },
1745 obj_link => sub {$self->url_for(action => 'edit', id => $_[0]->id, type => $self->type, callback => $callback)},
1746 sub => sub { $_[0]->record_number },
1749 sub => sub { $_[0]->employee ? $_[0]->employee->name : '' },
1752 sub => sub { $_[0]->salesman ? $_[0]->salesman->name : '' },
1755 sub => sub { $_[0]->language ? $_[0]->language->article_code : '' },
1758 sub => sub { $_[0]->department ? $_[0]->department->description : '' },
1761 obj_link => sub { $_[0]->globalproject_id ?
1763 controller => "controller.pl",
1764 action => 'Project/edit',
1765 id => $_[0]->globalproject_id,
1766 callback => $callback
1768 sub => sub { !$_[0]->globalproject ? '' : $_[0]->globalproject->projectnumber },
1770 cv_record_number => {
1771 sub => sub { $_[0]->cv_record_number },
1773 transaction_description => {
1774 sub => sub { $_[0]->transaction_description },
1777 sub => sub { $_[0]->notes },
1780 sub => sub { $_[0]->intnotes },
1783 sub => sub { $_[0]->shippingpoint },
1786 sub => sub { $_[0]->shipvia },
1788 # TODO: custom ship to is not safed in reclamation
1790 # sub => sub { $_[0]->shipto ? $_[0]->shipto->shiptoname : '' },
1793 sub => sub { $_[0]->amount_as_number },
1796 sub => sub { $_[0]->netamount_as_number },
1799 obj_link => sub { $_[0]->delivery_term_id ?
1801 controller => "controller.pl",
1802 action => 'DeliveryTerm/edit',
1803 id => $_[0]->delivery_term_id,
1804 callback => $callback
1806 sub => sub { $_[0]->delivery_term ? $_[0]->delivery_term->description : '' },
1809 obj_link => sub { $_[0]->payment_id ?
1811 controller => "controller.pl",
1812 action => 'PaymentTerm/edit',
1813 id => $_[0]->payment_id,
1814 callback => $callback
1816 sub => sub { $_[0]->payment ? $_[0]->payment->description : '' },
1819 sub => sub { $_[0]->currency ? $_[0]->currency->name : '' },
1822 sub => sub { $_[0]->exchangerate ? $_[0]->exchangerate_as_number : '' },
1825 sub => sub { $_[0]->taxincluded ? t8('Yes') : t8('No') },
1828 obj_link => sub { $_[0]->taxzone_id ?
1830 controller => "controller.pl",
1831 action => 'Taxzones/edit',
1832 id => $_[0]->taxzone_id,
1833 callback => $callback
1835 sub => sub { $_[0]->taxzone ? $_[0]->taxzone->description : '' },
1838 sub => sub { $_[0]->tax_point ? ($_[0]->tax_point)->to_kivitendo(precision => 'day') : '' },
1841 sub => sub { $_[0]->reqdate ? ($_[0]->reqdate)->to_kivitendo(precision => 'day') : '' },
1844 sub => sub { $_[0]->transdate ? ($_[0]->transdate)->to_kivitendo(precision => 'day') : '' },
1847 sub => sub { $_[0]->itime->to_kivitendo(precision => 'minute') }
1850 sub => sub { $_[0]->mtime ? $_[0]->mtime->to_kivitendo(precision => 'minute') : '' }
1853 sub => sub { $_[0]->delivered ? t8('Yes') : t8('No') },
1856 sub => sub { $_[0]->closed ? t8('Yes') : t8('No') },
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 },
1864 $column_defs{contact} = ({
1865 obj_link => sub { $self->url_for(
1866 controller => "controller.pl",
1867 action => 'CustomerVendor/edit',
1869 id => $_[0]->customer_id
1872 sub => sub { $_[0]->contact ? $_[0]->contact->cp_name : '' },
1875 $column_defs{vendor} = ({
1876 raw_data => sub { $_[0]->customervendor->presenter->vendor(display => 'table-cell', callback => $callback) },
1877 sub => sub { $_[0]->customervendor->name },
1879 $column_defs{contact} = ({
1880 obj_link => sub { $self->url_for(
1881 controller => "controller.pl",
1882 action => 'CustomerVendor/edit',
1884 id => $_[0]->vendor_id
1887 sub => sub { $_[0]->contact ? $_[0]->contact->cp_name : '' },
1890 $column_defs{$_}->{text} ||= t8( $self->models->get_sort_spec->{$_}->{title} || $_ ) for keys %column_defs;
1892 unless ($::form->{active_in_report}) {
1893 $::form->{active_in_report}->{$_} = 1 foreach @default_columns;
1895 $self->models->add_additional_url_params(
1896 active_in_report => $::form->{active_in_report});
1897 map { $column_defs{$_}->{visible} = $::form->{active_in_report}->{"$_"} }
1900 ## add cvars TODO: Add own cvars
1901 #my %cvar_column_defs = map {
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,
1908 #} @{ $self->includeable_cvar_configs };
1910 #push @columns, map { 'cvar_' . $_->name } @{ $self->includeable_cvar_configs };
1911 #%column_defs = (%column_defs, %cvar_column_defs);
1913 #my @cvar_column_form_names = ('_include_cvars_from_form', map { "include_cvars_" . $_->name } @{ $self->includeable_cvar_configs });
1916 my @sortable = keys %column_defs;
1918 my $filter_html = SL::Presenter::ReclamationFilter::filter(
1919 $::form->{filter}, $self->type, active_in_report => $::form->{active_in_report}
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',
1929 FILTER_HTML => $filter_html,
1931 raw_bottom_info_text => $self->render(
1932 'reclamation/_report_bottom',
1934 models => $self->models
1936 title => $self->type_data->text('list'),
1937 allow_pdf_export => 1,
1938 allow_csv_export => 1,
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);
1948 sub _setup_edit_action_bar {
1949 my ($self, %params) = @_;
1951 for my $bar ($::request->layout->get('actionbar')) {
1956 call => [ 'kivi.Reclamation.save', {
1958 warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
1959 warn_on_reqdate => $::instance_conf->get_reclamation_warn_no_reqdate,
1962 ['kivi.validate_form','#reclamation_form'],
1966 t8('Save and Close'),
1967 call => [ 'kivi.Reclamation.save', {
1969 warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
1970 warn_on_reqdate => $::instance_conf->get_reclamation_warn_no_reqdate,
1972 { name => 'back_to_caller', value => 1 },
1976 ['kivi.validate_form','#reclamation_form'],
1981 call => [ 'kivi.Reclamation.save', {
1982 action => 'save_as_new',
1983 warn_on_duplicates => $::instance_conf->get_reclamation_warn_duplicate_parts,
1985 disabled => !$self->reclamation->id ? t8('This object has not been saved yet.') : undef,
1987 ], # end of combobox "Save"
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,
2000 { name => 'to_type', value => SALES_RECLAMATION_TYPE() },
2003 only_if => $self->type_data->show_menu('save_and_sales_reclamation'),
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,
2012 { name => 'to_type', value => PURCHASE_RECLAMATION_TYPE() },
2016 only_if => $self->type_data->show_menu('save_and_purchase_reclamation'),
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,
2025 { name => 'to_type',
2026 value => $self->reclamation->is_sales ? SALES_ORDER_TYPE()
2027 : PURCHASE_ORDER_TYPE() },
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,
2038 { name => 'to_type', value => RMA_DELIVERY_ORDER_TYPE() },
2041 only_if => $self->type_data->show_menu('save_and_rma_delivery_order'),
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,
2050 { name => 'to_type', value => SUPPLIER_DELIVERY_ORDER_TYPE() },
2053 only_if => $self->type_data->show_menu('save_and_supplier_delivery_order'),
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,
2062 { name => 'to_type', value => 'credit_note' },
2065 only_if => $self->type_data->show_menu('save_and_credit_note'),
2067 ], # end of combobox "Workflow"
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,
2082 t8('Save and print'),
2084 'kivi.Reclamation.show_print_options',
2085 $::instance_conf->get_reclamation_warn_duplicate_parts,
2086 $::instance_conf->get_reclamation_warn_no_reqdate,
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,
2097 disabled => !$self->reclamation->id ? t8('This object has not been saved yet.') : undef,
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,
2105 ], # end of combobox "Export"
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'),
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),
2127 call => [ 'set_history_window', $self->reclamation->id, 'id' ],
2128 disabled => !$self->reclamation->id ? t8('This record has not been saved yet.') : undef,
2130 ], # end of combobox "more"
2135 sub _setup_search_action_bar {
2136 my ($self, %params) = @_;
2138 for my $bar ($::request->layout->get('actionbar')) {
2142 submit => [ '#search_form', { action => 'Reclamation/list', type => $self->type } ],
2143 accesskey => 'enter',
2147 link => $self->url_for(action => 'add', type => $self->type),
2154 my ($reclamation, $pdf_ref, $params) = @_;
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';
2168 $reclamation->language($params->{language});
2170 # Make reclamation available in template
2171 $print_form->{reclamation} = $reclamation;
2175 my $variable_content_types;
2176 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
2177 $template_ext = 'odt';
2178 $template_type = 'OpenDocument';
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);
2184 $variable_content_types = {
2185 longdescription => 'html',
2187 $::form->get_variable_content_types_for_cvars,
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},
2200 if (!defined $template_file) {
2202 'Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.',
2204 map { "'$_'"} @template_files
2208 return @errors if scalar @errors;
2210 $print_form->throw_on_error(sub {
2212 $print_form->prepare_for_printing;
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,
2222 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
2228 sub get_files_for_email_dialog {
2231 my %files = map { ($_ => []) } qw(versions files cv_files project_files part_files);
2233 return %files if !$::instance_conf->get_doc_storage;
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')
2244 object_id => $self->reclamation->id,
2245 object_type => $self->reclamation->type,
2246 file_type => 'attachment')
2248 $files{cv_files} = [
2250 object_id => $self->reclamation->customervendor->id,
2251 object_type => $self->cv,
2252 file_type => 'attachment')
2254 $files{project_files} = [
2256 object_id => $self->reclamation->globalproject_id,
2257 object_type => 'project',
2258 file_type => 'attachment')
2263 uniq_by { $_->{id} }
2265 +{ id => $_->part->id,
2266 partnumber => $_->part->partnumber }
2267 } @{$self->reclamation->items_sorted};
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;
2274 foreach my $key (keys %files) {
2275 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
2281 sub get_item_cvpartnumber {
2282 my ($self, $item) = @_;
2284 return if !$self->search_cvpartnumber;
2285 return if !$self->reclamation->customervendor;
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;
2296 sub get_part_texts {
2297 my ($part_or_id, $language_or_id, %defaults) = @_;
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;
2302 description => $defaults{description} // $part->description,
2303 longdescription => $defaults{longdescription} // $part->notes,
2306 return $texts unless $language_id;
2308 my $translation = SL::DB::Manager::Translation->get_first(
2310 parts_id => $part->id,
2311 language_id => $language_id,
2314 $texts->{description} = $translation->translation if $translation && $translation->translation;
2315 $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
2321 my ($self, $addition) = @_;
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,
2332 sub store_pdf_to_webdav_and_filemanagement {
2333 my($reclamation, $content, $filename) = @_;
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,
2343 my $webdav_file = SL::Webdav::File->new(
2345 filename => $filename,
2348 $webdav_file->store(data => \$content);
2351 push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
2354 if ($reclamation->id && $::instance_conf->get_doc_storage) {
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);
2365 push @errors, t8('Storing PDF in storage backend failed: #1', $@);
2372 sub init_type_data {
2374 SL::DB::Helper::TypeDataProxy->new('SL::DB::Reclamation', $self->reclamation->record_type);
2385 SL::Controller::Reclamation - controller for reclamations
2389 This is a new form to enter reclamations, written with the use
2390 of controller and java script techniques.
2392 The aim is to provide the user a good experience and a fast workflow.
2400 One input row, so that input happens every time at the same place.
2404 Use of pickers where possible.
2408 Possibility to enter more than one item at once.
2412 Item list in a scrollable area, so that the workflow buttons stay at
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).
2423 No C<update> is necessary. All entries and calculations are managed
2424 with ajax-calls and the page only reloads on C<save>.
2428 User can see changes immediately, because of the use of java script
2433 Parts that are linked though RecordLinks are protected against price editing.
2443 =item * C<SL/Controller/Reclamation.pm>
2447 =item * C<template/webpages/reclamation/form.html>
2451 =item * C<template/webpages/reclamation/tabs/basic_data.html>
2453 Main tab for basic_data.
2455 This is the only tab here for now. "webdav", "documents", "attachements" and
2456 "linked records" tabs are reused from generic code.
2460 =item * C<template/webpages/reclamation/tabs/basic_data/_business_info_row.html>
2462 For displaying information on business type
2464 =item * C<template/webpages/reclamation/tabs/basic_data/_item_input.html>
2466 The input line for items
2468 =item * C<template/webpages/reclamation/tabs/basic_data/_row.html>
2470 One row for already entered items
2472 =item * C<template/webpages/reclamation/tabs/basic_data/_second_row.html>
2474 Foldable second row for already entered items with more fields
2476 =item * C<template/webpages/reclamation/tabs/basic_data/_tax_row.html>
2478 Displaying tax information
2480 =item * C<template/webpages/reclamation/tabs/basic_data/_price_sources_dialog.html>
2482 Dialog for selecting price and discount sources
2486 =item * C<js/kivi.Reclamation.js>
2488 java script functions
2492 =head1 KNOWN BUGS AND CAVEATS
2498 Table header is not sticky in the scrolling area.
2502 Sorting does not include C<position>, neither does reordering.
2504 This behavior was implemented intentionally. But we can discuss, which behavior
2505 should be implemented.
2509 =head1 To discuss / Nice to have
2515 Possibility to select PriceSources in input row?
2519 This controller uses a (changed) copy of the template for the PriceSource
2520 dialog. Maybe there could be used one code source.
2524 A warning when leaving the page without saving unchanged inputs.
2530 Tamino Steinert E<lt>tamino.steinert@tamino.stE<gt>