Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 40c2f024

Von Tamino Steinert vor fast 2 Jahren hinzugefügt

  • ID 40c2f02412b20d0b561905b44d1d91b19292d525
  • Vorgänger 6b3c30a7
  • Nachfolger c1ed1e91

Reclamation: Controller and Templates created

also for ReclamationReason
Changes in SL/DB/Shipto.pm, SL/DB/Helper/TransNumberGenerator.pm,
SL/Controller/File.pm and SL/Webdav.pm for Reclamation
Links in menus added for:
- Reclamation/add
- Reclamation/list
- ReclamationReason/list

Unterschiede anzeigen:

SL/Controller/File.pm
58 58
  'sales_quotation'             => { gen => 1, gltype => '',   dir =>'SalesQuotation',       model => 'Order',          right => 'import_ar'  },
59 59
  'sales_order'                 => { gen => 5, gltype => '',   dir =>'SalesOrder',           model => 'Order',          right => 'import_ar'  },
60 60
  'sales_delivery_order'        => { gen => 1, gltype => '',   dir =>'SalesDeliveryOrder',   model => 'DeliveryOrder',  right => 'import_ar'  },
61
  'sales_reclamation'           => { gen => 5, gltype => '',   dir =>'SalesReclamation',     model => 'Reclamation',    right => 'import_ar'  },
61 62
  'invoice'                     => { gen => 1, gltype => 'ar', dir =>'SalesInvoice',         model => 'Invoice',        right => 'import_ar'  },
62 63
  'invoice_for_advance_payment' => { gen => 1, gltype => 'ar', dir =>'SalesInvoice',         model => 'Invoice',        right => 'import_ar'  },
63 64
  'final_invoice'               => { gen => 1, gltype => 'ar', dir =>'SalesInvoice',         model => 'Invoice',        right => 'import_ar'  },
......
65 66
  'request_quotation'           => { gen => 7, gltype => '',   dir =>'RequestForQuotation',  model => 'Order',          right => 'import_ap'  },
66 67
  'purchase_order'              => { gen => 7, gltype => '',   dir =>'PurchaseOrder',        model => 'Order',          right => 'import_ap'  },
67 68
  'purchase_delivery_order'     => { gen => 7, gltype => '',   dir =>'PurchaseDeliveryOrder',model => 'DeliveryOrder',  right => 'import_ap'  },
69
  'purchase_reclamation'        => { gen => 7, gltype => '',   dir =>'PurchaseReclamation',  model => 'Reclamation',    right => 'import_ap'  },
68 70
  'purchase_invoice'            => { gen => 6, gltype => 'ap', dir =>'PurchaseInvoice',      model => 'PurchaseInvoice',right => 'import_ap'  },
69 71
  'vendor'                      => { gen => 0, gltype => '',   dir =>'Vendor',               model => 'Vendor',         right => 'xx'         },
70 72
  'customer'                    => { gen => 1, gltype => '',   dir =>'Customer',             model => 'Customer',       right => 'xx'         },
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::Presenter::Tag qw(select_tag hidden_tag div_tag);
8
use SL::Locale::String qw(t8);
9
use SL::SessionFile::Random;
10
use SL::PriceSource;
11
use SL::ReportGenerator;
12
use SL::Controller::Helper::ReportGenerator;
13
use SL::Webdav;
14
use SL::File;
15
use SL::MIME;
16
use SL::Util qw(trim);
17
use SL::YAML;
18
use SL::DB::History;
19
use SL::DB::Reclamation;
20
use SL::DB::ReclamationItem;
21
use SL::DB::Default;
22
use SL::DB::Printer;
23
use SL::DB::Language;
24
use SL::DB::RecordLink;
25
use SL::DB::Shipto;
26
use SL::DB::Translation;
27

  
28
use SL::Helper::CreatePDF qw(:all);
29
use SL::Helper::PrintOptions;
30
use SL::Helper::ShippedQty;
31
use SL::Helper::UserPreferences::PositionsScrollbar;
32
use SL::Helper::UserPreferences::UpdatePositions;
33

  
34
use SL::Controller::Helper::GetModels;
35

  
36
use List::Util qw(first sum0);
37
use List::UtilsBy qw(sort_by uniq_by);
38
use List::MoreUtils qw(any none pairwise first_index);
39
use English qw(-no_match_vars);
40
use File::Spec;
41
use Cwd;
42
use Sort::Naturally;
43

  
44
# for _link_to_records
45
use SL::DB::Order;
46
use SL::DB::OrderItem;
47
use SL::DB::DeliveryOrder;
48
use SL::DB::DeliveryOrderItem;
49
use SL::DB::Invoice;
50
use SL::DB::PurchaseInvoice;
51
use SL::DB::InvoiceItem;
52

  
53
use Rose::Object::MakeMethods::Generic
54
(
55
 scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
56
 'scalar --get_set_init' => [qw(
57
    all_price_factors cv models p part_picker_classification_ids reclamation
58
    search_cvpartnumber show_update_button type valid_types
59
 )],
60
);
61

  
62

  
63
# safety
64
__PACKAGE__->run_before('check_auth');
65

  
66
__PACKAGE__->run_before('recalc',
67
                        only => [qw(
68
                          save save_as_new print preview_pdf send_email
69
                          save_and_show_email_dialog
70
                          workflow_save_and_sales_or_purchase_reclamation
71
                       )]);
72

  
73
__PACKAGE__->run_before('get_unalterable_data',
74
                        only => [qw(
75
                          save save_as_new print preview_pdf send_email
76
                          save_and_show_email_dialog
77
                          workflow_save_and_sales_or_purchase_reclamation
78
                        )]);
79

  
80
#
81
# actions
82
#
83

  
84
# add a new reclamation
85
sub action_add {
86
  my ($self) = @_;
87

  
88
  $self->reclamation->transdate(DateTime->now_local());
89

  
90
  $self->pre_render();
91

  
92
  $self->render(
93
    'reclamation/form',
94
    title => $self->get_title_for('add'),
95
    %{$self->{template_args}},
96
  );
97
}
98

  
99
# edit an existing reclamation
100
sub action_edit {
101
  my ($self) = @_;
102

  
103
  unless ($::form->{id}) {
104
    $self->js->flash('error', t8("Can't edit unsaved reclamation. No 'id' was given."));
105
    return $self->js->render();
106
  }
107

  
108
  $self->load_reclamation;
109

  
110
  $self->recalc();
111
  $self->pre_render();
112

  
113
  $self->render(
114
    'reclamation/form',
115
    title => $self->get_title_for('edit'),
116
    %{$self->{template_args}},
117
  );
118
}
119

  
120
# delete the reclamation
121
sub action_delete {
122
  my ($self) = @_;
123

  
124
  my $errors = $self->delete();
125

  
126
  if (scalar @{ $errors }) {
127
    $self->js->flash('error', $_) foreach @{ $errors };
128
    return $self->js->render();
129
  }
130

  
131
  flash_later('info', t8('The reclamation has been deleted'));
132

  
133
  my @redirect_params = (
134
    action => 'add',
135
    type   => $self->type,
136
  );
137

  
138
  $self->redirect_to(@redirect_params);
139
}
140

  
141
# save the reclamation
142
sub action_save {
143
  my ($self) = @_;
144

  
145
  my $errors = $self->save();
146
  if (scalar @{ $errors }) {
147
    $self->js->flash('error', $_) foreach @{ $errors };
148
    return $self->js->render();
149
  }
150
  flash_later('info', t8('The reclamation has been saved'));
151

  
152
  my @redirect_params = (
153
    action => 'edit',
154
    type   => $self->type,
155
    id     => $self->reclamation->id,
156
  );
157
  $self->redirect_to(@redirect_params);
158
}
159

  
160
sub action_list {
161
  my ($self) = @_;
162

  
163
  $self->_setup_search_action_bar;
164
  $self->prepare_report;
165
  $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
166
}
167

  
168
# save the reclamation as new document an open it for edit
169
sub action_save_as_new {
170
  my ($self) = @_;
171

  
172
  my $reclamation = $self->reclamation;
173

  
174
  if (!$reclamation->id) {
175
    $self->js->flash('error', t8('This object has not been saved yet.'));
176
    return $self->js->render();
177
  }
178

  
179
  # load reclamation from db to check if values changed
180
  my $saved_reclamation = SL::DB::Reclamation->new(id => $reclamation->id)->load;
181

  
182
  my %new_attrs;
183
  # If it has been changed manually then use it as-is, otherwise change.
184
  $new_attrs{record_number} = (trim($reclamation->record_number) eq $saved_reclamation->record_number)
185
                            ? ''
186
                            : trim($reclamation->record_number);
187
  $new_attrs{transdate} = ($reclamation->transdate == $saved_reclamation->transdate)
188
                        ? DateTime->today_local
189
                        : $reclamation->transdate;
190
  if ($reclamation->reqdate == $saved_reclamation->reqdate) {
191
    $new_attrs{reqdate} = '';
192
  } else {
193
    $new_attrs{reqdate} = $reclamation->reqdate;
194
  }
195

  
196
  # Update employee
197
  $new_attrs{employee}  = SL::DB::Manager::Employee->current;
198

  
199
  # Create new record from current one
200
  $self->reclamation(
201
           SL::DB::Reclamation->new_from(
202
             $reclamation,
203
             destination_type   => $reclamation->type,
204
             attributes         => \%new_attrs,
205
             no_linked_records => 1,
206
           )
207
         );
208

  
209
  # save
210
  $self->action_save();
211
}
212

  
213
# print the reclamation
214
#
215
# This is called if "print" is pressed in the print dialog.
216
# If PDF creation was requested and succeeded, the pdf is offered for download
217
# via send_file (which uses ajax in this case).
218
sub action_print {
219
  my ($self) = @_;
220

  
221
  $self->save_with_render_error();
222

  
223
  $self->js_reset_reclamation_and_item_ids_after_save;
224

  
225
  my $format      = $::form->{print_options}->{format};
226
  my $media       = $::form->{print_options}->{media};
227
  my $formname    = $::form->{print_options}->{formname};
228
  my $copies      = $::form->{print_options}->{copies};
229
  my $groupitems  = $::form->{print_options}->{groupitems};
230
  my $printer_id  = $::form->{print_options}->{printer_id};
231

  
232
  # only pdf and opendocument by now
233
  if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
234
    return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
235
  }
236

  
237
  # only screen or printer by now
238
  if (none { $media eq $_ } qw(screen printer)) {
239
    return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
240
  }
241

  
242
  # create a form for generate_attachment_filename
243
  my $form   = Form->new;
244
  $form->{record_number} = $self->reclamation->record_number;
245
  $form->{type}          = $self->type;
246
  $form->{format}        = $format;
247
  $form->{formname}      = $formname;
248
  $form->{language}      = '_' . $self->reclamation->language->template_code if $self->reclamation->language;
249
  my $pdf_filename       = $form->generate_attachment_filename();
250

  
251
  my $pdf;
252
  my @errors = generate_pdf($self->reclamation, \$pdf, {
253
                              format     => $format,
254
                              formname   => $formname,
255
                              language   => $self->reclamation->language,
256
                              printer_id => $printer_id,
257
                              groupitems => $groupitems,
258
                            });
259
  if (scalar @errors) {
260
    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
261
  }
262

  
263
  if ($media eq 'screen') { # screen/download
264
    $self->js->flash('info', t8('The PDF has been created'));
265
    $self->send_file(
266
      \$pdf,
267
      type         => SL::MIME->mime_type_from_ext($pdf_filename),
268
      name         => $pdf_filename,
269
      js_no_render => 1,
270
    );
271
  } elsif ($media eq 'printer') { # printer
272
    my $printer_id = $::form->{print_options}->{printer_id};
273
    SL::DB::Printer->new(id => $printer_id)->load->print_document(
274
      copies  => $copies,
275
      content => $pdf,
276
    );
277
    $self->js->flash('info', t8('The PDF has been printed'));
278
  }
279

  
280
  my @warnings = store_pdf_to_webdav_and_filemanagement($self->reclamation, $pdf, $pdf_filename);
281
  if (scalar @warnings) {
282
    $self->js->flash('warning', $_) for @warnings;
283
  }
284

  
285
  $self->save_history('PRINTED');
286

  
287
  $self->js
288
    ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
289
    ->render;
290
}
291

  
292
sub action_preview_pdf {
293
  my ($self) = @_;
294

  
295
  $self->save_with_render_error();
296

  
297
  $self->js_reset_reclamation_and_item_ids_after_save;
298

  
299
  my $format      = 'pdf';
300
  my $media       = 'screen';
301
  my $formname    = $self->type;
302

  
303
  # only pdf
304
  # create a form for generate_attachment_filename
305
  my $form   = Form->new;
306
  $form->{record_number} = $self->reclamation->record_number;
307
  $form->{type}          = $self->type;
308
  $form->{format}        = $format;
309
  $form->{formname}      = $formname;
310
  $form->{language}      = '_' . $self->reclamation->language->template_code if $self->reclamation->language;
311
  my $pdf_filename       = $form->generate_attachment_filename();
312

  
313
  my $pdf;
314
  my @errors = generate_pdf($self->reclamation, \$pdf, {
315
                             format     => $format,
316
                             formname   => $formname,
317
                             language   => $self->reclamation->language,
318
                           });
319
  if (scalar @errors) {
320
    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
321
  }
322
  $self->save_history('PREVIEWED');
323
  $self->js->flash('info', t8('The PDF has been previewed'));
324
  # screen/download
325
  $self->send_file(
326
    \$pdf,
327
    type         => SL::MIME->mime_type_from_ext($pdf_filename),
328
    name         => $pdf_filename,
329
    js_no_render => 0,
330
  );
331
}
332

  
333
# open the email dialog
334
sub action_save_and_show_email_dialog {
335
  my ($self) = @_;
336

  
337
  $self->save_with_render_error();
338

  
339
  unless ($self->reclamation->customervendor) {
340
    return $self->js->flash('error',
341
                            $self->cv eq 'customer' ?
342
                                t8('Cannot send E-mail without customer given')
343
                              : t8('Cannot send E-mail without vendor given'))
344
                    ->render($self);
345
  }
346

  
347
  my $form = Form->new;
348
  $form->{record_number}    = $self->reclamation->record_number;
349
  $form->{cv_record_number} = $self->reclamation->cv_record_number;
350
  $form->{formname}         = $self->type;
351
  $form->{type}             = $self->type;
352
  $form->{language}         = '_' . $self->reclamation->language->template_code if $self->reclamation->language;
353
  $form->{language_id}      = $self->reclamation->language->id                  if $self->reclamation->language;
354
  $form->{format}           = 'pdf';
355
  $form->{cp_id}            = $self->reclamation->contact->cp_id if $self->reclamation->contact;
356

  
357
  my $email_form;
358
  $email_form->{to}   = $self->reclamation->contact->cp_email if $self->reclamation->contact;
359
  $email_form->{to} ||= $self->reclamation->customervendor->email;
360
  $email_form->{cc}   = $self->reclamation->customervendor->cc;
361
  $email_form->{bcc}  = join ', ', grep $_, $self->reclamation->customervendor->bcc, SL::DB::Default->get->global_bcc;
362
  # TODO: get addresses from shipto, if any
363
  $email_form->{subject}             = $form->generate_email_subject();
364
  $email_form->{attachment_filename} = $form->generate_attachment_filename();
365
  $email_form->{message}             = $form->generate_email_body();
366
  $email_form->{js_send_function}    = 'kivi.Reclamation.send_email()';
367

  
368
  my %files = $self->get_files_for_email_dialog();
369
  $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
370
  my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
371
                                  email_form  => $email_form,
372
                                  show_bcc    => $::auth->assert('email_bcc', 'may fail'),
373
                                  FILES       => \%files,
374
                                  is_customer => $self->cv eq 'customer',
375
                                  ALL_EMPLOYEES => $self->{all_employees},
376
  );
377

  
378
  $self->js
379
      ->run('kivi.Reclamation.show_email_dialog', $dialog_html)
380
      ->reinit_widgets
381
      ->render($self);
382
}
383

  
384
# send email
385
#
386
# TODO: handling error messages: flash is not displayed in dialog, but in the main form
387
sub action_send_email {
388
  my ($self) = @_;
389

  
390
  my $errors = $self->save();
391

  
392
  if (scalar @{ $errors }) {
393
    $self->js->run('kivi.Reclamation.close_email_dialog');
394
    $self->js->flash('error', $_) foreach @{ $errors };
395
    return $self->js->render();
396
  }
397

  
398
  $self->js_reset_reclamation_and_item_ids_after_save;
399

  
400
  # move $::form->{email_form} to $::form
401
  my $email_form  = delete $::form->{email_form};
402
  my %field_names = (to => 'email');
403
  $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
404

  
405
  # for Form::cleanup which may be called in Form::send_email
406
  $::form->{cwd}    = getcwd();
407
  $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
408

  
409
  $::form->{$_}     = $::form->{print_options}->{$_} for keys %{$::form->{print_options}};
410
  $::form->{media}  = 'email';
411

  
412
  if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
413
    my $pdf;
414
    my @errors = generate_pdf($self->reclamation, \$pdf, {
415
                               media      => $::form->{media},
416
                               format     => $::form->{print_options}->{format},
417
                               formname   => $::form->{print_options}->{formname},
418
                               language   => $self->reclamation->language,
419
                               printer_id => $::form->{print_options}->{printer_id},
420
                               groupitems => $::form->{print_options}->{groupitems}
421
                             });
422
    if (scalar @errors) {
423
      return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
424
    }
425

  
426
    my @warnings = store_pdf_to_webdav_and_filemanagement($self->reclamation, $pdf, $::form->{attachment_filename});
427
    if (scalar @warnings) {
428
      flash_later('warning', $_) for @warnings;
429
    }
430

  
431
    my $sfile = SL::SessionFile::Random->new(mode => "w");
432
    $sfile->fh->print($pdf);
433
    $sfile->fh->close;
434

  
435
    $::form->{tmpfile} = $sfile->file_name;
436
    $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
437
  }
438

  
439
  $::form->{id} = $self->reclamation->id; # this is used in SL::Mailer to create a linked record to the mail
440
  $::form->send_email(\%::myconfig, 'pdf');
441

  
442
  # internal notes
443
  my $intnotes = $self->reclamation->intnotes;
444
  $intnotes   .= "\n\n" if $self->reclamation->intnotes;
445
  $intnotes   .= t8('[email]')                                       . "\n";
446
  $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(
447
                                             DateTime->now_local,
448
                                             precision => 'seconds') . "\n";
449
  $intnotes   .= t8('To (email)') . ": " . $::form->{email}          . "\n";
450
  $intnotes   .= t8('Cc')         . ": " . $::form->{cc}             . "\n"    if $::form->{cc};
451
  $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}            . "\n"    if $::form->{bcc};
452
  $intnotes   .= t8('Subject')    . ": " . $::form->{subject}        . "\n\n";
453
  $intnotes   .= t8('Message')    . ": " . $::form->{message};
454

  
455
  $self->reclamation->update_attributes(intnotes => $intnotes);
456

  
457
  $self->save_history('MAILED');
458

  
459
  flash_later('info', t8('The email has been sent.'));
460

  
461
  my @redirect_params = (
462
    action => 'edit',
463
    type   => $self->type,
464
    id     => $self->reclamation->id,
465
  );
466

  
467
  $self->redirect_to(@redirect_params);
468
}
469

  
470
# workflow from purchase to sales reclamation
471
sub action_save_and_sales_reclamation {
472
  $_[0]->workflow_save_and_sales_or_purchase_reclamation();
473
}
474

  
475
# workflow from sales to purchase reclamation
476
sub action_save_and_purchase_reclamation {
477
  $_[0]->workflow_save_and_sales_or_purchase_reclamation();
478
}
479

  
480
# set form elements in respect to a changed customer or vendor
481
#
482
# This action is called on an change of the customer/vendor picker.
483
sub action_customer_vendor_changed {
484
  my ($self) = @_;
485

  
486
  $self->setup_reclamation_from_cv($self->reclamation);
487
  $self->recalc();
488

  
489
  my $cv_method = $self->cv;
490

  
491
  if ( $self->reclamation->customervendor->contacts
492
       && scalar @{ $self->reclamation->customervendor->contacts } > 0) {
493
    $self->js->show('#cp_row');
494
  } else {
495
    $self->js->hide('#cp_row');
496
  }
497

  
498
  if ($self->reclamation->customervendor->shipto
499
      && scalar @{ $self->reclamation->customervendor->shipto } > 0) {
500
    $self->js->show('#shipto_selection');
501
  } else {
502
    $self->js->hide('#shipto_selection');
503
  }
504

  
505
  $self->js->val( '#reclamation_salesman_id', $self->reclamation->salesman_id) if $self->reclamation->is_sales;
506

  
507
  $self->js
508
    ->replaceWith('#reclamation_cp_id',            $self->build_contact_select)
509
    ->replaceWith('#reclamation_shipto_id',        $self->build_shipto_select)
510
    ->replaceWith('#shipto_inputs  ',              $self->build_shipto_inputs)
511
    ->replaceWith('#business_info_row',            $self->build_business_info_row)
512
    ->val(        '#reclamation_taxzone_id',       $self->reclamation->taxzone_id)
513
    ->val(        '#reclamation_taxincluded',      $self->reclamation->taxincluded)
514
    ->val(        '#reclamation_currency_id',      $self->reclamation->currency_id)
515
    ->val(        '#reclamation_payment_id',       $self->reclamation->payment_id)
516
    ->val(        '#reclamation_delivery_term_id', $self->reclamation->delivery_term_id)
517
    ->val(        '#reclamation_intnotes',         $self->reclamation->intnotes)
518
    ->val(        '#reclamation_language_id',      $self->reclamation->customervendor->language_id)
519
    ->focus(      '#reclamation_' . $self->cv . '_id')
520
    ->run('kivi.Reclamation.update_exchangerate');
521

  
522
  $self->js_redisplay_amounts_and_taxes;
523
  $self->js_redisplay_cvpartnumbers;
524
  $self->js->render();
525
}
526

  
527
# open the dialog for customer/vendor details
528
sub action_show_customer_vendor_details_dialog {
529
  my ($self) = @_;
530

  
531
  my $is_sales = ($self->cv eq 'customer');
532
  my $cv;
533
  if ($is_sales) {
534
    $cv = SL::DB::Customer->new(id => $::form->{cv_id})->load;
535
  } else {
536
    $cv = SL::DB::Vendor->new(id => $::form->{cv_id})->load;
537
  }
538

  
539
  my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
540
  $details{discount_as_percent} = $cv->discount_as_percent;
541
  $details{creditlimt}          = $cv->creditlimit_as_number;
542
  $details{business}            = $cv->business->description      if $cv->business;
543
  $details{language}            = $cv->language_obj->description  if $cv->language_obj;
544
  $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
545
  $details{payment_terms}       = $cv->payment->description       if $cv->payment;
546
  $details{pricegroup}          = $cv->pricegroup->pricegroup     if !$is_sales && $cv->pricegroup;
547

  
548
  foreach my $entry (@{ $cv->shipto }) {
549
    push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
550
  }
551
  foreach my $entry (@{ $cv->contacts }) {
552
    push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
553
  }
554

  
555
  $_[0]->render('common/show_vc_details', { layout => 0 },
556
                is_customer => !$is_sales,
557
                %details);
558
}
559

  
560
# called if a unit in an existing item row is changed
561
sub action_unit_changed {
562
  my ($self) = @_;
563

  
564
  my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{reclamationitem_ids} };
565
  my $item = $self->reclamation->items_sorted->[$idx];
566

  
567
  my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
568
  $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
569

  
570
  $self->recalc();
571

  
572
  $self->js
573
    ->run('kivi.Reclamation.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
574
  $self->js_redisplay_line_values;
575
  $self->js_redisplay_amounts_and_taxes;
576
  $self->js->render();
577
}
578

  
579
# add an item row for a new item entered in the input row
580
sub action_add_item {
581
  my ($self) = @_;
582

  
583
  delete $::form->{add_item}->{create_part_type};
584

  
585
  my $form_attr = $::form->{add_item};
586

  
587
  unless ($form_attr->{parts_id}) {
588
    $self->js->flash('error', t8("No part was selected."));
589
    return $self->js->render();
590
  }
591

  
592

  
593
  my $item = new_item($self->reclamation, $form_attr);
594

  
595
  $self->reclamation->add_items($item);
596

  
597
  $self->recalc();
598

  
599
  $self->get_item_cvpartnumber($item);
600

  
601
  my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
602
  my $row_as_html = $self->p->render('reclamation/tabs/basic_data/_row',
603
                                     ITEM => $item,
604
                                     ID   => $item_id,
605
                                     SELF => $self,
606
  );
607

  
608
  if ($::form->{insert_before_item_id}) {
609
    $self->js
610
      ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
611
  } else {
612
    $self->js
613
      ->append('#row_table_id', $row_as_html);
614
  }
615

  
616
  if ( $item->part->is_assortment ) {
617
    $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
618
    foreach my $assortment_item ( @{$item->part->assortment_items} ) {
619
      my $attr = { parts_id => $assortment_item->parts_id,
620
                   qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
621
                   unit     => $assortment_item->unit,
622
                   description => $assortment_item->part->description,
623
                 };
624
      my $item = new_item($self->reclamation, $attr);
625

  
626
      # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
627
      $item->discount(1) unless $assortment_item->charge;
628

  
629
      $self->reclamation->add_items( $item );
630
      $self->recalc();
631
      $self->get_item_cvpartnumber($item);
632
      my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
633
      my $row_as_html = $self->p->render('reclamation/tabs/basic_data/_row',
634
                                         ITEM => $item,
635
                                         ID   => $item_id,
636
                                         SELF => $self,
637
      );
638
      if ($::form->{insert_before_item_id}) {
639
        $self->js
640
          ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
641
      } else {
642
        $self->js
643
          ->append('#row_table_id', $row_as_html);
644
      }
645
    };
646
  };
647

  
648
  $self->js
649
    ->val('.add_item_input', '')
650
    ->run('kivi.Reclamation.init_row_handlers')
651
    ->run('kivi.Reclamation.renumber_positions')
652
    ->focus('#add_item_parts_id_name');
653

  
654
  $self->js->run('kivi.Reclamation.row_table_scroll_down') if !$::form->{insert_before_item_id};
655

  
656
  $self->js_redisplay_amounts_and_taxes;
657
  $self->js->render();
658
}
659

  
660
# add item rows for multiple items at once
661
sub action_add_multi_items {
662
  my ($self) = @_;
663

  
664
  my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
665
  unless (scalar(@form_attr)) {
666
    $self->js->flash('error', t8("No part was selected."));
667
    return $self->js->render();
668
  }
669

  
670
  my @items;
671
  foreach my $attr (@form_attr) {
672
    my $item = new_item($self->reclamation, $attr);
673
    push @items, $item;
674
    if ( $item->part->is_assortment ) {
675
      foreach my $assortment_item ( @{$item->part->assortment_items} ) {
676
        my $attr = { parts_id => $assortment_item->parts_id,
677
                     qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
678
                     unit     => $assortment_item->unit,
679
                     description => $assortment_item->part->description,
680
                   };
681
        my $item = new_item($self->reclamation, $attr);
682

  
683
        # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
684
        $item->discount(1) unless $assortment_item->charge;
685
        push @items, $item;
686
      }
687
    }
688
  }
689
  $self->reclamation->add_items(@items);
690

  
691
  $self->recalc();
692

  
693
  foreach my $item (@items) {
694
    $self->get_item_cvpartnumber($item);
695
    my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
696
    my $row_as_html = $self->p->render('reclamation/tabs/basic_data/_row',
697
                                       ITEM => $item,
698
                                       ID   => $item_id,
699
                                       SELF => $self,
700
    );
701

  
702
    if ($::form->{insert_before_item_id}) {
703
      $self->js
704
        ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
705
    } else {
706
      $self->js
707
        ->append('#row_table_id', $row_as_html);
708
    }
709
  }
710

  
711
  $self->js
712
    ->run('kivi.Part.close_picker_dialogs')
713
    ->run('kivi.Reclamation.init_row_handlers')
714
    ->run('kivi.Reclamation.renumber_positions')
715
    ->focus('#add_item_parts_id_name');
716

  
717
  $self->js->run('kivi.Reclamation.row_table_scroll_down') if !$::form->{insert_before_item_id};
718

  
719
  $self->js_redisplay_amounts_and_taxes;
720
  $self->js->render();
721
}
722

  
723
# recalculate all linetotals, amounts and taxes and redisplay them
724
sub action_recalc_amounts_and_taxes {
725
  my ($self) = @_;
726

  
727
  $self->recalc();
728

  
729
  $self->js_redisplay_line_values;
730
  $self->js_redisplay_amounts_and_taxes;
731
  $self->js->render();
732
}
733

  
734
sub action_update_exchangerate {
735
  my ($self) = @_;
736

  
737
  my $data = {
738
    is_standard   => $self->reclamation->currency_id == $::instance_conf->get_currency_id,
739
    currency_name => $self->reclamation->currency->name,
740
    exchangerate  => $self->reclamation->daily_exchangerate_as_null_number,
741
  };
742

  
743
  $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
744
}
745

  
746
# redisplay item rows if they are sorted by an attribute
747
sub action_reorder_items {
748
  my ($self) = @_;
749

  
750
  my %sort_keys = (
751
    partnumber   => sub { $_[0]->part->partnumber },
752
    description  => sub { $_[0]->description },
753
    reason       => sub { $_[0]->reason eq undef ? "" : $_[0]->reason->name },
754
    reason_description_ext => sub { $_[0]->reason_description_ext },
755
    reason_description_int => sub { $_[0]->reason_description_int },
756
    shipped_qty  => sub { $_[0]->shipped_qty },
757
    qty          => sub { $_[0]->qty },
758
    sellprice    => sub { $_[0]->sellprice },
759
    discount     => sub { $_[0]->discount },
760
    cvpartnumber => sub { $_[0]->{cvpartnumber} },
761
  );
762

  
763
  $self->get_item_cvpartnumber($_) for @{$self->reclamation->items_sorted};
764

  
765
  if ($::form->{order_by} eq 'shipped_qty') {
766
    # Calculate shipped qtys here to prevent calling calculate for every item
767
    # via the items method. Do not use write_to_objects to prevent
768
    # reclamation->delivered to be set, because this should be the value from
769
    # db, which can be set manually or is set when linked delivery orders are
770
    # saved.
771
    SL::Helper::ShippedQty->new->calculate($self->reclamation)->write_to(\@{$self->reclamation->items});
772
  }
773

  
774
  my $method = $sort_keys{$::form->{order_by}};
775
  my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->reclamation->items_sorted };
776
  if ($::form->{sort_dir}) {
777
    if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
778
      @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
779
    } else {
780
      @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
781
    }
782
  } else {
783
    if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
784
      @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
785
    } else {
786
      @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
787
    }
788
  }
789
  $self->js
790
    ->run('kivi.Reclamation.redisplay_items', \@to_sort)
791
    ->render;
792
}
793

  
794
# show the popup to choose a price/discount source
795
sub action_price_popup {
796
  my ($self) = @_;
797

  
798
  my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{reclamation_items_ids} };
799
  my $item = $self->reclamation->items_sorted->[$idx];
800
  if ($item->is_linked_to_record) {
801
    $self->js->flash('error', t8("Can't change price of a linked item"));
802
    return $self->js->render();
803
  }
804

  
805
  $self->render_price_dialog($item);
806
}
807

  
808
# save the reclamation in a session variable and redirect to the part controller
809
sub action_create_part {
810
  my ($self) = @_;
811

  
812
  my $previousform = $::auth->save_form_in_session(non_scalars => 1);
813

  
814
  my $callback     = $self->url_for(
815
    action       => 'return_from_create_part',
816
    type         => $self->type, # type is needed for check_auth on return
817
    previousform => $previousform,
818
  );
819

  
820
  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.'));
821

  
822
  my @redirect_params = (
823
    controller => 'Part',
824
    action     => 'add',
825
    part_type  => $::form->{add_item}->{create_part_type},
826
    callback   => $callback,
827
    show_abort => 1,
828
  );
829

  
830
  $self->redirect_to(@redirect_params);
831
}
832

  
833
sub action_return_from_create_part {
834
  my ($self) = @_;
835

  
836
  $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
837

  
838
  $::auth->restore_form_from_session(delete $::form->{previousform});
839

  
840
  # set item ids to new fake id, to identify them as new items
841
  foreach my $item (@{$self->reclamation->items_sorted}) {
842
    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
843
  }
844

  
845
  $self->recalc();
846
  $self->get_unalterable_data();
847
  $self->pre_render();
848

  
849
  # trigger rendering values for second row/longdescription as hidden, because
850
  # they are loaded only on demand. So we need to keep the values from the
851
  # source.
852
  $_->{render_second_row}      = 1 for @{ $self->reclamation->items_sorted };
853
  $_->{render_longdescription} = 1 for @{ $self->reclamation->items_sorted };
854

  
855
  $self->render(
856
    'reclamation/form',
857
    title => $self->get_title_for('edit'),
858
    %{$self->{template_args}}
859
  );
860
}
861

  
862
# load the second row for one or more items
863
#
864
# This action gets the html code for all items second rows by rendering a template for
865
# the second row and sets the html code via client js.
866
sub action_load_second_rows {
867
  my ($self) = @_;
868

  
869
  foreach my $item_id (@{ $::form->{reclamation_items_ids} }) {
870
    my $idx  = first_index { $_ eq $item_id } @{ $::form->{reclamation_items_ids} };
871
    my $item = $self->reclamation->items_sorted->[$idx];
872

  
873
    $self->js_load_second_row($item, $item_id, 0);
874
  }
875

  
876
  $self->js->run('kivi.Reclamation.init_row_handlers') if $self->reclamation->is_sales; # for lastcosts change-callback
877

  
878
  $self->js->render();
879
}
880

  
881
# update description, notes and sellprice from master data
882
sub action_update_row_from_master_data {
883
  my ($self) = @_;
884

  
885
  foreach my $item_id (@{ $::form->{item_ids} }) {
886
    my $idx   = first_index { $_ eq $item_id } @{ $::form->{reclamationitem_ids} };
887
    my $item  = $self->reclamation->items_sorted->[$idx];
888

  
889
    if ($item->is_linked_to_record) {
890
      $self->js->flash_later('error', t8("Can't change data of a linked item. Part: " . $item->part->partnumber));
891
      next;
892
    }
893

  
894
    my $texts = get_part_texts($item->part, $self->reclamation->language_id);
895

  
896
    $item->description($texts->{description});
897
    $item->longdescription($texts->{longdescription});
898

  
899
    my $price_source = SL::PriceSource->new(record_item => $item, record => $self->reclamation);
900

  
901
    my $price_src;
902
    if ($item->part->is_assortment) {
903
    # add assortment items with price 0, as the components carry the price
904
      $price_src = $price_source->price_from_source("");
905
      $price_src->price(0);
906
    } else {
907
      $price_src = $price_source->best_price
908
                 ? $price_source->best_price
909
                 : $price_source->price_from_source("");
910
      $price_src->price($::form->round_amount($price_src->price / $self->reclamation->exchangerate, 5)) if $self->reclamation->exchangerate;
911
      $price_src->price(0) if !$price_source->best_price;
912
    }
913

  
914
    $item->sellprice($price_src->price);
915
    $item->active_price_source($price_src);
916

  
917
    $self->js
918
      ->run('kivi.Reclamation.update_sellprice', $item_id, $item->sellprice_as_number)
919
      ->html('.row_entry:has(#item_' . $item_id
920
             . ') [name = "partnumber"] a', $item->part->partnumber)
921
      ->val ('.row_entry:has(#item_' . $item_id
922
             . ') [name = "reclamation.reclamation_items[].description"]',
923
             $item->description)
924
      ->val ('.row_entry:has(#item_' . $item_id
925
             . ') [name = "reclamation.reclamation_items[].longdescription"]',
926
             $item->longdescription);
927

  
928
    if ($self->search_cvpartnumber) {
929
      $self->get_item_cvpartnumber($item);
930
      $self->js->html('.row_entry:has(#item_' . $item_id
931
                      . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
932
    }
933
  }
934

  
935
  $self->recalc();
936
  $self->js_redisplay_line_values;
937
  $self->js_redisplay_amounts_and_taxes;
938

  
939
  $self->js->render();
940
}
941

  
942
sub js_load_second_row {
943
  my ($self, $item, $item_id, $do_parse) = @_;
944

  
945
  if ($do_parse) {
946
    # Parse values from form (they are formated while rendering (template)).
947
    # Workaround to pre-parse number-cvars (parse_custom_variable_values does
948
    # not parse number values). This parsing is not necessary at all, if we
949
    # assure that the second row/cvars are only loaded once.
950
    foreach my $var (@{ $item->cvars_by_config }) {
951
      if ($var->config->type eq 'number' && exists($var->{__unparsed_value})) {
952
        $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value}));
953
      }
954
    }
955
    $item->parse_custom_variable_values;
956
  }
957

  
958
  my $row_as_html = $self->p->render('reclamation/tabs/basic_data/_second_row', ITEM => $item, TYPE => $self->type);
959

  
960
  $self->js
961
    ->html('#second_row_' . $item_id, $row_as_html)
962
    ->data('#second_row_' . $item_id, 'loaded', 1);
963
}
964

  
965
sub js_redisplay_line_values {
966
  my ($self) = @_;
967

  
968
  my @data = map {[
969
       $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
970
      ]} @{ $self->reclamation->items_sorted };
971

  
972
  $self->js
973
    ->run('kivi.Reclamation.redisplay_line_values', $self->reclamation->is_sales, \@data);
974
}
975

  
976
sub js_redisplay_amounts_and_taxes {
977
  my ($self) = @_;
978

  
979
  if (scalar @{ $self->reclamation->taxes }) {
980
    $self->js->show('#taxincluded_row_id');
981
  } else {
982
    $self->js->hide('#taxincluded_row_id');
983
  }
984

  
985
  if ($self->reclamation->taxincluded) {
986
    $self->js->hide('#subtotal_row_id');
987
  } else {
988
    $self->js->show('#subtotal_row_id');
989
  }
990

  
991
  $self->js
992
    ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->reclamation->netamount, -2))
993
    ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->reclamation->amount,    -2))
994
    ->remove('.tax_row')
995
    ->insertBefore($self->build_tax_rows, '#amount_row_id');
996
}
997

  
998
sub js_redisplay_cvpartnumbers {
999
  my ($self) = @_;
1000

  
1001
  $self->get_item_cvpartnumber($_) for @{$self->reclamation->items_sorted};
1002

  
1003
  my @data = map {[$_->{cvpartnumber}]} @{ $self->reclamation->items_sorted };
1004

  
1005
  $self->js
1006
    ->run('kivi.Reclamation.redisplay_cvpartnumbers', \@data);
1007
}
1008
sub js_reset_reclamation_and_item_ids_after_save {
1009
  my ($self) = @_;
1010

  
1011
  $self->js
1012
    ->val('#id', $self->reclamation->id)
1013
    ->val('#converted_from_record_type_ref', '')
1014
    ->val('#converted_from_record_id',  '')
1015
    ->val('#reclamation_record_number', $self->reclamation->record_number);
1016

  
1017
  my $idx = 0;
1018
  foreach my $form_item_id (@{ $::form->{reclamationitem_ids} }) {
1019
    next if !$self->reclamation->items_sorted->[$idx]->id;
1020
    next if $form_item_id !~ m{^new};
1021
    $self->js
1022
      ->val ('[name="reclamationitem_ids[+]"][value="' . $form_item_id . '"]',
1023
             $self->reclamation->items_sorted->[$idx]->id)
1024
      ->val ('#item_' . $form_item_id,
1025
             $self->reclamation->items_sorted->[$idx]->id)
1026
      ->attr('#item_' . $form_item_id, "id",
1027
             'item_' . $self->reclamation->items_sorted->[$idx]->id);
1028
  } continue {
1029
    $idx++;
1030
  }
1031
  $self->js->val('[name="converted_from_record_item_type_refs[+]"]', '');
1032
  $self->js->val('[name="converted_from_record_item_ids[+]"]', '');
1033
}
1034

  
1035
#
1036
# helpers
1037
#
1038

  
1039
sub init_valid_types {
1040
  [ sales_reclamation_type(), purchase_reclamation_type() ];
1041
}
1042

  
1043
sub init_type {
1044
  my ($self) = @_;
1045

  
1046
  if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
1047
    die "Not a valid type for reclamation";
1048
  }
1049

  
1050
  $self->type($::form->{type});
1051
}
1052

  
1053
sub init_cv {
1054
  my ($self) = @_;
1055

  
1056
  my $cv = (any { $self->type eq $_ } (sales_reclamation_type()))   ? 'customer'
1057
         : (any { $self->type eq $_ } (purchase_reclamation_type())) ? 'vendor'
1058
         : die "Not a valid type for reclamation";
1059

  
1060
  return $cv;
1061
}
1062

  
1063
sub init_search_cvpartnumber {
1064
  my ($self) = @_;
1065

  
1066
  my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1067
  my $search_cvpartnumber;
1068
  $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
1069
  $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
1070

  
1071
  return $search_cvpartnumber;
1072
}
1073

  
1074
sub init_show_update_button {
1075
  my ($self) = @_;
1076

  
1077
  !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1078
}
1079

  
1080
sub init_models {
1081
  my ($self) = @_;
1082

  
1083
  SL::Controller::Helper::GetModels->new(
1084
    controller => $self,
1085
    sorted => {
1086
      _default  => {
1087
        by  => 'record_number',
1088
        dir => 0,
1089
      },
1090
      id                      => t8('ID'),
1091
      record_number           => t8('Reclamation Number'),
1092
      employee_id             => t8('Employee'),
1093
      salesman_id             => t8('Salesman'),
1094
      customer_id             => t8('Customer'),
1095
      vendor_id               => t8('Vendor'),
1096
      contact_id              => t8('Contact'),
1097
      language_id             => t8('Language'),
1098
      department_id           => t8('Department'),
1099
      globalproject_id        => t8('Project Number'),
1100
      cv_record_number        => ($self->type eq 'sales_reclamation' ? t8('Customer Record Number') : t8('Vendor Record Number')),
1101
      transaction_description => t8('Description'),
1102
      notes                   => t8('Notes'),
1103
      intnotes                => t8('Internal Notes'),
1104
      shippingpoint           => t8('Shipping Point'),
1105
      shipvia                 => t8('Ship via'),
1106
      shipto_id               => t8('Shipping Address'),
1107
      amount                  => t8('Total'),
1108
      netamount               => t8('Subtotal'),
1109
      delivery_term_id        => t8('Delivery Terms'),
1110
      payment_id              => t8('Payment Terms'),
1111
      currency_id             => t8('Currency'),
1112
      exchangerate            => t8('Exchangerate'),
1113
      taxincluded             => t8('Tax Included'),
1114
      taxzone_id              => t8('Tax zone'),
1115
      tax_point               => t8('Tax point'),
1116
      reqdate                 => t8('Due Date'),
1117
      transdate               => t8('Booking Date'),
1118
      itime                   => t8('Creation Time'),
1119
      mtime                   => t8('Last modification Time'),
1120
      delivered               => t8('Delivered'),
1121
      closed                  => t8('Closed'),
1122
    },
1123
    query => [
1124
      SL::DB::Manager::Reclamation->type_filter($self->type),
1125
      (salesman_id => SL::DB::Manager::Employee->current->id) x ($self->reclamation->is_sales  && !$::auth->assert('sales_all_edit', 1)),
1126
      (employee_id => SL::DB::Manager::Employee->current->id) x ($self->reclamation->is_sales  && !$::auth->assert('sales_all_edit', 1)),
1127
      (employee_id => SL::DB::Manager::Employee->current->id) x (!$self->reclamation->is_sales && !$::auth->assert('purchase_all_edit', 1)),
1128
    ],
1129
  );
1130
}
1131

  
1132
sub init_p {
1133
  SL::Presenter->get;
1134
}
1135

  
1136
sub init_reclamation {
1137
  $_[0]->make_reclamation;
1138
}
1139

  
1140
sub init_all_price_factors {
1141
  SL::DB::Manager::PriceFactor->get_all;
1142
}
1143

  
1144
sub init_part_picker_classification_ids {
1145
  my ($self)    = @_;
1146
  my $attribute = 'used_for_' . ($self->type eq sales_reclamation_type() ? 'sale' : 'purchase');
1147

  
1148
  return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
1149
}
1150

  
1151
sub check_auth {
1152
  my ($self) = @_;
1153

  
1154
  my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
1155

  
1156
  my $right   = $right_for->{ $self->type };
1157
  $right    ||= 'DOES_NOT_EXIST';
1158

  
1159
  $::auth->assert($right);
1160
}
1161

  
1162
# build the selection box for contacts
1163
#
1164
# Needed, if customer/vendor changed.
1165
sub build_contact_select {
1166
  my ($self) = @_;
1167

  
1168
  select_tag('reclamation.contact_id', [ $self->reclamation->customervendor->contacts ],
1169
    value_key  => 'cp_id',
1170
    title_key  => 'full_name_dep',
1171
    default    => $self->reclamation->contact_id,
1172
    with_empty => 1,
1173
    style      => 'width: 300px',
1174
  );
1175
}
1176

  
1177
# build the selection box for shiptos
1178
#
1179
# Needed, if customer/vendor changed.
1180
sub build_shipto_select {
1181
  my ($self) = @_;
1182

  
1183
  select_tag('reclamation.shipto_id',
1184
             [ {displayable_id => t8("No/individual shipping address"),
1185
                shipto_id => '',
1186
               },
1187
               $self->reclamation->customervendor->shipto
1188
             ],
1189
             value_key  => 'shipto_id',
1190
             title_key  => 'displayable_id',
1191
             default    => $self->reclamation->shipto_id,
1192
             with_empty => 0,
1193
             style      => 'width: 300px',
1194
  );
1195
}
1196

  
1197
# build the inputs for the cusom shipto dialog
1198
#
1199
# Needed, if customer/vendor changed.
1200
sub build_shipto_inputs {
1201
  my ($self) = @_;
1202

  
1203
  my $content = $self->p->render('common/_ship_to_dialog',
1204
                                 cv_obj      => $self->reclamation->customervendor,
1205
                                 cs_obj      => $self->reclamation->custom_shipto,
1206
                                 cvars       => $self->reclamation->custom_shipto->cvars_by_config,
1207
                                 id_selector => '#reclamation_shipto_id');
1208

  
1209
  div_tag($content, id => 'shipto_inputs');
1210
}
1211

  
1212
# render the info line for business
1213
#
1214
# Needed, if customer/vendor changed.
1215
sub build_business_info_row
1216
{
1217
  $_[0]->p->render('reclamation/tabs/basic_data/_business_info_row', SELF => $_[0]);
1218
}
1219

  
1220
# build the rows for displaying taxes
1221
#
1222
# Called if amounts where recalculated and redisplayed.
1223
sub build_tax_rows {
1224
  my ($self) = @_;
1225

  
1226
  my $rows_as_html;
1227
  foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->reclamation->taxes }) {
1228
    $rows_as_html .= $self->p->render(
1229
                       'reclamation/tabs/basic_data/_tax_row',
1230
                       TAX => $tax,
1231
                       TAXINCLUDED => $self->reclamation->taxincluded,
1232
                     );
1233
  }
1234
  return $rows_as_html;
1235
}
1236

  
1237
sub render_price_dialog {
1238
  my ($self, $record_item) = @_;
1239

  
1240
  my $price_source = SL::PriceSource->new(
1241
                       record_item => $record_item,
1242
                       record => $self->reclamation,
1243
                     );
1244

  
1245
  $self->js
1246
    ->run(
1247
      'kivi.io.price_chooser_dialog',
1248
      t8('Available Prices'),
1249
      $self->render(
1250
        'reclamation/tabs/basic_data/_price_sources_dialog',
1251
        { output => 0 },
1252
        price_source => $price_source,
1253
      ),
1254
    )
1255
    ->reinit_widgets;
1256

  
1257
#   if (@errors) {
1258
#     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1259
#     $self->js->show('#dialog_flash_error');
1260
#   }
1261

  
1262
  $self->js->render;
1263
}
1264

  
1265
sub load_reclamation {
1266
  my ($self) = @_;
1267

  
1268
  return if !$::form->{id};
1269

  
1270
  $self->reclamation(SL::DB::Reclamation->new(id => $::form->{id})->load);
1271

  
1272
  # Add an empty custom shipto to the reclamation, so that the dialog can render
1273
  # the cvar inputs. You need a custom shipto object to call cvars_by_config to
1274
  # get the cvars.
1275
  if (!$self->reclamation->custom_shipto) {
1276
    $self->reclamation->custom_shipto(SL::DB::Shipto->new(module => 'RC', custom_variables => []));
1277
  }
1278

  
1279
  return $self->reclamation;
1280
}
1281

  
1282
# load or create a new reclamation object
1283
#
1284
# And assign changes from the form to this object.
1285
# If the reclamation is loaded from db, check if items are deleted in the form,
1286
# remove them form the object and collect them for removing from db on saving.
1287
# Then create/update items from form (via make_item) and add them.
1288
sub make_reclamation {
1289
  my ($self) = @_;
1290

  
1291
  # add_items adds items to an reclamation with no items for saving, but they
1292
  # cannot be retrieved via items until the reclamation is saved. Adding empty
1293
  # items to new reclamation here solves this problem.
1294
  my $reclamation;
1295
  if ($::form->{id}) {
1296
    $reclamation = SL::DB::Reclamation->new(id => $::form->{id})->load();
1297
  } else {
1298
    $reclamation = SL::DB::Reclamation->new(
1299
                     reclamation_items  => [],
1300
                     currency_id => $::instance_conf->get_currency_id(),
1301
                   );
1302
    my $cv_id_method = $self->cv . '_id';
1303
    if ($::form->{$cv_id_method}) {
1304
      $reclamation->$cv_id_method($::form->{$cv_id_method});
1305
      $self->setup_reclamation_from_cv($reclamation);
1306
    }
1307
  }
1308

  
1309
  my $form_reclamation_items = delete $::form->{reclamation}->{reclamation_items};
1310

  
1311
  $reclamation->assign_attributes(%{$::form->{reclamation}});
1312

  
1313
  $self->setup_custom_shipto_from_form($reclamation, $::form);
1314

  
1315
  # remove deleted items
1316
  $self->item_ids_to_delete([]);
1317
  foreach my $idx (reverse 0..$#{$reclamation->reclamation_items}) {
1318
    my $item = $reclamation->reclamation_items->[$idx];
1319
    if (none { $item->id == $_->{id} } @{$form_reclamation_items}) {
1320
      splice @{$reclamation->reclamation_items}, $idx, 1;
1321
      push @{$self->item_ids_to_delete}, $item->id;
1322
    }
1323
  }
1324

  
1325
  my @items;
1326
  my $pos = 1;
1327
  foreach my $form_attr (@{$form_reclamation_items}) {
1328
    my $item = make_item($reclamation, $form_attr);
1329
    $item->position($pos);
1330
    push @items, $item;
1331
    $pos++;
1332
  }
1333
  $reclamation->add_items(grep {!$_->id} @items);
1334

  
1335
  return $reclamation;
1336
}
1337

  
1338
# create or update items from form
1339
#
1340
# Make item objects from form values. For items already existing read from db.
1341
# Create a new item else. And assign attributes.
1342
sub make_item {
1343
  my ($record, $attr) = @_;
1344

  
1345
  my $item;
1346
  $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1347

  
1348
  my $is_new = !$item;
1349

  
1350
  # add_custom_variables adds cvars to an reclamation_item with no cvars for
1351
  # saving, but they cannot be retrieved via custom_variables until the
1352
  # reclamation/reclamation_item is saved. Adding empty custom_variables to new
1353
  # reclamationitem here solves this problem.
1354
  $item ||= SL::DB::ReclamationItem->new(custom_variables => []);
1355

  
1356
  $item->assign_attributes(%$attr);
1357

  
1358
  if ($is_new) {
1359
    my $texts = get_part_texts($item->part, $record->language_id);
1360
    $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
1361
    $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
1362
    $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1363
  }
1364

  
1365
  return $item;
1366
}
1367

  
1368
# create a new item
1369
#
1370
# This is used to add one item
1371
sub new_item {
1372
  my ($record, $attr) = @_;
1373

  
1374
  my $item = SL::DB::ReclamationItem->new;
1375

  
1376
  # Remove attributes where the user left or set the inputs empty.
1377
  # So these attributes will be undefined and we can distinguish them
1378
  # from zero later on.
1379
  for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1380
    delete $attr->{$_} if $attr->{$_} eq '';
1381
  }
1382

  
1383
  $item->assign_attributes(%$attr);
1384

  
1385
  my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
1386
  my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1387
  $item->unit($part->unit) if !$item->unit;
1388

  
1389
  my $price_src;
1390
  if ( $part->is_assortment ) {
1391
    # add assortment items with price 0, as the components carry the price
1392
    $price_src = $price_source->price_from_source("");
1393
    $price_src->price(0);
1394
  } elsif (defined $item->sellprice) {
1395
    $price_src = $price_source->price_from_source("");
1396
    $price_src->price($item->sellprice);
1397
  } else {
1398
    $price_src = $price_source->best_price
1399
               ? $price_source->best_price
1400
               : $price_source->price_from_source("");
1401
    if ($record->exchangerate) {
1402
      $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5));
1403
    }
1404
    $price_src->price(0) if !$price_source->best_price;
1405
  }
1406

  
1407
  my $discount_src;
1408
  if (defined $item->discount) {
1409
    $discount_src = $price_source->discount_from_source("");
1410
    $discount_src->discount($item->discount);
1411
  } else {
1412
    $discount_src = $price_source->best_discount
1413
                  ? $price_source->best_discount
1414
                  : $price_source->discount_from_source("");
1415
    $discount_src->discount(0) if !$price_source->best_discount;
1416
  }
1417

  
1418
  my %new_attr;
1419
  $new_attr{part}                   = $part;
1420
  $new_attr{description}            = $part->description     if ! $item->description;
1421
  $new_attr{qty}                    = 1.0                    if ! $item->qty;
1422
  $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
1423
  $new_attr{sellprice}              = $price_src->price;
1424
  $new_attr{discount}               = $discount_src->discount;
1425
  $new_attr{active_price_source}    = $price_src;
1426
  $new_attr{active_discount_source} = $discount_src;
1427
  $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
1428
  $new_attr{project_id}             = $record->globalproject_id;
1429
  $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
1430

  
1431
  # add_custom_variables adds cvars to an reclamationitem with no cvars for saving, but
1432
  # they cannot be retrieved via custom_variables until the reclamation/reclamationitem is
1433
  # saved. Adding empty custom_variables to new reclamationitem here solves this problem.
1434
  $new_attr{custom_variables} = [];
1435

  
1436
  my $texts = get_part_texts($part, $record->language_id,
1437
                description => $new_attr{description},
1438
                longdescription => $new_attr{longdescription},
1439
              );
1440

  
1441
  $item->assign_attributes(%new_attr, %{ $texts });
1442

  
1443
  $item->reclamation($record);
1444
  return $item;
1445
}
1446

  
1447
sub setup_reclamation_from_cv {
1448
  my ($self, $reclamation) = @_;
1449

  
1450
  $reclamation->$_($reclamation->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
1451

  
1452
  $reclamation->intnotes($reclamation->customervendor->notes);
1453

  
1454
  if ($reclamation->is_sales ) {
1455
    $reclamation->salesman_id($reclamation->customer->salesman_id || SL::DB::Manager::Employee->current->id);
1456
    $reclamation->taxincluded(
1457
      defined($reclamation->customervendor->taxincluded_checked)
1458
        ? $reclamation->customervendor->taxincluded_checked
1459
        : $::myconfig{taxincluded_checked}
... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet.

Auch abrufbar als: Unified diff