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;
28 use SL::DB::RecordLink;
29 use SL::DB::RequirementSpec;
31 use SL::DB::Translation;
33 use SL::Helper::CreatePDF qw(:all);
34 use SL::Helper::PrintOptions;
35 use SL::Helper::ShippedQty;
36 use SL::Helper::UserPreferences::DisplayPreferences;
37 use SL::Helper::UserPreferences::PositionsScrollbar;
38 use SL::Helper::UserPreferences::UpdatePositions;
40 use SL::Controller::Helper::GetModels;
42 use List::Util qw(first sum0);
43 use List::UtilsBy qw(sort_by uniq_by);
44 use List::MoreUtils qw(any none pairwise first_index);
45 use English qw(-no_match_vars);
50 use Rose::Object::MakeMethods::Generic
52 scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
53 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
58 __PACKAGE__->run_before('check_auth');
60 __PACKAGE__->run_before('check_auth_for_edit',
61 except => [ qw(edit show_customer_vendor_details_dialog price_popup load_second_rows) ]);
63 __PACKAGE__->run_before('recalc',
64 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
67 __PACKAGE__->run_before('get_unalterable_data',
68 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
79 $self->order->transdate(DateTime->now_local());
80 my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval :
81 $self->type eq sales_order_type() ? $::instance_conf->get_delivery_date_interval : 1;
83 if ( ($self->type eq sales_order_type() && $::instance_conf->get_deliverydate_on)
84 || ($self->type eq sales_quotation_type() && $::instance_conf->get_reqdate_on)
85 && (!$self->order->reqdate)) {
86 $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
93 title => $self->get_title_for('add'),
94 %{$self->{template_args}}
98 # edit an existing order
106 # this is to edit an order from an unsaved order object
108 # set item ids to new fake id, to identify them as new items
109 foreach my $item (@{$self->order->items_sorted}) {
110 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
112 # trigger rendering values for second row as hidden, because they
113 # are loaded only on demand. So we need to keep the values from
115 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
122 title => $self->get_title_for('edit'),
123 %{$self->{template_args}}
127 # edit a collective order (consisting of one or more existing orders)
128 sub action_edit_collective {
132 my @multi_ids = map {
133 $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
134 } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
136 # fall back to add if no ids are given
137 if (scalar @multi_ids == 0) {
142 # fall back to save as new if only one id is given
143 if (scalar @multi_ids == 1) {
144 $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
145 $self->action_save_as_new();
149 # make new order from given orders
150 my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
151 $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
152 $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
154 $self->action_edit();
161 my $errors = $self->delete();
163 if (scalar @{ $errors }) {
164 $self->js->flash('error', $_) foreach @{ $errors };
165 return $self->js->render();
168 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been deleted')
169 : $self->type eq purchase_order_type() ? $::locale->text('The order has been deleted')
170 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been deleted')
171 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
173 flash_later('info', $text);
175 my @redirect_params = (
180 $self->redirect_to(@redirect_params);
187 my $errors = $self->save();
189 if (scalar @{ $errors }) {
190 $self->js->flash('error', $_) foreach @{ $errors };
191 return $self->js->render();
194 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
195 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
196 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
197 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
199 flash_later('info', $text);
202 if ($::form->{back_to_caller}) {
203 @redirect_params = $::form->{callback} ? ($::form->{callback})
204 : (controller => 'LoginScreen', action => 'user_login');
210 id => $self->order->id,
211 callback => $::form->{callback},
215 $self->redirect_to(@redirect_params);
218 # save the order as new document an open it for edit
219 sub action_save_as_new {
222 my $order = $self->order;
225 $self->js->flash('error', t8('This object has not been saved yet.'));
226 return $self->js->render();
229 # load order from db to check if values changed
230 my $saved_order = SL::DB::Order->new(id => $order->id)->load;
233 # Lets assign a new number if the user hasn't changed the previous one.
234 # If it has been changed manually then use it as-is.
235 $new_attrs{number} = (trim($order->number) eq $saved_order->number)
237 : trim($order->number);
239 # Clear transdate unless changed
240 $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
241 ? DateTime->today_local
244 # Set new reqdate unless changed if it is enabled in client config
245 if ($order->reqdate == $saved_order->reqdate) {
246 my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval :
247 $self->type eq sales_order_type() ? $::instance_conf->get_delivery_date_interval : 1;
249 if ( ($self->type eq sales_order_type() && !$::instance_conf->get_deliverydate_on)
250 || ($self->type eq sales_quotation_type() && !$::instance_conf->get_reqdate_on)) {
251 $new_attrs{reqdate} = '';
253 $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
256 $new_attrs{reqdate} = $order->reqdate;
260 $new_attrs{employee} = SL::DB::Manager::Employee->current;
262 # Create new record from current one
263 $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
265 # no linked records on save as new
266 delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
269 $self->action_save();
274 # This is called if "print" is pressed in the print dialog.
275 # If PDF creation was requested and succeeded, the pdf is offered for download
276 # via send_file (which uses ajax in this case).
280 my $errors = $self->save();
282 if (scalar @{ $errors }) {
283 $self->js->flash('error', $_) foreach @{ $errors };
284 return $self->js->render();
287 $self->js_reset_order_and_item_ids_after_save;
289 my $format = $::form->{print_options}->{format};
290 my $media = $::form->{print_options}->{media};
291 my $formname = $::form->{print_options}->{formname};
292 my $copies = $::form->{print_options}->{copies};
293 my $groupitems = $::form->{print_options}->{groupitems};
294 my $printer_id = $::form->{print_options}->{printer_id};
296 # only PDF, OpenDocument & HTML for now
297 if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
298 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
301 # only screen or printer by now
302 if (none { $media eq $_ } qw(screen printer)) {
303 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
306 # create a form for generate_attachment_filename
307 my $form = Form->new;
308 $form->{$self->nr_key()} = $self->order->number;
309 $form->{type} = $self->type;
310 $form->{format} = $format;
311 $form->{formname} = $formname;
312 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
313 my $doc_filename = $form->generate_attachment_filename();
316 my @errors = $self->generate_doc(\$doc, { media => $media,
318 formname => $formname,
319 language => $self->order->language,
320 printer_id => $printer_id,
321 groupitems => $groupitems });
322 if (scalar @errors) {
323 return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
326 if ($media eq 'screen') {
328 $self->js->flash('info', t8('The document has been created.'));
331 type => SL::MIME->mime_type_from_ext($doc_filename),
332 name => $doc_filename,
336 } elsif ($media eq 'printer') {
338 my $printer_id = $::form->{print_options}->{printer_id};
339 SL::DB::Printer->new(id => $printer_id)->load->print_document(
344 $self->js->flash('info', t8('The document has been printed.'));
347 my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
348 if (scalar @warnings) {
349 $self->js->flash('warning', $_) for @warnings;
352 $self->save_history('PRINTED');
355 ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
358 sub action_preview_pdf {
361 my $errors = $self->save();
362 if (scalar @{ $errors }) {
363 $self->js->flash('error', $_) foreach @{ $errors };
364 return $self->js->render();
367 $self->js_reset_order_and_item_ids_after_save;
370 my $media = 'screen';
371 my $formname = $self->type;
374 # create a form for generate_attachment_filename
375 my $form = Form->new;
376 $form->{$self->nr_key()} = $self->order->number;
377 $form->{type} = $self->type;
378 $form->{format} = $format;
379 $form->{formname} = $formname;
380 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
381 my $pdf_filename = $form->generate_attachment_filename();
384 my @errors = $self->generate_doc(\$pdf, { media => $media,
386 formname => $formname,
387 language => $self->order->language,
389 if (scalar @errors) {
390 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
392 $self->save_history('PREVIEWED');
393 $self->js->flash('info', t8('The PDF has been previewed'));
397 type => SL::MIME->mime_type_from_ext($pdf_filename),
398 name => $pdf_filename,
403 # open the email dialog
404 sub action_save_and_show_email_dialog {
407 my $errors = $self->save();
409 if (scalar @{ $errors }) {
410 $self->js->flash('error', $_) foreach @{ $errors };
411 return $self->js->render();
414 my $cv_method = $self->cv;
416 if (!$self->order->$cv_method) {
417 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'))
422 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
423 $email_form->{to} ||= $self->order->$cv_method->email;
424 $email_form->{cc} = $self->order->$cv_method->cc;
425 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
426 # Todo: get addresses from shipto, if any
428 my $form = Form->new;
429 $form->{$self->nr_key()} = $self->order->number;
430 $form->{cusordnumber} = $self->order->cusordnumber;
431 $form->{formname} = $self->type;
432 $form->{type} = $self->type;
433 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
434 $form->{language_id} = $self->order->language->id if $self->order->language;
435 $form->{format} = 'pdf';
436 $form->{cp_id} = $self->order->contact->cp_id if $self->order->contact;
438 $email_form->{subject} = $form->generate_email_subject();
439 $email_form->{attachment_filename} = $form->generate_attachment_filename();
440 $email_form->{message} = $form->generate_email_body();
441 $email_form->{js_send_function} = 'kivi.Order.send_email()';
443 my %files = $self->get_files_for_email_dialog();
445 my @employees_with_email = grep {
446 my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
447 $user && !!trim($user->get_config_value('email'));
448 } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
451 my $all_partner_email_addresses = $self->order->customervendor->get_all_email_addresses();
453 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
454 email_form => $email_form,
455 show_bcc => $::auth->assert('email_bcc', 'may fail'),
457 is_customer => $self->cv eq 'customer',
458 ALL_EMPLOYEES => \@employees_with_email,
459 ALL_PARTNER_EMAIL_ADDRESSES => $all_partner_email_addresses,
463 ->run('kivi.Order.show_email_dialog', $dialog_html)
470 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
471 sub action_send_email {
474 my $errors = $self->save();
476 if (scalar @{ $errors }) {
477 $self->js->run('kivi.Order.close_email_dialog');
478 $self->js->flash('error', $_) foreach @{ $errors };
479 return $self->js->render();
482 $self->js_reset_order_and_item_ids_after_save;
484 my $email_form = delete $::form->{email_form};
486 if ($email_form->{additional_to}) {
487 $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
488 delete $email_form->{additional_to};
491 my %field_names = (to => 'email');
493 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
495 # for Form::cleanup which may be called in Form::send_email
496 $::form->{cwd} = getcwd();
497 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
499 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
500 $::form->{media} = 'email';
502 $::form->{attachment_policy} //= '';
504 # Is an old file version available?
506 if ($::form->{attachment_policy} eq 'old_file') {
507 $attfile = SL::File->get_all(object_id => $self->order->id,
508 object_type => $self->type,
509 file_type => 'document',
510 print_variant => $::form->{formname});
513 if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
515 my @errors = $self->generate_doc(\$doc, {media => $::form->{media},
516 format => $::form->{print_options}->{format},
517 formname => $::form->{print_options}->{formname},
518 language => $self->order->language,
519 printer_id => $::form->{print_options}->{printer_id},
520 groupitems => $::form->{print_options}->{groupitems}});
521 if (scalar @errors) {
522 return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
525 my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename}, $::form->{formname});
526 if (scalar @warnings) {
527 flash_later('warning', $_) for @warnings;
530 my $sfile = SL::SessionFile::Random->new(mode => "w");
531 $sfile->fh->print($doc);
534 $::form->{tmpfile} = $sfile->file_name;
535 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
538 $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
539 $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
541 # internal notes unless no email journal
542 unless ($::instance_conf->get_email_journal) {
543 my $intnotes = $self->order->intnotes;
544 $intnotes .= "\n\n" if $self->order->intnotes;
545 $intnotes .= t8('[email]') . "\n";
546 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
547 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
548 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
549 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
550 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
551 $intnotes .= t8('Message') . ": " . SL::HTML::Util->strip($::form->{message});
553 $self->order->update_attributes(intnotes => $intnotes);
556 $self->save_history('MAILED');
558 flash_later('info', t8('The email has been sent.'));
560 my @redirect_params = (
563 id => $self->order->id,
566 $self->redirect_to(@redirect_params);
569 # open the periodic invoices config dialog
571 # If there are values in the form (i.e. dialog was opened before),
572 # then use this values. Create new ones, else.
573 sub action_show_periodic_invoices_config_dialog {
576 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
577 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
578 $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm',
579 order_value_periodicity => 'p', # = same as periodicity
580 start_date_as_date => $::form->{transdate_as_date} || $::form->current_date,
581 extend_automatically_by => 12,
583 email_subject => GenericTranslations->get(
584 language_id => $::form->{language_id},
585 translation_type =>"preset_text_periodic_invoices_email_subject"),
586 email_body => GenericTranslations->get(
587 language_id => $::form->{language_id},
588 translation_type => "salutation_general")
589 . GenericTranslations->get(
590 language_id => $::form->{language_id},
591 translation_type => "salutation_punctuation_mark") . "\n\n"
592 . GenericTranslations->get(
593 language_id => $::form->{language_id},
594 translation_type =>"preset_text_periodic_invoices_email_body"),
596 # for older configs, replace email preset text if not yet set.
597 $config->email_subject(GenericTranslations->get(
598 language_id => $::form->{language_id},
599 translation_type =>"preset_text_periodic_invoices_email_subject")
600 ) unless $config->email_subject;
602 $config->email_body(GenericTranslations->get(
603 language_id => $::form->{language_id},
604 translation_type => "salutation_general")
605 . GenericTranslations->get(
606 language_id => $::form->{language_id},
607 translation_type => "salutation_punctuation_mark") . "\n\n"
608 . GenericTranslations->get(
609 language_id => $::form->{language_id},
610 translation_type =>"preset_text_periodic_invoices_email_body")
611 ) unless $config->email_body;
613 $config->periodicity('m') if none { $_ eq $config->periodicity } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
614 $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
616 $::form->get_lists(printers => "ALL_PRINTERS",
617 charts => { key => 'ALL_CHARTS',
618 transdate => 'current_date' });
620 $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
622 if ($::form->{customer_id}) {
623 $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
624 my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
625 $::form->{postal_invoice} = $customer_object->postal_invoice;
626 $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
627 $config->send_email(0) if $::form->{postal_invoice};
630 $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
632 popup_js_close_function => 'kivi.Order.close_periodic_invoices_config_dialog()',
633 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
638 # assign the values of the periodic invoices config dialog
639 # as yaml in the hidden tag and set the status.
640 sub action_assign_periodic_invoices_config {
643 $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
645 my $config = { active => $::form->{active} ? 1 : 0,
646 terminated => $::form->{terminated} ? 1 : 0,
647 direct_debit => $::form->{direct_debit} ? 1 : 0,
648 periodicity => (any { $_ eq $::form->{periodicity} } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES) ? $::form->{periodicity} : 'm',
649 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
650 start_date_as_date => $::form->{start_date_as_date},
651 end_date_as_date => $::form->{end_date_as_date},
652 first_billing_date_as_date => $::form->{first_billing_date_as_date},
653 print => $::form->{print} ? 1 : 0,
654 printer_id => $::form->{print} ? $::form->{printer_id} * 1 : undef,
655 copies => $::form->{copies} * 1 ? $::form->{copies} : 1,
656 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
657 ar_chart_id => $::form->{ar_chart_id} * 1,
658 send_email => $::form->{send_email} ? 1 : 0,
659 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
660 email_recipient_address => $::form->{email_recipient_address},
661 email_sender => $::form->{email_sender},
662 email_subject => $::form->{email_subject},
663 email_body => $::form->{email_body},
666 my $periodic_invoices_config = SL::YAML::Dump($config);
668 my $status = $self->get_periodic_invoices_status($config);
671 ->remove('#order_periodic_invoices_config')
672 ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
673 ->run('kivi.Order.close_periodic_invoices_config_dialog')
674 ->html('#periodic_invoices_status', $status)
675 ->flash('info', t8('The periodic invoices config has been assigned.'))
679 sub action_get_has_active_periodic_invoices {
682 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
683 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
685 my $has_active_periodic_invoices =
686 $self->type eq sales_order_type()
689 && (!$config->end_date || ($config->end_date > DateTime->today_local))
690 && $config->get_previous_billed_period_start_date;
692 $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
695 # save the order and redirect to the frontend subroutine for a new
697 sub action_save_and_delivery_order {
700 $self->save_and_redirect_to(
701 controller => 'oe.pl',
702 action => 'oe_delivery_order_from_order',
706 sub action_save_and_supplier_delivery_order {
709 $self->save_and_redirect_to(
710 controller => 'controller.pl',
711 action => 'DeliveryOrder/add_from_order',
712 type => 'supplier_delivery_order',
716 # save the order and redirect to the frontend subroutine for a new
718 sub action_save_and_invoice {
721 $self->save_and_redirect_to(
722 controller => 'oe.pl',
723 action => 'oe_invoice_from_order',
727 sub action_save_and_invoice_for_advance_payment {
730 $self->save_and_redirect_to(
731 controller => 'oe.pl',
732 action => 'oe_invoice_from_order',
733 new_invoice_type => 'invoice_for_advance_payment',
737 sub action_save_and_final_invoice {
740 $self->save_and_redirect_to(
741 controller => 'oe.pl',
742 action => 'oe_invoice_from_order',
743 new_invoice_type => 'final_invoice',
747 # workflow from sales order to sales quotation
748 sub action_sales_quotation {
749 $_[0]->workflow_sales_or_request_for_quotation();
752 # workflow from sales order to sales quotation
753 sub action_request_for_quotation {
754 $_[0]->workflow_sales_or_request_for_quotation();
757 # workflow from sales quotation to sales order
758 sub action_sales_order {
759 $_[0]->workflow_sales_or_purchase_order();
762 # workflow from rfq to purchase order
763 sub action_purchase_order {
764 $_[0]->workflow_sales_or_purchase_order();
767 # workflow from purchase order to ap transaction
768 sub action_save_and_ap_transaction {
771 $self->save_and_redirect_to(
772 controller => 'ap.pl',
773 action => 'add_from_purchase_order',
777 # set form elements in respect to a changed customer or vendor
779 # This action is called on an change of the customer/vendor picker.
780 sub action_customer_vendor_changed {
783 setup_order_from_cv($self->order);
786 my $cv_method = $self->cv;
788 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
789 $self->js->show('#cp_row');
791 $self->js->hide('#cp_row');
794 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
795 $self->js->show('#shipto_selection');
797 $self->js->hide('#shipto_selection');
800 if ($cv_method eq 'customer') {
801 my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
802 $self->js->$show_hide('#billing_address_row');
805 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
808 ->replaceWith('#order_cp_id', $self->build_contact_select)
809 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
810 ->replaceWith('#shipto_inputs ', $self->build_shipto_inputs)
811 ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
812 ->replaceWith('#business_info_row', $self->build_business_info_row)
813 ->val( '#order_taxzone_id', $self->order->taxzone_id)
814 ->val( '#order_taxincluded', $self->order->taxincluded)
815 ->val( '#order_currency_id', $self->order->currency_id)
816 ->val( '#order_payment_id', $self->order->payment_id)
817 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
818 ->val( '#order_intnotes', $self->order->intnotes)
819 ->val( '#order_language_id', $self->order->$cv_method->language_id)
820 ->focus( '#order_' . $self->cv . '_id')
821 ->run('kivi.Order.update_exchangerate');
823 $self->js_redisplay_amounts_and_taxes;
824 $self->js_redisplay_cvpartnumbers;
828 # open the dialog for customer/vendor details
829 sub action_show_customer_vendor_details_dialog {
832 my $is_customer = 'customer' eq $::form->{vc};
835 $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
837 $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
840 my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
841 $details{discount_as_percent} = $cv->discount_as_percent;
842 $details{creditlimt} = $cv->creditlimit_as_number;
843 $details{business} = $cv->business->description if $cv->business;
844 $details{language} = $cv->language_obj->description if $cv->language_obj;
845 $details{delivery_terms} = $cv->delivery_term->description if $cv->delivery_term;
846 $details{payment_terms} = $cv->payment->description if $cv->payment;
847 $details{pricegroup} = $cv->pricegroup->pricegroup if $is_customer && $cv->pricegroup;
850 foreach my $entry (@{ $cv->additional_billing_addresses }) {
851 push @{ $details{ADDITIONAL_BILLING_ADDRESSES} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
854 foreach my $entry (@{ $cv->shipto }) {
855 push @{ $details{SHIPTO} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
857 foreach my $entry (@{ $cv->contacts }) {
858 push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
861 $_[0]->render('common/show_vc_details', { layout => 0 },
862 is_customer => $is_customer,
867 # called if a unit in an existing item row is changed
868 sub action_unit_changed {
871 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
872 my $item = $self->order->items_sorted->[$idx];
874 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
875 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
880 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
881 $self->js_redisplay_line_values;
882 $self->js_redisplay_amounts_and_taxes;
886 # add an item row for a new item entered in the input row
887 sub action_add_item {
890 delete $::form->{add_item}->{create_part_type};
892 my $form_attr = $::form->{add_item};
894 return unless $form_attr->{parts_id};
896 my $item = new_item($self->order, $form_attr);
898 $self->order->add_items($item);
902 $self->get_item_cvpartnumber($item);
904 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
905 my $row_as_html = $self->p->render('order/tabs/_row',
911 if ($::form->{insert_before_item_id}) {
913 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
916 ->append('#row_table_id', $row_as_html);
919 if ( $item->part->is_assortment ) {
920 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
921 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
922 my $attr = { parts_id => $assortment_item->parts_id,
923 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
924 unit => $assortment_item->unit,
925 description => $assortment_item->part->description,
927 my $item = new_item($self->order, $attr);
929 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
930 $item->discount(1) unless $assortment_item->charge;
932 $self->order->add_items( $item );
934 $self->get_item_cvpartnumber($item);
935 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
936 my $row_as_html = $self->p->render('order/tabs/_row',
941 if ($::form->{insert_before_item_id}) {
943 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
946 ->append('#row_table_id', $row_as_html);
952 ->val('.add_item_input', '')
953 ->run('kivi.Order.init_row_handlers')
954 ->run('kivi.Order.renumber_positions')
955 ->focus('#add_item_parts_id_name');
957 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
959 $self->js_redisplay_amounts_and_taxes;
963 # add item rows for multiple items at once
964 sub action_add_multi_items {
967 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
968 return $self->js->render() unless scalar @form_attr;
971 foreach my $attr (@form_attr) {
972 my $item = new_item($self->order, $attr);
974 if ( $item->part->is_assortment ) {
975 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
976 my $attr = { parts_id => $assortment_item->parts_id,
977 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
978 unit => $assortment_item->unit,
979 description => $assortment_item->part->description,
981 my $item = new_item($self->order, $attr);
983 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
984 $item->discount(1) unless $assortment_item->charge;
989 $self->order->add_items(@items);
993 foreach my $item (@items) {
994 $self->get_item_cvpartnumber($item);
995 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
996 my $row_as_html = $self->p->render('order/tabs/_row',
1002 if ($::form->{insert_before_item_id}) {
1004 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
1007 ->append('#row_table_id', $row_as_html);
1012 ->run('kivi.Part.close_picker_dialogs')
1013 ->run('kivi.Order.init_row_handlers')
1014 ->run('kivi.Order.renumber_positions')
1015 ->focus('#add_item_parts_id_name');
1017 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
1019 $self->js_redisplay_amounts_and_taxes;
1020 $self->js->render();
1023 # recalculate all linetotals, amounts and taxes and redisplay them
1024 sub action_recalc_amounts_and_taxes {
1029 $self->js_redisplay_line_values;
1030 $self->js_redisplay_amounts_and_taxes;
1031 $self->js->render();
1034 sub action_update_exchangerate {
1038 is_standard => $self->order->currency_id == $::instance_conf->get_currency_id,
1039 currency_name => $self->order->currency->name,
1040 exchangerate => $self->order->daily_exchangerate_as_null_number,
1043 $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
1046 # redisplay item rows if they are sorted by an attribute
1047 sub action_reorder_items {
1051 partnumber => sub { $_[0]->part->partnumber },
1052 description => sub { $_[0]->description },
1053 qty => sub { $_[0]->qty },
1054 sellprice => sub { $_[0]->sellprice },
1055 discount => sub { $_[0]->discount },
1056 cvpartnumber => sub { $_[0]->{cvpartnumber} },
1059 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1061 my $method = $sort_keys{$::form->{order_by}};
1062 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
1063 if ($::form->{sort_dir}) {
1064 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
1065 @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
1067 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
1070 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
1071 @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
1073 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
1077 ->run('kivi.Order.redisplay_items', \@to_sort)
1081 # show the popup to choose a price/discount source
1082 sub action_price_popup {
1085 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
1086 my $item = $self->order->items_sorted->[$idx];
1088 $self->render_price_dialog($item);
1091 # save the order in a session variable and redirect to the part controller
1092 sub action_create_part {
1095 my $previousform = $::auth->save_form_in_session(non_scalars => 1);
1097 my $callback = $self->url_for(
1098 action => 'return_from_create_part',
1099 type => $self->type, # type is needed for check_auth on return
1100 previousform => $previousform,
1103 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.'));
1105 my @redirect_params = (
1106 controller => 'Part',
1108 part_type => $::form->{add_item}->{create_part_type},
1109 callback => $callback,
1113 $self->redirect_to(@redirect_params);
1116 sub action_return_from_create_part {
1119 $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
1121 $::auth->restore_form_from_session(delete $::form->{previousform});
1123 # set item ids to new fake id, to identify them as new items
1124 foreach my $item (@{$self->order->items_sorted}) {
1125 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1129 $self->get_unalterable_data();
1130 $self->pre_render();
1132 # trigger rendering values for second row/longdescription as hidden,
1133 # because they are loaded only on demand. So we need to keep the values
1135 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1136 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
1140 title => $self->get_title_for('edit'),
1141 %{$self->{template_args}}
1146 # load the second row for one or more items
1148 # This action gets the html code for all items second rows by rendering a template for
1149 # the second row and sets the html code via client js.
1150 sub action_load_second_rows {
1153 $self->recalc() if $self->order->is_sales; # for margin calculation
1155 foreach my $item_id (@{ $::form->{item_ids} }) {
1156 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1157 my $item = $self->order->items_sorted->[$idx];
1159 $self->js_load_second_row($item, $item_id, 0);
1162 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
1164 $self->js->render();
1167 # update description, notes and sellprice from master data
1168 sub action_update_row_from_master_data {
1171 foreach my $item_id (@{ $::form->{item_ids} }) {
1172 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1173 my $item = $self->order->items_sorted->[$idx];
1174 my $texts = get_part_texts($item->part, $self->order->language_id);
1176 $item->description($texts->{description});
1177 $item->longdescription($texts->{longdescription});
1179 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1182 if ($item->part->is_assortment) {
1183 # add assortment items with price 0, as the components carry the price
1184 $price_src = $price_source->price_from_source("");
1185 $price_src->price(0);
1187 $price_src = $price_source->best_price
1188 ? $price_source->best_price
1189 : $price_source->price_from_source("");
1190 $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
1191 $price_src->price(0) if !$price_source->best_price;
1195 $item->sellprice($price_src->price);
1196 $item->active_price_source($price_src);
1199 ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
1200 ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
1201 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
1202 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
1204 if ($self->search_cvpartnumber) {
1205 $self->get_item_cvpartnumber($item);
1206 $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
1211 $self->js_redisplay_line_values;
1212 $self->js_redisplay_amounts_and_taxes;
1214 $self->js->render();
1217 sub js_load_second_row {
1218 my ($self, $item, $item_id, $do_parse) = @_;
1221 # Parse values from form (they are formated while rendering (template)).
1222 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1223 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
1224 foreach my $var (@{ $item->cvars_by_config }) {
1225 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1227 $item->parse_custom_variable_values;
1230 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
1233 ->html('#second_row_' . $item_id, $row_as_html)
1234 ->data('#second_row_' . $item_id, 'loaded', 1);
1237 sub js_redisplay_line_values {
1240 my $is_sales = $self->order->is_sales;
1242 # sales orders with margins
1247 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1248 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
1249 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
1250 ]} @{ $self->order->items_sorted };
1254 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1255 ]} @{ $self->order->items_sorted };
1259 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
1262 sub js_redisplay_amounts_and_taxes {
1265 if (scalar @{ $self->{taxes} }) {
1266 $self->js->show('#taxincluded_row_id');
1268 $self->js->hide('#taxincluded_row_id');
1271 if ($self->order->taxincluded) {
1272 $self->js->hide('#subtotal_row_id');
1274 $self->js->show('#subtotal_row_id');
1277 if ($self->order->is_sales) {
1278 my $is_neg = $self->order->marge_total < 0;
1280 ->html('#marge_total_id', $::form->format_amount(\%::myconfig, $self->order->marge_total, 2))
1281 ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
1282 ->action_if( $is_neg, 'addClass', '#marge_total_id', 'plus0')
1283 ->action_if( $is_neg, 'addClass', '#marge_percent_id', 'plus0')
1284 ->action_if( $is_neg, 'addClass', '#marge_percent_sign_id', 'plus0')
1285 ->action_if(!$is_neg, 'removeClass', '#marge_total_id', 'plus0')
1286 ->action_if(!$is_neg, 'removeClass', '#marge_percent_id', 'plus0')
1287 ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
1291 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
1292 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
1293 ->remove('.tax_row')
1294 ->insertBefore($self->build_tax_rows, '#amount_row_id');
1297 sub js_redisplay_cvpartnumbers {
1300 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1302 my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
1305 ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
1308 sub js_reset_order_and_item_ids_after_save {
1312 ->val('#id', $self->order->id)
1313 ->val('#converted_from_oe_id', '')
1314 ->val('#order_' . $self->nr_key(), $self->order->number);
1317 foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
1318 next if !$self->order->items_sorted->[$idx]->id;
1319 next if $form_item_id !~ m{^new};
1321 ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
1322 ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
1323 ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
1327 $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
1334 sub init_valid_types {
1335 [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
1341 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
1342 die "Not a valid type for order";
1345 $self->type($::form->{type});
1351 my $cv = (any { $self->type eq $_ } (sales_order_type(), sales_quotation_type())) ? 'customer'
1352 : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
1353 : die "Not a valid type for order";
1358 sub init_search_cvpartnumber {
1361 my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1362 my $search_cvpartnumber;
1363 $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
1364 $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel() if $self->cv eq 'vendor';
1366 return $search_cvpartnumber;
1369 sub init_show_update_button {
1372 !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1383 sub init_all_price_factors {
1384 SL::DB::Manager::PriceFactor->get_all;
1387 sub init_part_picker_classification_ids {
1389 my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
1391 return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
1397 my $right_for = { map { $_ => $_.'_edit' . ' | ' . $_.'_view' } @{$self->valid_types} };
1399 my $right = $right_for->{ $self->type };
1400 $right ||= 'DOES_NOT_EXIST';
1402 $::auth->assert($right);
1405 sub check_auth_for_edit {
1408 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
1410 my $right = $right_for->{ $self->type };
1411 $right ||= 'DOES_NOT_EXIST';
1413 $::auth->assert($right);
1416 # build the selection box for contacts
1418 # Needed, if customer/vendor changed.
1419 sub build_contact_select {
1422 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1423 value_key => 'cp_id',
1424 title_key => 'full_name_dep',
1425 default => $self->order->cp_id,
1427 style => 'width: 300px',
1431 # build the selection box for the additional billing address
1433 # Needed, if customer/vendor changed.
1434 sub build_billing_address_select {
1437 return '' if $self->cv ne 'customer';
1439 select_tag('order.billing_address_id',
1440 [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
1442 title_key => 'displayable_id',
1443 default => $self->order->billing_address_id,
1445 style => 'width: 300px',
1449 # build the selection box for shiptos
1451 # Needed, if customer/vendor changed.
1452 sub build_shipto_select {
1455 select_tag('order.shipto_id',
1456 [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
1457 value_key => 'shipto_id',
1458 title_key => 'displayable_id',
1459 default => $self->order->shipto_id,
1461 style => 'width: 300px',
1465 # build the inputs for the cusom shipto dialog
1467 # Needed, if customer/vendor changed.
1468 sub build_shipto_inputs {
1471 my $content = $self->p->render('common/_ship_to_dialog',
1472 vc_obj => $self->order->customervendor,
1473 cs_obj => $self->order->custom_shipto,
1474 cvars => $self->order->custom_shipto->cvars_by_config,
1475 id_selector => '#order_shipto_id');
1477 div_tag($content, id => 'shipto_inputs');
1480 # render the info line for business
1482 # Needed, if customer/vendor changed.
1483 sub build_business_info_row
1485 $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
1488 # build the rows for displaying taxes
1490 # Called if amounts where recalculated and redisplayed.
1491 sub build_tax_rows {
1495 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
1496 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
1498 return $rows_as_html;
1502 sub render_price_dialog {
1503 my ($self, $record_item) = @_;
1505 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1509 'kivi.io.price_chooser_dialog',
1510 t8('Available Prices'),
1511 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1516 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1517 # $self->js->show('#dialog_flash_error');
1526 return if !$::form->{id};
1528 $self->order(SL::DB::Order->new(id => $::form->{id})->load);
1530 # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
1531 # You need a custom shipto object to call cvars_by_config to get the cvars.
1532 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
1534 return $self->order;
1537 # load or create a new order object
1539 # And assign changes from the form to this object.
1540 # If the order is loaded from db, check if items are deleted in the form,
1541 # remove them form the object and collect them for removing from db on saving.
1542 # Then create/update items from form (via make_item) and add them.
1546 # add_items adds items to an order with no items for saving, but they cannot
1547 # be retrieved via items until the order is saved. Adding empty items to new
1548 # order here solves this problem.
1550 $order = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
1551 $order ||= SL::DB::Order->new(orderitems => [],
1552 quotation => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
1553 currency_id => $::instance_conf->get_currency_id(),);
1555 my $cv_id_method = $self->cv . '_id';
1556 if (!$::form->{id} && $::form->{$cv_id_method}) {
1557 $order->$cv_id_method($::form->{$cv_id_method});
1558 setup_order_from_cv($order);
1561 my $form_orderitems = delete $::form->{order}->{orderitems};
1562 my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1564 $order->assign_attributes(%{$::form->{order}});
1566 $self->setup_custom_shipto_from_form($order, $::form);
1568 if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
1569 my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
1570 $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
1573 # remove deleted items
1574 $self->item_ids_to_delete([]);
1575 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1576 my $item = $order->orderitems->[$idx];
1577 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1578 splice @{$order->orderitems}, $idx, 1;
1579 push @{$self->item_ids_to_delete}, $item->id;
1585 foreach my $form_attr (@{$form_orderitems}) {
1586 my $item = make_item($order, $form_attr);
1587 $item->position($pos);
1591 $order->add_items(grep {!$_->id} @items);
1596 # create or update items from form
1598 # Make item objects from form values. For items already existing read from db.
1599 # Create a new item else. And assign attributes.
1601 my ($record, $attr) = @_;
1604 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1606 my $is_new = !$item;
1608 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1609 # they cannot be retrieved via custom_variables until the order/orderitem is
1610 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1611 $item ||= SL::DB::OrderItem->new(custom_variables => []);
1613 $item->assign_attributes(%$attr);
1616 my $texts = get_part_texts($item->part, $record->language_id);
1617 $item->longdescription($texts->{longdescription}) if !defined $attr->{longdescription};
1618 $item->project_id($record->globalproject_id) if !defined $attr->{project_id};
1619 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1627 # This is used to add one item
1629 my ($record, $attr) = @_;
1631 my $item = SL::DB::OrderItem->new;
1633 # Remove attributes where the user left or set the inputs empty.
1634 # So these attributes will be undefined and we can distinguish them
1635 # from zero later on.
1636 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1637 delete $attr->{$_} if $attr->{$_} eq '';
1640 $item->assign_attributes(%$attr);
1642 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1643 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1645 $item->unit($part->unit) if !$item->unit;
1648 if ( $part->is_assortment ) {
1649 # add assortment items with price 0, as the components carry the price
1650 $price_src = $price_source->price_from_source("");
1651 $price_src->price(0);
1652 } elsif (defined $item->sellprice) {
1653 $price_src = $price_source->price_from_source("");
1654 $price_src->price($item->sellprice);
1656 $price_src = $price_source->best_price
1657 ? $price_source->best_price
1658 : $price_source->price_from_source("");
1659 $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
1660 $price_src->price(0) if !$price_source->best_price;
1664 if (defined $item->discount) {
1665 $discount_src = $price_source->discount_from_source("");
1666 $discount_src->discount($item->discount);
1668 $discount_src = $price_source->best_discount
1669 ? $price_source->best_discount
1670 : $price_source->discount_from_source("");
1671 $discount_src->discount(0) if !$price_source->best_discount;
1675 $new_attr{part} = $part;
1676 $new_attr{description} = $part->description if ! $item->description;
1677 $new_attr{qty} = 1.0 if ! $item->qty;
1678 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1679 $new_attr{sellprice} = $price_src->price;
1680 $new_attr{discount} = $discount_src->discount;
1681 $new_attr{active_price_source} = $price_src;
1682 $new_attr{active_discount_source} = $discount_src;
1683 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1684 $new_attr{project_id} = $record->globalproject_id;
1685 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1687 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1688 # they cannot be retrieved via custom_variables until the order/orderitem is
1689 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1690 $new_attr{custom_variables} = [];
1692 my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
1694 $item->assign_attributes(%new_attr, %{ $texts });
1699 sub setup_order_from_cv {
1702 $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id language_id));
1704 $order->intnotes($order->customervendor->notes);
1706 return if !$order->is_sales;
1708 $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
1709 $order->taxincluded(defined($order->customer->taxincluded_checked)
1710 ? $order->customer->taxincluded_checked
1711 : $::myconfig{taxincluded_checked});
1713 my $address = $order->customer->default_billing_address;;
1714 $order->billing_address_id($address ? $address->id : undef);
1717 # setup custom shipto from form
1719 # The dialog returns form variables starting with 'shipto' and cvars starting
1720 # with 'shiptocvar_'.
1721 # Mark it to be deleted if a shipto from master data is selected
1722 # (i.e. order has a shipto).
1723 # Else, update or create a new custom shipto. If the fields are empty, it
1724 # will not be saved on save.
1725 sub setup_custom_shipto_from_form {
1726 my ($self, $order, $form) = @_;
1728 if ($order->shipto) {
1729 $self->is_custom_shipto_to_delete(1);
1731 my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1733 my $shipto_cvars = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
1734 my $shipto_attrs = {map { $_ => delete $form->{$_}} grep { m{^shipto} } keys %$form};
1736 $custom_shipto->assign_attributes(%$shipto_attrs);
1737 $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
1741 # recalculate prices and taxes
1743 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1747 my %pat = $self->order->calculate_prices_and_taxes();
1749 $self->{taxes} = [];
1750 foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
1751 my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
1753 push(@{ $self->{taxes} }, { amount => $pat{taxes_by_tax_id}->{$tax_id},
1754 netamount => $netamount,
1755 tax => SL::DB::Tax->new(id => $tax_id)->load });
1757 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
1760 # get data for saving, printing, ..., that is not changed in the form
1762 # Only cvars for now.
1763 sub get_unalterable_data {
1766 foreach my $item (@{ $self->order->items }) {
1767 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1768 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1769 foreach my $var (@{ $item->cvars_by_config }) {
1770 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1772 $item->parse_custom_variable_values;
1778 # And remove related files in the spool directory
1783 my $db = $self->order->db;
1785 $db->with_transaction(
1787 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1788 $self->order->delete;
1789 my $spool = $::lx_office_conf{paths}->{spool};
1790 unlink map { "$spool/$_" } @spoolfiles if $spool;
1792 $self->save_history('DELETED');
1795 }) || push(@{$errors}, $db->error);
1802 # And delete items that are deleted in the form.
1807 my $db = $self->order->db;
1809 $db->with_transaction(sub {
1810 # delete custom shipto if it is to be deleted or if it is empty
1811 if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
1812 $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
1813 $self->order->custom_shipto(undef);
1816 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
1817 $self->order->save(cascade => 1);
1820 if ($::form->{converted_from_oe_id}) {
1821 my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
1823 foreach my $converted_from_oe_id (@converted_from_oe_ids) {
1824 my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
1825 $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
1826 $src->link_to_record($self->order);
1828 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1830 foreach (@{ $self->order->items_sorted }) {
1831 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1833 SL::DB::RecordLink->new(from_table => 'orderitems',
1834 from_id => $from_id,
1835 to_table => 'orderitems',
1842 $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
1845 $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
1847 $self->save_history('SAVED');
1850 }) || push(@{$errors}, $db->error);
1855 sub workflow_sales_or_request_for_quotation {
1859 my $errors = $self->save();
1861 if (scalar @{ $errors }) {
1862 $self->js->flash('error', $_) for @{ $errors };
1863 return $self->js->render();
1866 my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
1868 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1869 delete $::form->{id};
1871 # no linked records from order to quotations
1872 delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
1874 # set item ids to new fake id, to identify them as new items
1875 foreach my $item (@{$self->order->items_sorted}) {
1876 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1880 $::form->{type} = $destination_type;
1881 $self->type($self->init_type);
1882 $self->cv ($self->init_cv);
1886 $self->get_unalterable_data();
1887 $self->pre_render();
1889 # trigger rendering values for second row as hidden, because they
1890 # are loaded only on demand. So we need to keep the values from the
1892 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1896 title => $self->get_title_for('edit'),
1897 %{$self->{template_args}}
1901 sub workflow_sales_or_purchase_order {
1905 my $errors = $self->save();
1907 if (scalar @{ $errors }) {
1908 $self->js->flash('error', $_) foreach @{ $errors };
1909 return $self->js->render();
1912 my $destination_type = $::form->{type} eq sales_quotation_type() ? sales_order_type()
1913 : $::form->{type} eq request_quotation_type() ? purchase_order_type()
1914 : $::form->{type} eq purchase_order_type() ? sales_order_type()
1915 : $::form->{type} eq sales_order_type() ? purchase_order_type()
1918 # check for direct delivery
1919 # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
1921 if ( $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
1922 && $::form->{use_shipto} && $self->order->shipto) {
1923 $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
1926 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1927 $self->{converted_from_oe_id} = delete $::form->{id};
1929 # set item ids to new fake id, to identify them as new items
1930 foreach my $item (@{$self->order->items_sorted}) {
1931 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1934 if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
1935 if ($::form->{use_shipto}) {
1936 $self->order->custom_shipto($custom_shipto) if $custom_shipto;
1938 # remove any custom shipto if not wanted
1939 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1944 $::form->{type} = $destination_type;
1945 $self->type($self->init_type);
1946 $self->cv ($self->init_cv);
1950 $self->get_unalterable_data();
1951 $self->pre_render();
1953 # trigger rendering values for second row as hidden, because they
1954 # are loaded only on demand. So we need to keep the values from the
1956 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1960 title => $self->get_title_for('edit'),
1961 %{$self->{template_args}}
1969 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1970 $self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
1971 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1972 $self->{all_languages} = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
1973 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1976 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1979 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1981 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1982 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1983 $self->{periodic_invoices_status} = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
1984 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1985 $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
1987 my $print_form = Form->new('');
1988 $print_form->{type} = $self->type;
1989 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1990 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1991 form => $print_form,
1992 options => {dialog_name_prefix => 'print_options.',
1996 no_opendocument => 0,
2000 foreach my $item (@{$self->order->orderitems}) {
2001 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
2002 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
2003 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
2006 if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
2007 # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
2008 # Do not use write_to_objects to prevent order->delivered to be set, because this should be
2009 # the value from db, which can be set manually or is set when linked delivery orders are saved.
2010 SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
2013 if ($self->order->number && $::instance_conf->get_webdav) {
2014 my $webdav = SL::Webdav->new(
2015 type => $self->type,
2016 number => $self->order->number,
2018 my @all_objects = $webdav->get_all_objects;
2019 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
2021 link => File::Spec->catfile($_->full_filedescriptor),
2025 if ( (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
2026 && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
2027 $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
2029 $self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
2031 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
2033 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
2034 edit_periodic_invoices_config calculate_qty follow_up show_history);
2035 $self->setup_edit_action_bar;
2038 sub setup_edit_action_bar {
2039 my ($self, %params) = @_;
2041 my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
2042 || (($self->type eq sales_order_type()) && $::instance_conf->get_sales_order_show_delete)
2043 || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
2045 my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
2046 my @req_cusordnumber = qw(kivi.Order.check_cusordnumber_presence) x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
2048 my $has_invoice_for_advance_payment;
2049 if ($self->order->id && $self->type eq sales_order_type()) {
2050 my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
2051 $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
2054 my $has_final_invoice;
2055 if ($self->order->id && $self->type eq sales_order_type()) {
2056 my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
2057 $has_final_invoice = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
2060 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
2061 my $right = $right_for->{ $self->type };
2062 $right ||= 'DOES_NOT_EXIST';
2063 my $may_edit_create = $::auth->assert($right, 'may fail');
2065 for my $bar ($::request->layout->get('actionbar')) {
2070 call => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
2071 $::instance_conf->get_order_warn_no_deliverydate,
2073 checks => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
2074 @req_trans_cost_art, @req_cusordnumber,
2076 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2079 t8('Save and Close'),
2080 call => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
2081 $::instance_conf->get_order_warn_no_deliverydate,
2084 checks => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
2085 @req_trans_cost_art, @req_cusordnumber,
2087 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2091 call => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
2092 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2093 @req_trans_cost_art, @req_cusordnumber,
2095 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2096 : !$self->order->id ? t8('This object has not been saved yet.')
2099 ], # end of combobox "Save"
2106 t8('Save and Quotation'),
2107 submit => [ '#order_form', { action => "Order/sales_quotation" } ],
2108 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2109 only_if => (any { $self->type eq $_ } (sales_order_type())),
2110 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2114 submit => [ '#order_form', { action => "Order/request_for_quotation" } ],
2115 only_if => (any { $self->type eq $_ } (purchase_order_type())),
2116 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2119 t8('Save and Sales Order'),
2120 submit => [ '#order_form', { action => "Order/sales_order" } ],
2121 checks => [ @req_trans_cost_art ],
2122 only_if => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
2123 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2126 t8('Save and Purchase Order'),
2127 call => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
2128 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2129 only_if => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
2130 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2133 t8('Save and Delivery Order'),
2134 call => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
2135 $::instance_conf->get_order_warn_no_deliverydate,
2137 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2138 @req_trans_cost_art, @req_cusordnumber,
2140 only_if => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())),
2141 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2144 t8('Save and Supplier Delivery Order'),
2145 call => [ 'kivi.Order.save', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
2146 $::instance_conf->get_order_warn_no_deliverydate,
2148 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2149 @req_trans_cost_art, @req_cusordnumber,
2151 only_if => (any { $self->type eq $_ } (purchase_order_type())),
2152 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2155 t8('Save and Invoice'),
2156 call => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
2157 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2158 @req_trans_cost_art, @req_cusordnumber,
2160 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2163 ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
2164 call => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::instance_conf->get_order_warn_duplicate_parts ],
2165 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2166 @req_trans_cost_art, @req_cusordnumber,
2168 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2169 : $has_final_invoice ? t8('This order has already a final invoice.')
2171 only_if => (any { $self->type eq $_ } (sales_order_type())),
2174 t8('Save and Final Invoice'),
2175 call => [ 'kivi.Order.save', 'save_and_final_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
2176 checks => [ 'kivi.Order.check_save_active_periodic_invoices',
2177 @req_trans_cost_art, @req_cusordnumber,
2179 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2180 : $has_final_invoice ? t8('This order has already a final invoice.')
2182 only_if => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
2185 t8('Save and AP Transaction'),
2186 call => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
2187 only_if => (any { $self->type eq $_ } (purchase_order_type())),
2188 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2191 ], # end of combobox "Workflow"
2198 t8('Save and preview PDF'),
2199 call => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
2200 $::instance_conf->get_order_warn_no_deliverydate,
2202 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2203 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2206 t8('Save and print'),
2207 call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
2208 $::instance_conf->get_order_warn_no_deliverydate,
2210 checks => [ @req_trans_cost_art, @req_cusordnumber ],
2211 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2214 t8('Save and E-mail'),
2215 id => 'save_and_email_action',
2216 call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
2217 $::instance_conf->get_order_warn_no_deliverydate,
2219 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2220 : !$self->order->id ? t8('This object has not been saved yet.')
2224 t8('Download attachments of all parts'),
2225 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
2226 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2227 only_if => $::instance_conf->get_doc_storage,
2229 ], # end of combobox "Export"
2233 call => [ 'kivi.Order.delete_order' ],
2234 confirm => $::locale->text('Do you really want to delete this object?'),
2235 disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2236 : !$self->order->id ? t8('This object has not been saved yet.')
2238 only_if => $deletion_allowed,
2247 call => [ 'set_history_window', $self->order->id, 'id' ],
2248 disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
2252 call => [ 'kivi.Order.follow_up_window' ],
2253 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2254 only_if => $::auth->assert('productivity', 1),
2256 ], # end of combobox "more"
2262 my ($self, $doc_ref, $params) = @_;
2264 my $order = $self->order;
2267 my $print_form = Form->new('');
2268 $print_form->{type} = $order->type;
2269 $print_form->{formname} = $params->{formname} || $order->type;
2270 $print_form->{format} = $params->{format} || 'pdf';
2271 $print_form->{media} = $params->{media} || 'file';
2272 $print_form->{groupitems} = $params->{groupitems};
2273 $print_form->{printer_id} = $params->{printer_id};
2274 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
2276 $order->language($params->{language});
2277 $order->flatten_to_form($print_form, format_amounts => 1);
2281 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
2282 $template_ext = 'odt';
2283 $template_type = 'OpenDocument';
2284 } elsif ($print_form->{format} =~ m{html}i) {
2285 $template_ext = 'html';
2286 $template_type = 'HTML';
2289 # search for the template
2290 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
2291 name => $print_form->{formname},
2292 extension => $template_ext,
2293 email => $print_form->{media} eq 'email',
2294 language => $params->{language},
2295 printer_id => $print_form->{printer_id},
2298 if (!defined $template_file) {
2299 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);
2302 return @errors if scalar @errors;
2304 $print_form->throw_on_error(sub {
2306 $print_form->prepare_for_printing;
2308 $$doc_ref = SL::Helper::CreatePDF->create_pdf(
2309 format => $print_form->{format},
2310 template_type => $template_type,
2311 template => $template_file,
2312 variables => $print_form,
2313 variable_content_types => {
2314 longdescription => 'html',
2315 partnotes => 'html',
2317 $::form->get_variable_content_types_for_cvars,
2321 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
2327 sub get_files_for_email_dialog {
2330 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
2332 return %files if !$::instance_conf->get_doc_storage;
2334 if ($self->order->id) {
2335 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
2336 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
2337 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
2338 $files{project_files} = [ SL::File->get_all( object_id => $self->order->globalproject_id, object_type => 'project', file_type => 'attachment') ];
2342 uniq_by { $_->{id} }
2344 +{ id => $_->part->id,
2345 partnumber => $_->part->partnumber }
2346 } @{$self->order->items_sorted};
2348 foreach my $part (@parts) {
2349 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
2350 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
2353 foreach my $key (keys %files) {
2354 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
2360 sub make_periodic_invoices_config_from_yaml {
2361 my ($yaml_config) = @_;
2363 return if !$yaml_config;
2364 my $attr = SL::YAML::Load($yaml_config);
2365 return if 'HASH' ne ref $attr;
2366 return SL::DB::PeriodicInvoicesConfig->new(%$attr);
2370 sub get_periodic_invoices_status {
2371 my ($self, $config) = @_;
2373 return if $self->type ne sales_order_type();
2374 return t8('not configured') if !$config;
2376 my $active = ('HASH' eq ref $config) ? $config->{active}
2377 : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
2378 : die "Cannot get status of periodic invoices config";
2380 return $active ? t8('active') : t8('inactive');
2384 my ($self, $action) = @_;
2386 return '' if none { lc($action)} qw(add edit);
2389 # $::locale->text("Add Sales Order");
2390 # $::locale->text("Add Purchase Order");
2391 # $::locale->text("Add Quotation");
2392 # $::locale->text("Add Request for Quotation");
2393 # $::locale->text("Edit Sales Order");
2394 # $::locale->text("Edit Purchase Order");
2395 # $::locale->text("Edit Quotation");
2396 # $::locale->text("Edit Request for Quotation");
2398 $action = ucfirst(lc($action));
2399 return $self->type eq sales_order_type() ? $::locale->text("$action Sales Order")
2400 : $self->type eq purchase_order_type() ? $::locale->text("$action Purchase Order")
2401 : $self->type eq sales_quotation_type() ? $::locale->text("$action Quotation")
2402 : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
2406 sub get_item_cvpartnumber {
2407 my ($self, $item) = @_;
2409 return if !$self->search_cvpartnumber;
2410 return if !$self->order->customervendor;
2412 if ($self->cv eq 'vendor') {
2413 my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
2414 $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
2415 } elsif ($self->cv eq 'customer') {
2416 my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
2417 $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
2421 sub get_part_texts {
2422 my ($part_or_id, $language_or_id, %defaults) = @_;
2424 my $part = ref($part_or_id) ? $part_or_id : SL::DB::Part->load_cached($part_or_id);
2425 my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
2427 description => $defaults{description} // $part->description,
2428 longdescription => $defaults{longdescription} // $part->notes,
2431 return $texts unless $language_id;
2433 my $translation = SL::DB::Manager::Translation->get_first(
2435 parts_id => $part->id,
2436 language_id => $language_id,
2439 $texts->{description} = $translation->translation if $translation && $translation->translation;
2440 $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
2445 sub sales_order_type {
2449 sub purchase_order_type {
2453 sub sales_quotation_type {
2457 sub request_quotation_type {
2458 'request_quotation';
2462 return $_[0]->type eq sales_order_type() ? 'ordnumber'
2463 : $_[0]->type eq purchase_order_type() ? 'ordnumber'
2464 : $_[0]->type eq sales_quotation_type() ? 'quonumber'
2465 : $_[0]->type eq request_quotation_type() ? 'quonumber'
2469 sub save_and_redirect_to {
2470 my ($self, %params) = @_;
2472 my $errors = $self->save();
2474 if (scalar @{ $errors }) {
2475 $self->js->flash('error', $_) foreach @{ $errors };
2476 return $self->js->render();
2479 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
2480 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
2481 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
2482 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
2484 flash_later('info', $text);
2486 $self->redirect_to(%params, id => $self->order->id);
2490 my ($self, $addition) = @_;
2492 my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
2493 my $snumbers = $number_type . '_' . $self->order->$number_type;
2495 SL::DB::History->new(
2496 trans_id => $self->order->id,
2497 employee_id => SL::DB::Manager::Employee->current->id,
2498 what_done => $self->order->type,
2499 snumbers => $snumbers,
2500 addition => $addition,
2504 sub store_doc_to_webdav_and_filemanagement {
2505 my ($self, $content, $filename, $variant) = @_;
2507 my $order = $self->order;
2510 # copy file to webdav folder
2511 if ($order->number && $::instance_conf->get_webdav_documents) {
2512 my $webdav = SL::Webdav->new(
2513 type => $order->type,
2514 number => $order->number,
2516 my $webdav_file = SL::Webdav::File->new(
2518 filename => $filename,
2521 $webdav_file->store(data => \$content);
2524 push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
2527 if ($order->id && $::instance_conf->get_doc_storage) {
2529 SL::File->save(object_id => $order->id,
2530 object_type => $order->type,
2531 mime_type => SL::MIME->mime_type_from_ext($filename),
2532 source => 'created',
2533 file_type => 'document',
2534 file_name => $filename,
2535 file_contents => $content,
2536 print_variant => $variant);
2539 push @errors, t8('Storing the document in the storage backend failed: #1', $@);
2546 sub link_requirement_specs_linking_to_created_from_objects {
2547 my ($self, @converted_from_oe_ids) = @_;
2549 return unless @converted_from_oe_ids;
2551 my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
2552 foreach my $rs_order (@{ $rs_orders }) {
2553 SL::DB::RequirementSpecOrder->new(
2554 order_id => $self->order->id,
2555 requirement_spec_id => $rs_order->requirement_spec_id,
2556 version_id => $rs_order->version_id,
2561 sub set_project_in_linked_requirement_specs {
2564 my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
2565 foreach my $rs_order (@{ $rs_orders }) {
2566 next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
2568 $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
2580 SL::Controller::Order - controller for orders
2584 This is a new form to enter orders, completely rewritten with the use
2585 of controller and java script techniques.
2587 The aim is to provide the user a better experience and a faster workflow. Also
2588 the code should be more readable, more reliable and better to maintain.
2596 One input row, so that input happens every time at the same place.
2600 Use of pickers where possible.
2604 Possibility to enter more than one item at once.
2608 Item list in a scrollable area, so that the workflow buttons stay at
2613 Reordering item rows with drag and drop is possible. Sorting item rows is
2614 possible (by partnumber, description, qty, sellprice and discount for now).
2618 No C<update> is necessary. All entries and calculations are managed
2619 with ajax-calls and the page only reloads on C<save>.
2623 User can see changes immediately, because of the use of java script
2634 =item * C<SL/Controller/Order.pm>
2638 =item * C<template/webpages/order/form.html>
2642 =item * C<template/webpages/order/tabs/basic_data.html>
2644 Main tab for basic_data.
2646 This is the only tab here for now. "linked records" and "webdav" tabs are
2647 reused from generic code.
2651 =item * C<template/webpages/order/tabs/_business_info_row.html>
2653 For displaying information on business type
2655 =item * C<template/webpages/order/tabs/_item_input.html>
2657 The input line for items
2659 =item * C<template/webpages/order/tabs/_row.html>
2661 One row for already entered items
2663 =item * C<template/webpages/order/tabs/_tax_row.html>
2665 Displaying tax information
2667 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
2669 Dialog for selecting price and discount sources
2673 =item * C<js/kivi.Order.js>
2675 java script functions
2685 =item * price sources: little symbols showing better price / better discount
2687 =item * select units in input row?
2689 =item * check for direct delivery (workflow sales order -> purchase order)
2691 =item * access rights
2693 =item * display weights
2697 =item * optional client/user behaviour
2699 (transactions has to be set - department has to be set -
2700 force project if enabled in client config)
2704 =head1 KNOWN BUGS AND CAVEATS
2710 Customer discount is not displayed as a valid discount in price source popup
2711 (this might be a bug in price sources)
2713 (I cannot reproduce this (Bernd))
2717 No indication that <shift>-up/down expands/collapses second row.
2721 Inline creation of parts is not currently supported
2725 Table header is not sticky in the scrolling area.
2729 Sorting does not include C<position>, neither does reordering.
2731 This behavior was implemented intentionally. But we can discuss, which behavior
2732 should be implemented.
2736 =head1 To discuss / Nice to have
2742 How to expand/collapse second row. Now it can be done clicking the icon or
2747 Possibility to select PriceSources in input row?
2751 This controller uses a (changed) copy of the template for the PriceSource
2752 dialog. Maybe there could be used one code source.
2756 Rounding-differences between this controller (PriceTaxCalculator) and the old
2757 form. This is not only a problem here, but also in all parts using the PTC.
2758 There exists a ticket and a patch. This patch should be testet.
2762 An indicator, if the actual inputs are saved (like in an
2763 editor or on text processing application).
2767 A warning when leaving the page without saveing unchanged inputs.
2774 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>