1 package SL::Controller::Order;
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::Locale::String qw(t8);
10 use SL::SessionFile::Random;
15 use SL::Util qw(trim);
17 use SL::DB::AdditionalBillingAddress;
24 use SL::DB::PartClassification;
25 use SL::DB::PartsGroup;
29 use SL::DB::RecordLink;
30 use SL::DB::RequirementSpec;
32 use SL::DB::Translation;
34 use SL::Helper::CreatePDF qw(:all);
35 use SL::Helper::PrintOptions;
36 use SL::Helper::ShippedQty;
37 use SL::Helper::UserPreferences::DisplayPreferences;
38 use SL::Helper::UserPreferences::PositionsScrollbar;
39 use SL::Helper::UserPreferences::UpdatePositions;
41 use SL::Controller::Helper::GetModels;
43 use List::Util qw(first sum0);
44 use List::UtilsBy qw(sort_by uniq_by);
45 use List::MoreUtils qw(any none pairwise first_index);
46 use English qw(-no_match_vars);
51 use Rose::Object::MakeMethods::Generic
53 scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
54 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
59 __PACKAGE__->run_before('check_auth');
61 __PACKAGE__->run_before('check_auth_for_edit',
62 except => [ qw(edit show_customer_vendor_details_dialog price_popup load_second_rows) ]);
64 __PACKAGE__->run_before('recalc',
65 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
68 __PACKAGE__->run_before('get_unalterable_data',
69 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
80 $self->order->transdate(DateTime->now_local());
81 my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval :
82 $self->type eq sales_order_type() ? $::instance_conf->get_delivery_date_interval : 1;
84 if ( ($self->type eq sales_order_type() && $::instance_conf->get_deliverydate_on)
85 || ($self->type eq sales_quotation_type() && $::instance_conf->get_reqdate_on)
86 && (!$self->order->reqdate)) {
87 $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
94 title => $self->get_title_for('add'),
95 %{$self->{template_args}}
99 # edit an existing order
107 # this is to edit an order from an unsaved order object
109 # set item ids to new fake id, to identify them as new items
110 foreach my $item (@{$self->order->items_sorted}) {
111 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
113 # trigger rendering values for second row as hidden, because they
114 # are loaded only on demand. So we need to keep the values from
116 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
123 title => $self->get_title_for('edit'),
124 %{$self->{template_args}}
128 # edit a collective order (consisting of one or more existing orders)
129 sub action_edit_collective {
133 my @multi_ids = map {
134 $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
135 } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
137 # fall back to add if no ids are given
138 if (scalar @multi_ids == 0) {
143 # fall back to save as new if only one id is given
144 if (scalar @multi_ids == 1) {
145 $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
146 $self->action_save_as_new();
150 # make new order from given orders
151 my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
152 $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
153 $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
155 $self->action_edit();
162 my $errors = $self->delete();
164 if (scalar @{ $errors }) {
165 $self->js->flash('error', $_) foreach @{ $errors };
166 return $self->js->render();
169 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been deleted')
170 : $self->type eq purchase_order_type() ? $::locale->text('The order has been deleted')
171 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been deleted')
172 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
174 flash_later('info', $text);
176 my @redirect_params = (
181 $self->redirect_to(@redirect_params);
188 my $errors = $self->save();
190 if (scalar @{ $errors }) {
191 $self->js->flash('error', $_) foreach @{ $errors };
192 return $self->js->render();
195 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
196 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
197 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
198 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
200 flash_later('info', $text);
203 if ($::form->{back_to_caller}) {
204 @redirect_params = $::form->{callback} ? ($::form->{callback})
205 : (controller => 'LoginScreen', action => 'user_login');
211 id => $self->order->id,
212 callback => $::form->{callback},
216 $self->redirect_to(@redirect_params);
219 # save the order as new document an open it for edit
220 sub action_save_as_new {
223 my $order = $self->order;
226 $self->js->flash('error', t8('This object has not been saved yet.'));
227 return $self->js->render();
230 # load order from db to check if values changed
231 my $saved_order = SL::DB::Order->new(id => $order->id)->load;
234 # Lets assign a new number if the user hasn't changed the previous one.
235 # If it has been changed manually then use it as-is.
236 $new_attrs{number} = (trim($order->number) eq $saved_order->number)
238 : trim($order->number);
240 # Clear transdate unless changed
241 $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
242 ? DateTime->today_local
245 # Set new reqdate unless changed if it is enabled in client config
246 if ($order->reqdate == $saved_order->reqdate) {
247 my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval :
248 $self->type eq sales_order_type() ? $::instance_conf->get_delivery_date_interval : 1;
250 if ( ($self->type eq sales_order_type() && !$::instance_conf->get_deliverydate_on)
251 || ($self->type eq sales_quotation_type() && !$::instance_conf->get_reqdate_on)) {
252 $new_attrs{reqdate} = '';
254 $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
257 $new_attrs{reqdate} = $order->reqdate;
261 $new_attrs{employee} = SL::DB::Manager::Employee->current;
263 # Warn on obsolete items
264 my @obsolete_positions = map { $_->position } grep { $_->part->obsolete } @{ $order->items_sorted };
265 flash_later('warning', t8('This record containts obsolete items at position #1', join ', ', @obsolete_positions)) if @obsolete_positions;
267 # Create new record from current one
268 $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
270 # no linked records on save as new
271 delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
274 $self->action_save();
279 # This is called if "print" is pressed in the print dialog.
280 # If PDF creation was requested and succeeded, the pdf is offered for download
281 # via send_file (which uses ajax in this case).
285 my $errors = $self->save();
287 if (scalar @{ $errors }) {
288 $self->js->flash('error', $_) foreach @{ $errors };
289 return $self->js->render();
292 $self->js_reset_order_and_item_ids_after_save;
294 my $format = $::form->{print_options}->{format};
295 my $media = $::form->{print_options}->{media};
296 my $formname = $::form->{print_options}->{formname};
297 my $copies = $::form->{print_options}->{copies};
298 my $groupitems = $::form->{print_options}->{groupitems};
299 my $printer_id = $::form->{print_options}->{printer_id};
301 # only PDF, OpenDocument & HTML for now
302 if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
303 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
306 # only screen or printer by now
307 if (none { $media eq $_ } qw(screen printer)) {
308 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
311 # create a form for generate_attachment_filename
312 my $form = Form->new;
313 $form->{$self->nr_key()} = $self->order->number;
314 $form->{type} = $self->type;
315 $form->{format} = $format;
316 $form->{formname} = $formname;
317 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
318 my $doc_filename = $form->generate_attachment_filename();
321 my @errors = $self->generate_doc(\$doc, { media => $media,
323 formname => $formname,
324 language => $self->order->language,
325 printer_id => $printer_id,
326 groupitems => $groupitems });
327 if (scalar @errors) {
328 return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
331 if ($media eq 'screen') {
333 flash_later('info', t8('The document has been created.'));
336 type => SL::MIME->mime_type_from_ext($doc_filename),
337 name => $doc_filename,
341 } elsif ($media eq 'printer') {
343 my $printer_id = $::form->{print_options}->{printer_id};
344 SL::DB::Printer->new(id => $printer_id)->load->print_document(
349 flash_later('info', t8('The document has been printed.'));
352 my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
353 if (scalar @warnings) {
354 flash_later('warning', $_) for @warnings;
357 $self->save_history('PRINTED');
359 my @redirect_params = (
362 id => $self->order->id,
364 $self->js->redirect_to($self->url_for(@redirect_params))->render;
367 sub action_preview_pdf {
370 my $errors = $self->save();
371 if (scalar @{ $errors }) {
372 $self->js->flash('error', $_) foreach @{ $errors };
373 return $self->js->render();
376 $self->js_reset_order_and_item_ids_after_save;
379 my $media = 'screen';
380 my $formname = $self->type;
383 # create a form for generate_attachment_filename
384 my $form = Form->new;
385 $form->{$self->nr_key()} = $self->order->number;
386 $form->{type} = $self->type;
387 $form->{format} = $format;
388 $form->{formname} = $formname;
389 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
390 my $pdf_filename = $form->generate_attachment_filename();
393 my @errors = $self->generate_doc(\$pdf, { media => $media,
395 formname => $formname,
396 language => $self->order->language,
398 if (scalar @errors) {
399 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
401 $self->save_history('PREVIEWED');
402 flash_later('info', t8('The PDF has been previewed'));
406 type => SL::MIME->mime_type_from_ext($pdf_filename),
407 name => $pdf_filename,
411 my @redirect_params = (
414 id => $self->order->id,
416 $self->js->redirect_to($self->url_for(@redirect_params))->render;
419 # open the email dialog
420 sub action_save_and_show_email_dialog {
423 my $errors = $self->save();
425 if (scalar @{ $errors }) {
426 $self->js->flash('error', $_) foreach @{ $errors };
427 return $self->js->render();
430 $self->js_reset_order_and_item_ids_after_save;
432 my $cv_method = $self->cv;
434 if (!$self->order->$cv_method) {
435 return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
440 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
441 $email_form->{to} ||= $self->order->$cv_method->email;
442 $email_form->{cc} = $self->order->$cv_method->cc;
443 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
444 # Todo: get addresses from shipto, if any
446 my $form = Form->new;
447 $form->{$self->nr_key()} = $self->order->number;
448 $form->{cusordnumber} = $self->order->cusordnumber;
449 $form->{formname} = $self->type;
450 $form->{type} = $self->type;
451 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
452 $form->{language_id} = $self->order->language->id if $self->order->language;
453 $form->{format} = 'pdf';
454 $form->{cp_id} = $self->order->contact->cp_id if $self->order->contact;
456 $email_form->{subject} = $form->generate_email_subject();
457 $email_form->{attachment_filename} = $form->generate_attachment_filename();
458 $email_form->{message} = $form->generate_email_body();
459 $email_form->{js_send_function} = 'kivi.Order.send_email()';
461 my %files = $self->get_files_for_email_dialog();
463 my @employees_with_email = grep {
464 my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
465 $user && !!trim($user->get_config_value('email'));
466 } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
469 my $all_partner_email_addresses = $self->order->customervendor->get_all_email_addresses();
471 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
472 email_form => $email_form,
473 show_bcc => $::auth->assert('email_bcc', 'may fail'),
475 is_customer => $self->cv eq 'customer',
476 ALL_EMPLOYEES => \@employees_with_email,
477 ALL_PARTNER_EMAIL_ADDRESSES => $all_partner_email_addresses,
481 ->run('kivi.Order.show_email_dialog', $dialog_html)
488 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
489 sub action_send_email {
492 my $errors = $self->save();
494 if (scalar @{ $errors }) {
495 $self->js->run('kivi.Order.close_email_dialog');
496 $self->js->flash('error', $_) foreach @{ $errors };
497 return $self->js->render();
500 $self->js_reset_order_and_item_ids_after_save;
502 my $email_form = delete $::form->{email_form};
504 if ($email_form->{additional_to}) {
505 $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
506 delete $email_form->{additional_to};
509 my %field_names = (to => 'email');
511 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
513 # for Form::cleanup which may be called in Form::send_email
514 $::form->{cwd} = getcwd();
515 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
517 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
518 $::form->{media} = 'email';
520 $::form->{attachment_policy} //= '';
522 # Is an old file version available?
524 if ($::form->{attachment_policy} eq 'old_file') {
525 $attfile = SL::File->get_all(object_id => $self->order->id,
526 object_type => $self->type,
527 file_type => 'document',
528 print_variant => $::form->{formname});
531 if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
533 my @errors = $self->generate_doc(\$doc, {media => $::form->{media},
534 format => $::form->{print_options}->{format},
535 formname => $::form->{print_options}->{formname},
536 language => $self->order->language,
537 printer_id => $::form->{print_options}->{printer_id},
538 groupitems => $::form->{print_options}->{groupitems}});
539 if (scalar @errors) {
540 return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
543 my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename}, $::form->{formname});
544 if (scalar @warnings) {
545 flash_later('warning', $_) for @warnings;
548 my $sfile = SL::SessionFile::Random->new(mode => "w");
549 $sfile->fh->print($doc);
552 $::form->{tmpfile} = $sfile->file_name;
553 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
556 $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
557 $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
559 # internal notes unless no email journal
560 unless ($::instance_conf->get_email_journal) {
561 my $intnotes = $self->order->intnotes;
562 $intnotes .= "\n\n" if $self->order->intnotes;
563 $intnotes .= t8('[email]') . "\n";
564 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
565 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
566 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
567 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
568 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
569 $intnotes .= t8('Message') . ": " . SL::HTML::Util->strip($::form->{message});
571 $self->order->update_attributes(intnotes => $intnotes);
574 $self->save_history('MAILED');
576 flash_later('info', t8('The email has been sent.'));
578 my @redirect_params = (
581 id => $self->order->id,
584 $self->redirect_to(@redirect_params);
587 # open the periodic invoices config dialog
589 # If there are values in the form (i.e. dialog was opened before),
590 # then use this values. Create new ones, else.
591 sub action_show_periodic_invoices_config_dialog {
594 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
595 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
596 $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm',
597 order_value_periodicity => 'p', # = same as periodicity
598 start_date_as_date => $::form->{transdate_as_date} || $::form->current_date,
599 extend_automatically_by => 12,
601 email_subject => GenericTranslations->get(
602 language_id => $::form->{language_id},
603 translation_type =>"preset_text_periodic_invoices_email_subject"),
604 email_body => GenericTranslations->get(
605 language_id => $::form->{language_id},
606 translation_type => "salutation_general")
607 . GenericTranslations->get(
608 language_id => $::form->{language_id},
609 translation_type => "salutation_punctuation_mark") . "\n\n"
610 . GenericTranslations->get(
611 language_id => $::form->{language_id},
612 translation_type =>"preset_text_periodic_invoices_email_body"),
614 # for older configs, replace email preset text if not yet set.
615 $config->email_subject(GenericTranslations->get(
616 language_id => $::form->{language_id},
617 translation_type =>"preset_text_periodic_invoices_email_subject")
618 ) unless $config->email_subject;
620 $config->email_body(GenericTranslations->get(
621 language_id => $::form->{language_id},
622 translation_type => "salutation_general")
623 . GenericTranslations->get(
624 language_id => $::form->{language_id},
625 translation_type => "salutation_punctuation_mark") . "\n\n"
626 . GenericTranslations->get(
627 language_id => $::form->{language_id},
628 translation_type =>"preset_text_periodic_invoices_email_body")
629 ) unless $config->email_body;
631 $config->periodicity('m') if none { $_ eq $config->periodicity } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
632 $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
634 $::form->get_lists(printers => "ALL_PRINTERS",
635 charts => { key => 'ALL_CHARTS',
636 transdate => 'current_date' });
638 $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
640 if ($::form->{customer_id}) {
641 $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
642 my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
643 $::form->{postal_invoice} = $customer_object->postal_invoice;
644 $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
645 $config->send_email(0) if $::form->{postal_invoice};
648 $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
650 popup_js_close_function => 'kivi.Order.close_periodic_invoices_config_dialog()',
651 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
656 # assign the values of the periodic invoices config dialog
657 # as yaml in the hidden tag and set the status.
658 sub action_assign_periodic_invoices_config {
661 $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
663 my $config = { active => $::form->{active} ? 1 : 0,
664 terminated => $::form->{terminated} ? 1 : 0,
665 direct_debit => $::form->{direct_debit} ? 1 : 0,
666 periodicity => (any { $_ eq $::form->{periodicity} } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES) ? $::form->{periodicity} : 'm',
667 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
668 start_date_as_date => $::form->{start_date_as_date},
669 end_date_as_date => $::form->{end_date_as_date},
670 first_billing_date_as_date => $::form->{first_billing_date_as_date},
671 print => $::form->{print} ? 1 : 0,
672 printer_id => $::form->{print} ? $::form->{printer_id} * 1 : undef,
673 copies => $::form->{copies} * 1 ? $::form->{copies} : 1,
674 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
675 ar_chart_id => $::form->{ar_chart_id} * 1,
676 send_email => $::form->{send_email} ? 1 : 0,
677 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
678 email_recipient_address => $::form->{email_recipient_address},
679 email_sender => $::form->{email_sender},
680 email_subject => $::form->{email_subject},
681 email_body => $::form->{email_body},
684 my $periodic_invoices_config = SL::YAML::Dump($config);
686 my $status = $self->get_periodic_invoices_status($config);
689 ->remove('#order_periodic_invoices_config')
690 ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
691 ->run('kivi.Order.close_periodic_invoices_config_dialog')
692 ->html('#periodic_invoices_status', $status)
693 ->flash('info', t8('The periodic invoices config has been assigned.'))
697 sub action_get_has_active_periodic_invoices {
700 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
701 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
703 my $has_active_periodic_invoices =
704 $self->type eq sales_order_type()
707 && (!$config->end_date || ($config->end_date > DateTime->today_local))
708 && $config->get_previous_billed_period_start_date;
710 $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
713 # save the order and redirect to the frontend subroutine for a new
715 sub action_save_and_delivery_order {
718 $self->save_and_redirect_to(
719 controller => 'oe.pl',
720 action => 'oe_delivery_order_from_order',
724 sub action_save_and_supplier_delivery_order {
727 $self->save_and_redirect_to(
728 controller => 'controller.pl',
729 action => 'DeliveryOrder/add_from_order',
730 type => 'supplier_delivery_order',
734 # save the order and redirect to the frontend subroutine for a new
736 sub action_save_and_invoice {
739 $self->save_and_redirect_to(
740 controller => 'oe.pl',
741 action => 'oe_invoice_from_order',
745 sub action_save_and_invoice_for_advance_payment {
748 $self->save_and_redirect_to(
749 controller => 'oe.pl',
750 action => 'oe_invoice_from_order',
751 new_invoice_type => 'invoice_for_advance_payment',
755 sub action_save_and_final_invoice {
758 $self->save_and_redirect_to(
759 controller => 'oe.pl',
760 action => 'oe_invoice_from_order',
761 new_invoice_type => 'final_invoice',
765 # workflow from sales order to sales quotation
766 sub action_sales_quotation {
767 $_[0]->workflow_sales_or_request_for_quotation();
770 # workflow from sales order to sales quotation
771 sub action_request_for_quotation {
772 $_[0]->workflow_sales_or_request_for_quotation();
775 # workflow from sales quotation to sales order
776 sub action_sales_order {
777 $_[0]->workflow_sales_or_purchase_order();
780 # workflow from rfq to purchase order
781 sub action_purchase_order {
782 $_[0]->workflow_sales_or_purchase_order();
785 # workflow from purchase order to ap transaction
786 sub action_save_and_ap_transaction {
789 $self->save_and_redirect_to(
790 controller => 'ap.pl',
791 action => 'add_from_purchase_order',
795 # set form elements in respect to a changed customer or vendor
797 # This action is called on an change of the customer/vendor picker.
798 sub action_customer_vendor_changed {
801 setup_order_from_cv($self->order);
804 my $cv_method = $self->cv;
806 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
807 $self->js->show('#cp_row');
809 $self->js->hide('#cp_row');
812 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
813 $self->js->show('#shipto_selection');
815 $self->js->hide('#shipto_selection');
818 if ($cv_method eq 'customer') {
819 my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
820 $self->js->$show_hide('#billing_address_row');
823 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
826 ->replaceWith('#order_cp_id', $self->build_contact_select)
827 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
828 ->replaceWith('#shipto_inputs ', $self->build_shipto_inputs)
829 ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
830 ->replaceWith('#business_info_row', $self->build_business_info_row)
831 ->val( '#order_taxzone_id', $self->order->taxzone_id)
832 ->val( '#order_taxincluded', $self->order->taxincluded)
833 ->val( '#order_currency_id', $self->order->currency_id)
834 ->val( '#order_payment_id', $self->order->payment_id)
835 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
836 ->val( '#order_intnotes', $self->order->intnotes)
837 ->val( '#order_language_id', $self->order->$cv_method->language_id)
838 ->focus( '#order_' . $self->cv . '_id')
839 ->run('kivi.Order.update_exchangerate');
841 $self->js_redisplay_amounts_and_taxes;
842 $self->js_redisplay_cvpartnumbers;
846 # open the dialog for customer/vendor details
847 sub action_show_customer_vendor_details_dialog {
850 my $is_customer = 'customer' eq $::form->{vc};
853 $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
855 $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
858 my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
859 $details{discount_as_percent} = $cv->discount_as_percent;
860 $details{creditlimt} = $cv->creditlimit_as_number;
861 $details{business} = $cv->business->description if $cv->business;
862 $details{language} = $cv->language_obj->description if $cv->language_obj;
863 $details{delivery_terms} = $cv->delivery_term->description if $cv->delivery_term;
864 $details{payment_terms} = $cv->payment->description if $cv->payment;
865 $details{pricegroup} = $cv->pricegroup->pricegroup if $is_customer && $cv->pricegroup;
868 foreach my $entry (@{ $cv->additional_billing_addresses }) {
869 push @{ $details{ADDITIONAL_BILLING_ADDRESSES} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
872 foreach my $entry (@{ $cv->shipto }) {
873 push @{ $details{SHIPTO} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
875 foreach my $entry (@{ $cv->contacts }) {
876 push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
879 $_[0]->render('common/show_vc_details', { layout => 0 },
880 is_customer => $is_customer,
885 # called if a unit in an existing item row is changed
886 sub action_unit_changed {
889 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
890 my $item = $self->order->items_sorted->[$idx];
892 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
893 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
898 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
899 $self->js_redisplay_line_values;
900 $self->js_redisplay_amounts_and_taxes;
904 # update item input row when a part ist picked
905 sub action_update_item_input_row {
908 delete $::form->{add_item}->{$_} for qw(create_part_type sellprice_as_number discount_as_percent);
910 my $form_attr = $::form->{add_item};
912 return unless $form_attr->{parts_id};
914 my $record = $self->order;
915 my $item = SL::DB::OrderItem->new(%$form_attr);
916 $item->unit($item->part->unit);
918 my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
921 ->val ('#add_item_unit', $item->unit)
922 ->val ('#add_item_description', $item->part->description)
923 ->val ('#add_item_sellprice_as_number', '')
924 ->attr ('#add_item_sellprice_as_number', 'placeholder', $price_src->price_as_number)
925 ->attr ('#add_item_sellprice_as_number', 'title', $price_src->source_description)
926 ->val ('#add_item_discount_as_percent', '')
927 ->attr ('#add_item_discount_as_percent', 'placeholder', $discount_src->discount_as_percent)
928 ->attr ('#add_item_discount_as_percent', 'title', $discount_src->source_description)
932 # add an item row for a new item entered in the input row
933 sub action_add_item {
936 delete $::form->{add_item}->{create_part_type};
938 my $form_attr = $::form->{add_item};
940 return unless $form_attr->{parts_id};
942 my $item = new_item($self->order, $form_attr);
944 $self->order->add_items($item);
948 $self->get_item_cvpartnumber($item);
950 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
951 my $row_as_html = $self->p->render('order/tabs/_row',
957 if ($::form->{insert_before_item_id}) {
959 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
962 ->append('#row_table_id', $row_as_html);
965 if ( $item->part->is_assortment ) {
966 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
967 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
968 my $attr = { parts_id => $assortment_item->parts_id,
969 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
970 unit => $assortment_item->unit,
971 description => $assortment_item->part->description,
973 my $item = new_item($self->order, $attr);
975 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
976 $item->discount(1) unless $assortment_item->charge;
978 $self->order->add_items( $item );
980 $self->get_item_cvpartnumber($item);
981 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
982 my $row_as_html = $self->p->render('order/tabs/_row',
987 if ($::form->{insert_before_item_id}) {
989 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
992 ->append('#row_table_id', $row_as_html);
998 ->val('.add_item_input', '')
999 ->attr('.add_item_input', 'placeholder', '')
1000 ->attr('.add_item_input', 'title', '')
1001 ->run('kivi.Order.init_row_handlers')
1002 ->run('kivi.Order.renumber_positions')
1003 ->focus('#add_item_parts_id_name');
1005 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
1007 $self->js_redisplay_amounts_and_taxes;
1008 $self->js->render();
1011 # add item rows for multiple items at once
1012 sub action_add_multi_items {
1015 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
1016 return $self->js->render() unless scalar @form_attr;
1019 foreach my $attr (@form_attr) {
1020 my $item = new_item($self->order, $attr);
1022 if ( $item->part->is_assortment ) {
1023 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
1024 my $attr = { parts_id => $assortment_item->parts_id,
1025 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
1026 unit => $assortment_item->unit,
1027 description => $assortment_item->part->description,
1029 my $item = new_item($self->order, $attr);
1031 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
1032 $item->discount(1) unless $assortment_item->charge;
1037 $self->order->add_items(@items);
1041 foreach my $item (@items) {
1042 $self->get_item_cvpartnumber($item);
1043 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1044 my $row_as_html = $self->p->render('order/tabs/_row',
1050 if ($::form->{insert_before_item_id}) {
1052 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
1055 ->append('#row_table_id', $row_as_html);
1060 ->run('kivi.Part.close_picker_dialogs')
1061 ->run('kivi.Order.init_row_handlers')
1062 ->run('kivi.Order.renumber_positions')
1063 ->focus('#add_item_parts_id_name');
1065 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
1067 $self->js_redisplay_amounts_and_taxes;
1068 $self->js->render();
1071 # recalculate all linetotals, amounts and taxes and redisplay them
1072 sub action_recalc_amounts_and_taxes {
1077 $self->js_redisplay_line_values;
1078 $self->js_redisplay_amounts_and_taxes;
1079 $self->js->render();
1082 sub action_update_exchangerate {
1086 is_standard => $self->order->currency_id == $::instance_conf->get_currency_id,
1087 currency_name => $self->order->currency->name,
1088 exchangerate => $self->order->daily_exchangerate_as_null_number,
1091 $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
1094 # redisplay item rows if they are sorted by an attribute
1095 sub action_reorder_items {
1099 partnumber => sub { $_[0]->part->partnumber },
1100 description => sub { $_[0]->description },
1101 qty => sub { $_[0]->qty },
1102 sellprice => sub { $_[0]->sellprice },
1103 discount => sub { $_[0]->discount },
1104 cvpartnumber => sub { $_[0]->{cvpartnumber} },
1107 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1109 my $method = $sort_keys{$::form->{order_by}};
1110 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
1111 if ($::form->{sort_dir}) {
1112 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
1113 @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
1115 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
1118 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
1119 @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
1121 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
1125 ->run('kivi.Order.redisplay_items', \@to_sort)
1129 # show the popup to choose a price/discount source
1130 sub action_price_popup {
1133 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
1134 my $item = $self->order->items_sorted->[$idx];
1136 $self->render_price_dialog($item);
1139 # save the order in a session variable and redirect to the part controller
1140 sub action_create_part {
1143 my $previousform = $::auth->save_form_in_session(non_scalars => 1);
1145 my $callback = $self->url_for(
1146 action => 'return_from_create_part',
1147 type => $self->type, # type is needed for check_auth on return
1148 previousform => $previousform,
1151 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.'));
1153 my @redirect_params = (
1154 controller => 'Part',
1156 part_type => $::form->{add_item}->{create_part_type},
1157 callback => $callback,
1161 $self->redirect_to(@redirect_params);
1164 sub action_return_from_create_part {
1167 $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
1169 $::auth->restore_form_from_session(delete $::form->{previousform});
1171 # set item ids to new fake id, to identify them as new items
1172 foreach my $item (@{$self->order->items_sorted}) {
1173 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1177 $self->get_unalterable_data();
1178 $self->pre_render();
1180 # trigger rendering values for second row/longdescription as hidden,
1181 # because they are loaded only on demand. So we need to keep the values
1183 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1184 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
1188 title => $self->get_title_for('edit'),
1189 %{$self->{template_args}}
1194 # load the second row for one or more items
1196 # This action gets the html code for all items second rows by rendering a template for
1197 # the second row and sets the html code via client js.
1198 sub action_load_second_rows {
1201 $self->recalc() if $self->order->is_sales; # for margin calculation
1203 foreach my $item_id (@{ $::form->{item_ids} }) {
1204 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1205 my $item = $self->order->items_sorted->[$idx];
1207 $self->js_load_second_row($item, $item_id, 0);
1210 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
1212 $self->js->render();
1215 # update description, notes and sellprice from master data
1216 sub action_update_row_from_master_data {
1219 foreach my $item_id (@{ $::form->{item_ids} }) {
1220 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1221 my $item = $self->order->items_sorted->[$idx];
1222 my $texts = get_part_texts($item->part, $self->order->language_id);
1224 $item->description($texts->{description});
1225 $item->longdescription($texts->{longdescription});
1227 my ($price_src, $discount_src) = get_best_price_and_discount_source($self->order, $item, 1);
1229 $item->sellprice($price_src->price);
1230 $item->active_price_source($price_src);
1231 $item->discount($discount_src->discount);
1232 $item->active_discount_source($discount_src);
1234 my $price_editable = $self->order->is_sales ? $::auth->assert('sales_edit_prices', 1) : $::auth->assert('purchase_edit_prices', 1);
1237 ->run('kivi.Order.set_price_and_source_text', $item_id, $price_src ->source, $price_src ->source_description, $item->sellprice_as_number, $price_editable)
1238 ->run('kivi.Order.set_discount_and_source_text', $item_id, $discount_src->source, $discount_src->source_description, $item->discount_as_percent, $price_editable)
1239 ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
1240 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
1241 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
1243 if ($self->search_cvpartnumber) {
1244 $self->get_item_cvpartnumber($item);
1245 $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
1250 $self->js_redisplay_line_values;
1251 $self->js_redisplay_amounts_and_taxes;
1253 $self->js->render();
1256 sub action_save_phone_note {
1259 if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
1260 return $self->js->flash('error', t8('Phone note needs a subject and a body.'))->render;
1264 if ($::form->{phone_note}->{id}) {
1265 $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
1266 return $self->js->flash('error', t8('Phone note not found for this order.'))->render if !$phone_note;
1269 $phone_note = SL::DB::Note->new() if !$phone_note;
1270 my $is_new = !$phone_note->id;
1272 $phone_note->assign_attributes(%{ $::form->{phone_note} },
1273 trans_id => $self->order->id,
1274 trans_module => 'oe',
1275 employee => SL::DB::Manager::Employee->current);
1278 $self->order(SL::DB::Order->new(id => $self->order->id)->load);
1280 my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
1283 ->replaceWith('#phone-notes', $tab_as_html)
1284 ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
1285 ->flash('info', $is_new ? t8('Phone note has been created.') : t8('Phone note has been updated.'))
1289 sub action_delete_phone_note {
1292 my $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
1294 return $self->js->flash('error', t8('Phone note not found for this order.'))->render if !$phone_note;
1296 $phone_note->delete;
1297 $self->order(SL::DB::Order->new(id => $self->order->id)->load);
1299 my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
1302 ->replaceWith('#phone-notes', $tab_as_html)
1303 ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
1304 ->flash('info', t8('Phone note has been deleted.'))
1308 sub js_load_second_row {
1309 my ($self, $item, $item_id, $do_parse) = @_;
1312 # Parse values from form (they are formated while rendering (template)).
1313 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1314 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
1315 foreach my $var (@{ $item->cvars_by_config }) {
1316 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1318 $item->parse_custom_variable_values;
1321 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
1324 ->html('#second_row_' . $item_id, $row_as_html)
1325 ->data('#second_row_' . $item_id, 'loaded', 1);
1328 sub js_redisplay_line_values {
1331 my $is_sales = $self->order->is_sales;
1333 # sales orders with margins
1338 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1339 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
1340 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
1341 ]} @{ $self->order->items_sorted };
1345 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1346 ]} @{ $self->order->items_sorted };
1350 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
1353 sub js_redisplay_amounts_and_taxes {
1356 if (scalar @{ $self->{taxes} }) {
1357 $self->js->show('#taxincluded_row_id');
1359 $self->js->hide('#taxincluded_row_id');
1362 if ($self->order->taxincluded) {
1363 $self->js->hide('#subtotal_row_id');
1365 $self->js->show('#subtotal_row_id');
1368 if ($self->order->is_sales) {
1369 my $is_neg = $self->order->marge_total < 0;
1371 ->html('#marge_total_id', $::form->format_amount(\%::myconfig, $self->order->marge_total, 2))
1372 ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
1373 ->action_if( $is_neg, 'addClass', '#marge_total_id', 'plus0')
1374 ->action_if( $is_neg, 'addClass', '#marge_percent_id', 'plus0')
1375 ->action_if( $is_neg, 'addClass', '#marge_percent_sign_id', 'plus0')
1376 ->action_if(!$is_neg, 'removeClass', '#marge_total_id', 'plus0')
1377 ->action_if(!$is_neg, 'removeClass', '#marge_percent_id', 'plus0')
1378 ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
1382 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
1383 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
1384 ->remove('.tax_row')
1385 ->insertBefore($self->build_tax_rows, '#amount_row_id');
1388 sub js_redisplay_cvpartnumbers {
1391 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1393 my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
1396 ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
1399 sub js_reset_order_and_item_ids_after_save {
1403 ->val('#id', $self->order->id)
1404 ->val('#converted_from_oe_id', '')
1405 ->val('#order_' . $self->nr_key(), $self->order->number);
1408 foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
1409 next if !$self->order->items_sorted->[$idx]->id;
1410 next if $form_item_id !~ m{^new};
1412 ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
1413 ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
1414 ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
1418 $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
1425 sub init_valid_types {
1426 [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
1432 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
1433 die "Not a valid type for order";
1436 $self->type($::form->{type});
1442 my $cv = (any { $self->type eq $_ } (sales_order_type(), sales_quotation_type())) ? 'customer'
1443 : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
1444 : die "Not a valid type for order";
1449 sub init_search_cvpartnumber {
1452 my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1453 my $search_cvpartnumber;
1454 $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
1455 $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel() if $self->cv eq 'vendor';
1457 return $search_cvpartnumber;
1460 sub init_show_update_button {
1463 !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1474 sub init_all_price_factors {
1475 SL::DB::Manager::PriceFactor->get_all;
1478 sub init_part_picker_classification_ids {
1480 my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
1482 return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
1488 my $right_for = { map { $_ => $_.'_edit' . ' | ' . $_.'_view' } @{$self->valid_types} };
1490 my $right = $right_for->{ $self->type };
1491 $right ||= 'DOES_NOT_EXIST';
1493 $::auth->assert($right);
1496 sub check_auth_for_edit {
1499 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
1501 my $right = $right_for->{ $self->type };
1502 $right ||= 'DOES_NOT_EXIST';
1504 $::auth->assert($right);
1507 # build the selection box for contacts
1509 # Needed, if customer/vendor changed.
1510 sub build_contact_select {
1513 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1514 value_key => 'cp_id',
1515 title_key => 'full_name_dep',
1516 default => $self->order->cp_id,
1518 style => 'width: 300px',
1522 # build the selection box for the additional billing address
1524 # Needed, if customer/vendor changed.
1525 sub build_billing_address_select {
1528 return '' if $self->cv ne 'customer';
1530 select_tag('order.billing_address_id',
1531 [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
1533 title_key => 'displayable_id',
1534 default => $self->order->billing_address_id,
1536 style => 'width: 300px',
1540 # build the selection box for shiptos
1542 # Needed, if customer/vendor changed.
1543 sub build_shipto_select {
1546 select_tag('order.shipto_id',
1547 [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
1548 value_key => 'shipto_id',
1549 title_key => 'displayable_id',
1550 default => $self->order->shipto_id,
1552 style => 'width: 300px',
1556 # build the inputs for the cusom shipto dialog
1558 # Needed, if customer/vendor changed.
1559 sub build_shipto_inputs {
1562 my $content = $self->p->render('common/_ship_to_dialog',
1563 vc_obj => $self->order->customervendor,
1564 cs_obj => $self->order->custom_shipto,
1565 cvars => $self->order->custom_shipto->cvars_by_config,
1566 id_selector => '#order_shipto_id');
1568 div_tag($content, id => 'shipto_inputs');
1571 # render the info line for business
1573 # Needed, if customer/vendor changed.
1574 sub build_business_info_row
1576 $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
1579 # build the rows for displaying taxes
1581 # Called if amounts where recalculated and redisplayed.
1582 sub build_tax_rows {
1586 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
1587 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
1589 return $rows_as_html;
1593 sub render_price_dialog {
1594 my ($self, $record_item) = @_;
1596 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1600 'kivi.io.price_chooser_dialog',
1601 t8('Available Prices'),
1602 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1607 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1608 # $self->js->show('#dialog_flash_error');
1617 return if !$::form->{id};
1619 $self->order(SL::DB::Order->new(id => $::form->{id})->load);
1621 # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
1622 # You need a custom shipto object to call cvars_by_config to get the cvars.
1623 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
1625 return $self->order;
1628 # load or create a new order object
1630 # And assign changes from the form to this object.
1631 # If the order is loaded from db, check if items are deleted in the form,
1632 # remove them form the object and collect them for removing from db on saving.
1633 # Then create/update items from form (via make_item) and add them.
1637 # add_items adds items to an order with no items for saving, but they cannot
1638 # be retrieved via items until the order is saved. Adding empty items to new
1639 # order here solves this problem.
1641 $order = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
1642 $order ||= SL::DB::Order->new(orderitems => [],
1643 quotation => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
1644 currency_id => $::instance_conf->get_currency_id(),);
1646 my $cv_id_method = $self->cv . '_id';
1647 if (!$::form->{id} && $::form->{$cv_id_method}) {
1648 $order->$cv_id_method($::form->{$cv_id_method});
1649 setup_order_from_cv($order);
1652 my $form_orderitems = delete $::form->{order}->{orderitems};
1653 my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1655 $order->assign_attributes(%{$::form->{order}});
1657 $self->setup_custom_shipto_from_form($order, $::form);
1659 if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
1660 my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
1661 $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
1664 # remove deleted items
1665 $self->item_ids_to_delete([]);
1666 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1667 my $item = $order->orderitems->[$idx];
1668 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1669 splice @{$order->orderitems}, $idx, 1;
1670 push @{$self->item_ids_to_delete}, $item->id;
1676 foreach my $form_attr (@{$form_orderitems}) {
1677 my $item = make_item($order, $form_attr);
1678 $item->position($pos);
1682 $order->add_items(grep {!$_->id} @items);
1687 # create or update items from form
1689 # Make item objects from form values. For items already existing read from db.
1690 # Create a new item else. And assign attributes.
1692 my ($record, $attr) = @_;
1695 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1697 my $is_new = !$item;
1699 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1700 # they cannot be retrieved via custom_variables until the order/orderitem is
1701 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1702 $item ||= SL::DB::OrderItem->new(custom_variables => []);
1704 $item->assign_attributes(%$attr);
1707 my $texts = get_part_texts($item->part, $record->language_id);
1708 $item->longdescription($texts->{longdescription}) if !defined $attr->{longdescription};
1709 $item->project_id($record->globalproject_id) if !defined $attr->{project_id};
1710 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1718 # This is used to add one item
1720 my ($record, $attr) = @_;
1722 my $item = SL::DB::OrderItem->new;
1724 # Remove attributes where the user left or set the inputs empty.
1725 # So these attributes will be undefined and we can distinguish them
1726 # from zero later on.
1727 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1728 delete $attr->{$_} if $attr->{$_} eq '';
1731 $item->assign_attributes(%$attr);
1732 $item->qty(1.0) if !$item->qty;
1733 $item->unit($item->part->unit) if !$item->unit;
1735 my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
1738 $new_attr{description} = $item->part->description if ! $item->description;
1739 $new_attr{qty} = 1.0 if ! $item->qty;
1740 $new_attr{price_factor_id} = $item->part->price_factor_id if ! $item->price_factor_id;
1741 $new_attr{sellprice} = $price_src->price;
1742 $new_attr{discount} = $discount_src->discount;
1743 $new_attr{active_price_source} = $price_src;
1744 $new_attr{active_discount_source} = $discount_src;
1745 $new_attr{longdescription} = $item->part->notes if ! defined $attr->{longdescription};
1746 $new_attr{project_id} = $record->globalproject_id;
1747 $new_attr{lastcost} = $record->is_sales ? $item->part->lastcost : 0;
1749 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1750 # they cannot be retrieved via custom_variables until the order/orderitem is
1751 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1752 $new_attr{custom_variables} = [];
1754 my $texts = get_part_texts($item->part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
1756 $item->assign_attributes(%new_attr, %{ $texts });
1761 sub setup_order_from_cv {
1764 $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id language_id));
1766 $order->intnotes($order->customervendor->notes);
1768 return if !$order->is_sales;
1770 $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
1771 $order->taxincluded(defined($order->customer->taxincluded_checked)
1772 ? $order->customer->taxincluded_checked
1773 : $::myconfig{taxincluded_checked});
1775 my $address = $order->customer->default_billing_address;;
1776 $order->billing_address_id($address ? $address->id : undef);
1779 # setup custom shipto from form
1781 # The dialog returns form variables starting with 'shipto' and cvars starting
1782 # with 'shiptocvar_'.
1783 # Mark it to be deleted if a shipto from master data is selected
1784 # (i.e. order has a shipto).
1785 # Else, update or create a new custom shipto. If the fields are empty, it
1786 # will not be saved on save.
1787 sub setup_custom_shipto_from_form {
1788 my ($self, $order, $form) = @_;
1790 if ($order->shipto) {
1791 $self->is_custom_shipto_to_delete(1);
1793 my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1795 my $shipto_cvars = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
1796 my $shipto_attrs = {map { $_ => delete $form->{$_}} grep { m{^shipto} } keys %$form};
1798 $custom_shipto->assign_attributes(%$shipto_attrs);
1799 $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
1803 # recalculate prices and taxes
1805 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1809 my %pat = $self->order->calculate_prices_and_taxes();
1811 $self->{taxes} = [];
1812 foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
1813 my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
1815 push(@{ $self->{taxes} }, { amount => $pat{taxes_by_tax_id}->{$tax_id},
1816 netamount => $netamount,
1817 tax => SL::DB::Tax->new(id => $tax_id)->load });
1819 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
1822 # get data for saving, printing, ..., that is not changed in the form
1824 # Only cvars for now.
1825 sub get_unalterable_data {
1828 foreach my $item (@{ $self->order->items }) {
1829 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1830 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1831 foreach my $var (@{ $item->cvars_by_config }) {
1832 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1834 $item->parse_custom_variable_values;
1840 # And remove related files in the spool directory
1845 my $db = $self->order->db;
1847 $db->with_transaction(
1849 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1850 $self->order->delete;
1851 my $spool = $::lx_office_conf{paths}->{spool};
1852 unlink map { "$spool/$_" } @spoolfiles if $spool;
1854 $self->save_history('DELETED');
1857 }) || push(@{$errors}, $db->error);
1864 # And delete items that are deleted in the form.
1869 my $db = $self->order->db;
1871 # check for new or updated phone note
1872 if ($::form->{phone_note}->{subject} || $::form->{phone_note}->{body}) {
1873 if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
1874 return [t8('Phone note needs a subject and a body.')];
1878 if ($::form->{phone_note}->{id}) {
1879 $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
1880 return [t8('Phone note not found for this order.')] if !$phone_note;
1883 $phone_note = SL::DB::Note->new() if !$phone_note;
1884 my $is_new = !$phone_note->id;
1886 $phone_note->assign_attributes(%{ $::form->{phone_note} },
1887 trans_id => $self->order->id,
1888 trans_module => 'oe',
1889 employee => SL::DB::Manager::Employee->current);
1891 $self->order->add_phone_notes($phone_note) if $is_new;
1894 $db->with_transaction(sub {
1895 # delete custom shipto if it is to be deleted or if it is empty
1896 if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
1897 $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
1898 $self->order->custom_shipto(undef);
1901 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
1902 $self->order->save(cascade => 1);
1905 if ($::form->{converted_from_oe_id}) {
1906 my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
1908 foreach my $converted_from_oe_id (@converted_from_oe_ids) {
1909 my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
1910 $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
1911 $src->link_to_record($self->order);
1913 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1915 foreach (@{ $self->order->items_sorted }) {
1916 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1918 SL::DB::RecordLink->new(from_table => 'orderitems',
1919 from_id => $from_id,
1920 to_table => 'orderitems',
1927 $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
1930 $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
1932 $self->save_history('SAVED');
1935 }) || push(@{$errors}, $db->error);
1940 sub workflow_sales_or_request_for_quotation {
1944 my $errors = $self->save();
1946 if (scalar @{ $errors }) {
1947 $self->js->flash('error', $_) for @{ $errors };
1948 return $self->js->render();
1951 my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
1953 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1954 delete $::form->{id};
1956 # no linked records from order to quotations
1957 delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
1959 # set item ids to new fake id, to identify them as new items
1960 foreach my $item (@{$self->order->items_sorted}) {
1961 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1965 $::form->{type} = $destination_type;
1966 $self->type($self->init_type);
1967 $self->cv ($self->init_cv);
1971 $self->get_unalterable_data();
1972 $self->pre_render();
1974 # trigger rendering values for second row as hidden, because they
1975 # are loaded only on demand. So we need to keep the values from the
1977 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1981 title => $self->get_title_for('edit'),
1982 %{$self->{template_args}}
1986 sub workflow_sales_or_purchase_order {
1990 my $errors = $self->save();
1992 if (scalar @{ $errors }) {
1993 $self->js->flash('error', $_) foreach @{ $errors };
1994 return $self->js->render();
1997 my $destination_type = $::form->{type} eq sales_quotation_type() ? sales_order_type()
1998 : $::form->{type} eq request_quotation_type() ? purchase_order_type()
1999 : $::form->{type} eq purchase_order_type() ? sales_order_type()
2000 : $::form->{type} eq sales_order_type() ? purchase_order_type()
2003 # check for direct delivery
2004 # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
2006 if ( $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
2007 && $::form->{use_shipto} && $self->order->shipto) {
2008 $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
2011 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
2012 $self->{converted_from_oe_id} = delete $::form->{id};
2014 # set item ids to new fake id, to identify them as new items
2015 foreach my $item (@{$self->order->items_sorted}) {
2016 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
2019 if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
2020 if ($::form->{use_shipto}) {
2021 $self->order->custom_shipto($custom_shipto) if $custom_shipto;
2023 # remove any custom shipto if not wanted
2024 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
2029 $::form->{type} = $destination_type;
2030 $self->type($self->init_type);
2031 $self->cv ($self->init_cv);
2035 $self->get_unalterable_data();
2036 $self->pre_render();
2038 # trigger rendering values for second row as hidden, because they
2039 # are loaded only on demand. So we need to keep the values from the
2041 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
2045 title => $self->get_title_for('edit'),
2046 %{$self->{template_args}}
2054 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
2055 $self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
2056 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
2057 $self->{all_languages} = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
2058 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
2061 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
2064 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
2066 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
2067 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
2068 $self->{periodic_invoices_status} = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
2069 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
2070 $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
2072 my $print_form = Form->new('');
2073 $print_form->{type} = $self->type;
2074 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
2075 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
2076 form => $print_form,
2077 options => {dialog_name_prefix => 'print_options.',
2081 no_opendocument => 0,
2085 foreach my $item (@{$self->order->orderitems}) {
2086 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
2087 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
2088 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
2091 if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
2092 # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
2093 # Do not use write_to_objects to prevent order->delivered to be set, because this should be
2094 # the value from db, which can be set manually or is set when linked delivery orders are saved.
2095 SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
2098 if ($self->order->number && $::instance_conf->get_webdav) {
2099 my $webdav = SL::Webdav->new(
2100 type => $self->type,
2101 number => $self->order->number,
2103 my @all_objects = $webdav->get_all_objects;
2104 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
2106 link => File::Spec->catfile($_->full_filedescriptor),
2110 if ( (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
2111 && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
2112 $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
2114 $self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
2116 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
2118 $self->{template_args}->{num_phone_notes} = scalar @{ $self->order->phone_notes || [] };
2120 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
2121 edit_periodic_invoices_config calculate_qty follow_up show_history);
2122 $self->setup_edit_action_bar;
2125 sub setup_edit_action_bar {
2126 my ($self, %params) = @_;
2128 my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
2129 || (($self->type eq sales_order_type()) && $::instance_conf->get_sales_order_show_delete)
2130 || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
2132 my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
2133 my @req_cusordnumber = qw(kivi.Order.check_cusordnumber_presence) x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
2135 my $has_invoice_for_advance_payment;
2136 if ($self->order->id && $self->type eq sales_order_type()) {
2137 my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
2138 $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
2141 my $has_final_invoice;
2142 if ($self->order->id && $self->type eq sales_order_type()) {
2143 my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
2144 $has_final_invoice = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
2147 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
2148 my $right = $right_for->{ $self->type };
2149 $right ||= 'DOES_NOT_EXIST';
2150 my $may_edit_create = $::auth->assert($right, 'may fail');
2152 for my $bar ($::request->layout->get('actionbar')) {
2157 call => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
2158 $::instance_conf->get_order_warn_no_deliverydate,
2160 checks => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
2161 @req_trans_cost_art, @req_cusordnumber,
2163 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2166 t8('Save and Close'),
2167 call => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
2168 $::instance_conf->get_order_warn_no_deliverydate,
2171 checks => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
2172 @req_trans_cost_art, @req_cusordnumber,
2174 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2178 call => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
2179 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2180 @req_trans_cost_art, @req_cusordnumber,
2182 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2183 : !$self->order->id ? t8('This object has not been saved yet.')
2186 ], # end of combobox "Save"
2193 t8('Save and Quotation'),
2194 submit => [ '#order_form', { action => "Order/sales_quotation" } ],
2195 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2196 only_if => (any { $self->type eq $_ } (sales_order_type())),
2197 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2201 submit => [ '#order_form', { action => "Order/request_for_quotation" } ],
2202 only_if => (any { $self->type eq $_ } (purchase_order_type())),
2203 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2206 t8('Save and Sales Order'),
2207 submit => [ '#order_form', { action => "Order/sales_order" } ],
2208 checks => [ @req_trans_cost_art ],
2209 only_if => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
2210 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2213 t8('Save and Purchase Order'),
2214 call => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
2215 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2216 only_if => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
2217 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2220 t8('Save and Delivery Order'),
2221 call => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
2222 $::instance_conf->get_order_warn_no_deliverydate,
2224 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2225 @req_trans_cost_art, @req_cusordnumber,
2227 only_if => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())),
2228 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2231 t8('Save and Supplier Delivery Order'),
2232 call => [ 'kivi.Order.save', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
2233 $::instance_conf->get_order_warn_no_deliverydate,
2235 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2236 @req_trans_cost_art, @req_cusordnumber,
2238 only_if => (any { $self->type eq $_ } (purchase_order_type())),
2239 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2242 t8('Save and Invoice'),
2243 call => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
2244 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2245 @req_trans_cost_art, @req_cusordnumber,
2247 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2250 ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
2251 call => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::instance_conf->get_order_warn_duplicate_parts ],
2252 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2253 @req_trans_cost_art, @req_cusordnumber,
2255 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2256 : $has_final_invoice ? t8('This order has already a final invoice.')
2258 only_if => (any { $self->type eq $_ } (sales_order_type())),
2261 t8('Save and Final Invoice'),
2262 call => [ 'kivi.Order.save', 'save_and_final_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
2263 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2264 @req_trans_cost_art, @req_cusordnumber,
2266 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2267 : $has_final_invoice ? t8('This order has already a final invoice.')
2269 only_if => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
2272 t8('Save and AP Transaction'),
2273 call => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
2274 only_if => (any { $self->type eq $_ } (purchase_order_type())),
2275 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2278 ], # end of combobox "Workflow"
2285 t8('Save and preview PDF'),
2286 call => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
2287 $::instance_conf->get_order_warn_no_deliverydate,
2289 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2290 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2293 t8('Save and print'),
2294 call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
2295 $::instance_conf->get_order_warn_no_deliverydate,
2297 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2298 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2301 t8('Save and E-mail'),
2302 id => 'save_and_email_action',
2303 call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
2304 $::instance_conf->get_order_warn_no_deliverydate,
2306 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2307 : !$self->order->id ? t8('This object has not been saved yet.')
2311 t8('Download attachments of all parts'),
2312 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
2313 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2314 only_if => $::instance_conf->get_doc_storage,
2316 ], # end of combobox "Export"
2320 call => [ 'kivi.Order.delete_order' ],
2321 confirm => $::locale->text('Do you really want to delete this object?'),
2322 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2323 : !$self->order->id ? t8('This object has not been saved yet.')
2325 only_if => $deletion_allowed,
2334 call => [ 'set_history_window', $self->order->id, 'id' ],
2335 disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
2339 call => [ 'kivi.Order.follow_up_window' ],
2340 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2341 only_if => $::auth->assert('productivity', 1),
2343 ], # end of combobox "more"
2349 my ($self, $doc_ref, $params) = @_;
2351 my $order = $self->order;
2354 my $print_form = Form->new('');
2355 $print_form->{type} = $order->type;
2356 $print_form->{formname} = $params->{formname} || $order->type;
2357 $print_form->{format} = $params->{format} || 'pdf';
2358 $print_form->{media} = $params->{media} || 'file';
2359 $print_form->{groupitems} = $params->{groupitems};
2360 $print_form->{printer_id} = $params->{printer_id};
2361 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
2363 $order->language($params->{language});
2364 $order->flatten_to_form($print_form, format_amounts => 1);
2368 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
2369 $template_ext = 'odt';
2370 $template_type = 'OpenDocument';
2371 } elsif ($print_form->{format} =~ m{html}i) {
2372 $template_ext = 'html';
2373 $template_type = 'HTML';
2376 # search for the template
2377 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
2378 name => $print_form->{formname},
2379 extension => $template_ext,
2380 email => $print_form->{media} eq 'email',
2381 language => $params->{language},
2382 printer_id => $print_form->{printer_id},
2385 if (!defined $template_file) {
2386 push @errors, $::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files);
2389 return @errors if scalar @errors;
2391 $print_form->throw_on_error(sub {
2393 $print_form->prepare_for_printing;
2395 $$doc_ref = SL::Helper::CreatePDF->create_pdf(
2396 format => $print_form->{format},
2397 template_type => $template_type,
2398 template => $template_file,
2399 variables => $print_form,
2400 variable_content_types => {
2401 longdescription => 'html',
2402 partnotes => 'html',
2404 $::form->get_variable_content_types_for_cvars,
2408 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
2414 sub get_files_for_email_dialog {
2417 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
2419 return %files if !$::instance_conf->get_doc_storage;
2421 if ($self->order->id) {
2422 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
2423 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
2424 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
2425 $files{project_files} = [ SL::File->get_all( object_id => $self->order->globalproject_id, object_type => 'project', file_type => 'attachment') ];
2429 uniq_by { $_->{id} }
2431 +{ id => $_->part->id,
2432 partnumber => $_->part->partnumber }
2433 } @{$self->order->items_sorted};
2435 foreach my $part (@parts) {
2436 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
2437 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
2440 foreach my $key (keys %files) {
2441 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
2447 sub make_periodic_invoices_config_from_yaml {
2448 my ($yaml_config) = @_;
2450 return if !$yaml_config;
2451 my $attr = SL::YAML::Load($yaml_config);
2452 return if 'HASH' ne ref $attr;
2453 return SL::DB::PeriodicInvoicesConfig->new(%$attr);
2457 sub get_periodic_invoices_status {
2458 my ($self, $config) = @_;
2460 return if $self->type ne sales_order_type();
2461 return t8('not configured') if !$config;
2463 my $active = ('HASH' eq ref $config) ? $config->{active}
2464 : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
2465 : die "Cannot get status of periodic invoices config";
2467 return $active ? t8('active') : t8('inactive');
2471 my ($self, $action) = @_;
2473 return '' if none { lc($action)} qw(add edit);
2476 # $::locale->text("Add Sales Order");
2477 # $::locale->text("Add Purchase Order");
2478 # $::locale->text("Add Quotation");
2479 # $::locale->text("Add Request for Quotation");
2480 # $::locale->text("Edit Sales Order");
2481 # $::locale->text("Edit Purchase Order");
2482 # $::locale->text("Edit Quotation");
2483 # $::locale->text("Edit Request for Quotation");
2485 $action = ucfirst(lc($action));
2486 return $self->type eq sales_order_type() ? $::locale->text("$action Sales Order")
2487 : $self->type eq purchase_order_type() ? $::locale->text("$action Purchase Order")
2488 : $self->type eq sales_quotation_type() ? $::locale->text("$action Quotation")
2489 : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
2493 sub get_item_cvpartnumber {
2494 my ($self, $item) = @_;
2496 return if !$self->search_cvpartnumber;
2497 return if !$self->order->customervendor;
2499 if ($self->cv eq 'vendor') {
2500 my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
2501 $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
2502 } elsif ($self->cv eq 'customer') {
2503 my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
2504 $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
2508 sub get_part_texts {
2509 my ($part_or_id, $language_or_id, %defaults) = @_;
2511 my $part = ref($part_or_id) ? $part_or_id : SL::DB::Part->load_cached($part_or_id);
2512 my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
2514 description => $defaults{description} // $part->description,
2515 longdescription => $defaults{longdescription} // $part->notes,
2518 return $texts unless $language_id;
2520 my $translation = SL::DB::Manager::Translation->get_first(
2522 parts_id => $part->id,
2523 language_id => $language_id,
2526 $texts->{description} = $translation->translation if $translation && $translation->translation;
2527 $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
2532 sub get_best_price_and_discount_source {
2533 my ($record, $item, $ignore_given) = @_;
2535 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
2538 if ( $item->part->is_assortment ) {
2539 # add assortment items with price 0, as the components carry the price
2540 $price_src = $price_source->price_from_source("");
2541 $price_src->price(0);
2542 } elsif (!$ignore_given && defined $item->sellprice) {
2543 $price_src = $price_source->price_from_source("");
2544 $price_src->price($item->sellprice);
2546 $price_src = $price_source->best_price
2547 ? $price_source->best_price
2548 : $price_source->price_from_source("");
2549 $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
2550 $price_src->price(0) if !$price_source->best_price;
2554 if (!$ignore_given && defined $item->discount) {
2555 $discount_src = $price_source->discount_from_source("");
2556 $discount_src->discount($item->discount);
2558 $discount_src = $price_source->best_discount
2559 ? $price_source->best_discount
2560 : $price_source->discount_from_source("");
2561 $discount_src->discount(0) if !$price_source->best_discount;
2564 return ($price_src, $discount_src);
2567 sub sales_order_type {
2571 sub purchase_order_type {
2575 sub sales_quotation_type {
2579 sub request_quotation_type {
2580 'request_quotation';
2584 return $_[0]->type eq sales_order_type() ? 'ordnumber'
2585 : $_[0]->type eq purchase_order_type() ? 'ordnumber'
2586 : $_[0]->type eq sales_quotation_type() ? 'quonumber'
2587 : $_[0]->type eq request_quotation_type() ? 'quonumber'
2591 sub save_and_redirect_to {
2592 my ($self, %params) = @_;
2594 my $errors = $self->save();
2596 if (scalar @{ $errors }) {
2597 $self->js->flash('error', $_) foreach @{ $errors };
2598 return $self->js->render();
2601 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
2602 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
2603 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
2604 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
2606 flash_later('info', $text);
2608 $self->redirect_to(%params, id => $self->order->id);
2612 my ($self, $addition) = @_;
2614 my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
2615 my $snumbers = $number_type . '_' . $self->order->$number_type;
2617 SL::DB::History->new(
2618 trans_id => $self->order->id,
2619 employee_id => SL::DB::Manager::Employee->current->id,
2620 what_done => $self->order->type,
2621 snumbers => $snumbers,
2622 addition => $addition,
2626 sub store_doc_to_webdav_and_filemanagement {
2627 my ($self, $content, $filename, $variant) = @_;
2629 my $order = $self->order;
2632 # copy file to webdav folder
2633 if ($order->number && $::instance_conf->get_webdav_documents) {
2634 my $webdav = SL::Webdav->new(
2635 type => $order->type,
2636 number => $order->number,
2638 my $webdav_file = SL::Webdav::File->new(
2640 filename => $filename,
2643 $webdav_file->store(data => \$content);
2646 push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
2649 if ($order->id && $::instance_conf->get_doc_storage) {
2651 SL::File->save(object_id => $order->id,
2652 object_type => $order->type,
2653 mime_type => SL::MIME->mime_type_from_ext($filename),
2654 source => 'created',
2655 file_type => 'document',
2656 file_name => $filename,
2657 file_contents => $content,
2658 print_variant => $variant);
2661 push @errors, t8('Storing the document in the storage backend failed: #1', $@);
2668 sub link_requirement_specs_linking_to_created_from_objects {
2669 my ($self, @converted_from_oe_ids) = @_;
2671 return unless @converted_from_oe_ids;
2673 my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
2674 foreach my $rs_order (@{ $rs_orders }) {
2675 SL::DB::RequirementSpecOrder->new(
2676 order_id => $self->order->id,
2677 requirement_spec_id => $rs_order->requirement_spec_id,
2678 version_id => $rs_order->version_id,
2683 sub set_project_in_linked_requirement_specs {
2686 my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
2687 foreach my $rs_order (@{ $rs_orders }) {
2688 next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
2690 $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
2702 SL::Controller::Order - controller for orders
2706 This is a new form to enter orders, completely rewritten with the use
2707 of controller and java script techniques.
2709 The aim is to provide the user a better experience and a faster workflow. Also
2710 the code should be more readable, more reliable and better to maintain.
2718 One input row, so that input happens every time at the same place.
2722 Use of pickers where possible.
2726 Possibility to enter more than one item at once.
2730 Item list in a scrollable area, so that the workflow buttons stay at
2735 Reordering item rows with drag and drop is possible. Sorting item rows is
2736 possible (by partnumber, description, qty, sellprice and discount for now).
2740 No C<update> is necessary. All entries and calculations are managed
2741 with ajax-calls and the page only reloads on C<save>.
2745 User can see changes immediately, because of the use of java script
2756 =item * C<SL/Controller/Order.pm>
2760 =item * C<template/webpages/order/form.html>
2764 =item * C<template/webpages/order/tabs/basic_data.html>
2766 Main tab for basic_data.
2768 This is the only tab here for now. "linked records" and "webdav" tabs are
2769 reused from generic code.
2773 =item * C<template/webpages/order/tabs/_business_info_row.html>
2775 For displaying information on business type
2777 =item * C<template/webpages/order/tabs/_item_input.html>
2779 The input line for items
2781 =item * C<template/webpages/order/tabs/_row.html>
2783 One row for already entered items
2785 =item * C<template/webpages/order/tabs/_tax_row.html>
2787 Displaying tax information
2789 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
2791 Dialog for selecting price and discount sources
2795 =item * C<js/kivi.Order.js>
2797 java script functions
2807 =item * price sources: little symbols showing better price / better discount
2809 =item * select units in input row?
2811 =item * check for direct delivery (workflow sales order -> purchase order)
2813 =item * access rights
2815 =item * display weights
2819 =item * optional client/user behaviour
2821 (transactions has to be set - department has to be set -
2822 force project if enabled in client config)
2826 =head1 KNOWN BUGS AND CAVEATS
2832 No indication that <shift>-up/down expands/collapses second row.
2836 Table header is not sticky in the scrolling area.
2840 Sorting does not include C<position>, neither does reordering.
2842 This behavior was implemented intentionally. But we can discuss, which behavior
2843 should be implemented.
2847 =head1 To discuss / Nice to have
2853 How to expand/collapse second row. Now it can be done clicking the icon or
2858 This controller uses a (changed) copy of the template for the PriceSource
2859 dialog. Maybe there could be used one code source.
2863 Rounding-differences between this controller (PriceTaxCalculator) and the old
2864 form. This is not only a problem here, but also in all parts using the PTC.
2865 There exists a ticket and a patch. This patch should be testet.
2869 An indicator, if the actual inputs are saved (like in an
2870 editor or on text processing application).
2874 A warning when leaving the page without saveing unchanged inputs.
2881 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>