1 package SL::Controller::DeliveryOrder;
4 use parent qw(SL::Controller::Base);
6 use SL::Helper::Flash qw(flash_later);
7 use SL::Helper::Number qw(_format_number_units);
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);
22 use SL::DB::PartClassification;
23 use SL::DB::PartsGroup;
26 use SL::DB::RecordLink;
28 use SL::DB::Translation;
29 use SL::DB::TransferType;
31 use SL::Helper::CreatePDF qw(:all);
32 use SL::Helper::PrintOptions;
33 use SL::Helper::ShippedQty;
34 use SL::Helper::UserPreferences::PositionsScrollbar;
35 use SL::Helper::UserPreferences::UpdatePositions;
37 use SL::Controller::Helper::GetModels;
38 use SL::Controller::DeliveryOrder::TypeData;
40 use List::Util qw(first sum0);
41 use List::UtilsBy qw(sort_by uniq_by);
42 use List::MoreUtils qw(any none pairwise first_index);
43 use English qw(-no_match_vars);
48 use Rose::Object::MakeMethods::Generic
50 scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
51 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids type_data) ],
56 __PACKAGE__->run_before('check_auth');
58 __PACKAGE__->run_before('get_unalterable_data',
59 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
70 $self->order->transdate(DateTime->now_local());
71 $self->type_data->set_reqdate_by_type;
76 'delivery_order/form',
77 title => $self->get_title_for('add'),
78 %{$self->{template_args}}
82 sub action_add_from_order {
84 # this interfers with init_order
85 $self->{converted_from_oe_id} = delete $::form->{id};
87 # TODO copy data and remember to link them on save
92 # edit an existing order
100 # this is to edit an order from an unsaved order object
102 # set item ids to new fake id, to identify them as new items
103 foreach my $item (@{$self->order->items_sorted}) {
104 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
106 # trigger rendering values for second row as hidden, because they
107 # are loaded only on demand. So we need to keep the values from
109 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
114 'delivery_order/form',
115 title => $self->get_title_for('edit'),
116 %{$self->{template_args}}
120 # edit a collective order (consisting of one or more existing orders)
121 sub action_edit_collective {
125 my @multi_ids = map {
126 $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
127 } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
129 # fall back to add if no ids are given
130 if (scalar @multi_ids == 0) {
135 # fall back to save as new if only one id is given
136 if (scalar @multi_ids == 1) {
137 $self->order(SL::DB::DeliveryOrder->new(id => $multi_ids[0])->load);
138 $self->action_save_as_new();
142 # make new order from given orders
143 my @multi_orders = map { SL::DB::DeliveryOrder->new(id => $_)->load } @multi_ids;
144 $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
145 $self->order(SL::DB::DeliveryOrder->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
147 $self->action_edit();
154 my $errors = $self->delete();
156 if (scalar @{ $errors }) {
157 $self->js->flash('error', $_) foreach @{ $errors };
158 return $self->js->render();
161 flash_later('info', $self->type_data->text("delete"));
163 my @redirect_params = (
168 $self->redirect_to(@redirect_params);
175 my $errors = $self->save();
177 if (scalar @{ $errors }) {
178 $self->js->flash('error', $_) foreach @{ $errors };
179 return $self->js->render();
182 flash_later('info', $self->type_data->text("saved"));
184 my @redirect_params = (
187 id => $self->order->id,
190 $self->redirect_to(@redirect_params);
193 # save the order as new document an open it for edit
194 sub action_save_as_new {
197 my $order = $self->order;
200 $self->js->flash('error', t8('This object has not been saved yet.'));
201 return $self->js->render();
204 # load order from db to check if values changed
205 my $saved_order = SL::DB::DeliveryOrder->new(id => $order->id)->load;
208 # Lets assign a new number if the user hasn't changed the previous one.
209 # If it has been changed manually then use it as-is.
210 $new_attrs{number} = (trim($order->number) eq $saved_order->number)
212 : trim($order->number);
214 # Clear transdate unless changed
215 $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
216 ? DateTime->today_local
219 # Set new reqdate unless changed if it is enabled in client config
220 $new_attrs{reqdate} = $self->type_data->get_reqdate_by_type($order->reqdate, $saved_order->reqdate);
223 $new_attrs{employee} = SL::DB::Manager::Employee->current;
225 # Create new record from current one
226 $self->order(SL::DB::DeliveryOrder->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
228 # no linked records on save as new
229 delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
232 $self->action_save();
237 # This is called if "print" is pressed in the print dialog.
238 # If PDF creation was requested and succeeded, the pdf is offered for download
239 # via send_file (which uses ajax in this case).
243 my $errors = $self->save();
245 if (scalar @{ $errors }) {
246 $self->js->flash('error', $_) foreach @{ $errors };
247 return $self->js->render();
250 $self->js_reset_order_and_item_ids_after_save;
252 my $format = $::form->{print_options}->{format};
253 my $media = $::form->{print_options}->{media};
254 my $formname = $::form->{print_options}->{formname};
255 my $copies = $::form->{print_options}->{copies};
256 my $groupitems = $::form->{print_options}->{groupitems};
257 my $printer_id = $::form->{print_options}->{printer_id};
259 # only pdf and opendocument by now
260 if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
261 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
264 # only screen or printer by now
265 if (none { $media eq $_ } qw(screen printer)) {
266 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
269 # create a form for generate_attachment_filename
270 my $form = Form->new;
271 $form->{$self->nr_key()} = $self->order->number;
272 $form->{type} = $self->type;
273 $form->{format} = $format;
274 $form->{formname} = $formname;
275 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
276 my $pdf_filename = $form->generate_attachment_filename();
279 my @errors = generate_pdf($self->order, \$pdf, { format => $format,
280 formname => $formname,
281 language => $self->order->language,
282 printer_id => $printer_id,
283 groupitems => $groupitems });
284 if (scalar @errors) {
285 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
288 if ($media eq 'screen') {
290 $self->js->flash('info', t8('The PDF has been created'));
293 type => SL::MIME->mime_type_from_ext($pdf_filename),
294 name => $pdf_filename,
298 } elsif ($media eq 'printer') {
300 my $printer_id = $::form->{print_options}->{printer_id};
301 SL::DB::Printer->new(id => $printer_id)->load->print_document(
306 $self->js->flash('info', t8('The PDF has been printed'));
309 my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $pdf_filename);
310 if (scalar @warnings) {
311 $self->js->flash('warning', $_) for @warnings;
314 $self->save_history('PRINTED');
317 ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
320 sub action_preview_pdf {
323 my $errors = $self->save();
324 if (scalar @{ $errors }) {
325 $self->js->flash('error', $_) foreach @{ $errors };
326 return $self->js->render();
329 $self->js_reset_order_and_item_ids_after_save;
332 my $media = 'screen';
333 my $formname = $self->type;
336 # create a form for generate_attachment_filename
337 my $form = Form->new;
338 $form->{$self->nr_key()} = $self->order->number;
339 $form->{type} = $self->type;
340 $form->{format} = $format;
341 $form->{formname} = $formname;
342 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
343 my $pdf_filename = $form->generate_attachment_filename();
346 my @errors = generate_pdf($self->order, \$pdf, { format => $format,
347 formname => $formname,
348 language => $self->order->language,
350 if (scalar @errors) {
351 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
353 $self->save_history('PREVIEWED');
354 $self->js->flash('info', t8('The PDF has been previewed'));
358 type => SL::MIME->mime_type_from_ext($pdf_filename),
359 name => $pdf_filename,
364 # open the email dialog
365 sub action_save_and_show_email_dialog {
368 my $errors = $self->save();
370 if (scalar @{ $errors }) {
371 $self->js->flash('error', $_) foreach @{ $errors };
372 return $self->js->render();
375 my $cv_method = $self->cv;
377 if (!$self->order->$cv_method) {
378 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'))
383 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
384 $email_form->{to} ||= $self->order->$cv_method->email;
385 $email_form->{cc} = $self->order->$cv_method->cc;
386 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
387 # Todo: get addresses from shipto, if any
389 my $form = Form->new;
390 $form->{$self->nr_key()} = $self->order->number;
391 $form->{cusordnumber} = $self->order->cusordnumber;
392 $form->{formname} = $self->type;
393 $form->{type} = $self->type;
394 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
395 $form->{language_id} = $self->order->language->id if $self->order->language;
396 $form->{format} = 'pdf';
397 $form->{cp_id} = $self->order->contact->cp_id if $self->order->contact;
399 $email_form->{subject} = $form->generate_email_subject();
400 $email_form->{attachment_filename} = $form->generate_attachment_filename();
401 $email_form->{message} = $form->generate_email_body();
402 $email_form->{js_send_function} = 'kivi.Order.send_email()';
404 my %files = $self->get_files_for_email_dialog();
405 $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
406 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
407 email_form => $email_form,
408 show_bcc => $::auth->assert('email_bcc', 'may fail'),
410 is_customer => $self->cv eq 'customer',
411 ALL_EMPLOYEES => $self->{all_employees},
415 ->run('kivi.Order.show_email_dialog', $dialog_html)
422 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
423 sub action_send_email {
426 my $errors = $self->save();
428 if (scalar @{ $errors }) {
429 $self->js->run('kivi.Order.close_email_dialog');
430 $self->js->flash('error', $_) foreach @{ $errors };
431 return $self->js->render();
434 $self->js_reset_order_and_item_ids_after_save;
436 my $email_form = delete $::form->{email_form};
437 my %field_names = (to => 'email');
439 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
441 # for Form::cleanup which may be called in Form::send_email
442 $::form->{cwd} = getcwd();
443 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
445 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
446 $::form->{media} = 'email';
448 if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
450 my @errors = generate_pdf($self->order, \$pdf, {media => $::form->{media},
451 format => $::form->{print_options}->{format},
452 formname => $::form->{print_options}->{formname},
453 language => $self->order->language,
454 printer_id => $::form->{print_options}->{printer_id},
455 groupitems => $::form->{print_options}->{groupitems}});
456 if (scalar @errors) {
457 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
460 my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $::form->{attachment_filename});
461 if (scalar @warnings) {
462 flash_later('warning', $_) for @warnings;
465 my $sfile = SL::SessionFile::Random->new(mode => "w");
466 $sfile->fh->print($pdf);
469 $::form->{tmpfile} = $sfile->file_name;
470 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
473 $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
474 $::form->send_email(\%::myconfig, 'pdf');
477 my $intnotes = $self->order->intnotes;
478 $intnotes .= "\n\n" if $self->order->intnotes;
479 $intnotes .= t8('[email]') . "\n";
480 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
481 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
482 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
483 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
484 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
485 $intnotes .= t8('Message') . ": " . $::form->{message};
487 $self->order->update_attributes(intnotes => $intnotes);
489 $self->save_history('MAILED');
491 flash_later('info', t8('The email has been sent.'));
493 my @redirect_params = (
496 id => $self->order->id,
499 $self->redirect_to(@redirect_params);
502 # save the order and redirect to the frontend subroutine for a new
504 sub action_save_and_delivery_order {
507 $self->save_and_redirect_to(
508 controller => 'oe.pl',
509 action => 'oe_delivery_order_from_order',
513 # save the order and redirect to the frontend subroutine for a new
515 sub action_save_and_invoice {
518 $self->save_and_redirect_to(
519 controller => 'oe.pl',
520 action => 'oe_invoice_from_order',
524 # workflow from sales order to sales quotation
525 sub action_sales_quotation {
526 $_[0]->workflow_sales_or_request_for_quotation();
529 # workflow from sales order to sales quotation
530 sub action_request_for_quotation {
531 $_[0]->workflow_sales_or_request_for_quotation();
534 # workflow from sales quotation to sales order
535 sub action_sales_order {
536 $_[0]->workflow_sales_or_purchase_order();
539 # workflow from rfq to purchase order
540 sub action_purchase_order {
541 $_[0]->workflow_sales_or_purchase_order();
544 # workflow from purchase order to ap transaction
545 sub action_save_and_ap_transaction {
548 $self->save_and_redirect_to(
549 controller => 'ap.pl',
550 action => 'add_from_purchase_order',
554 # set form elements in respect to a changed customer or vendor
556 # This action is called on an change of the customer/vendor picker.
557 sub action_customer_vendor_changed {
560 setup_order_from_cv($self->order);
562 my $cv_method = $self->cv;
564 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
565 $self->js->show('#cp_row');
567 $self->js->hide('#cp_row');
570 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
571 $self->js->show('#shipto_selection');
573 $self->js->hide('#shipto_selection');
576 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
579 ->replaceWith('#order_cp_id', $self->build_contact_select)
580 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
581 ->replaceWith('#shipto_inputs ', $self->build_shipto_inputs)
582 ->replaceWith('#business_info_row', $self->build_business_info_row)
583 ->val( '#order_taxzone_id', $self->order->taxzone_id)
584 ->val( '#order_taxincluded', $self->order->taxincluded)
585 ->val( '#order_currency_id', $self->order->currency_id)
586 ->val( '#order_payment_id', $self->order->payment_id)
587 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
588 ->val( '#order_intnotes', $self->order->intnotes)
589 ->val( '#order_language_id', $self->order->$cv_method->language_id)
590 ->focus( '#order_' . $self->cv . '_id')
591 ->run('kivi.Order.update_exchangerate');
593 $self->js_redisplay_cvpartnumbers;
597 # open the dialog for customer/vendor details
598 sub action_show_customer_vendor_details_dialog {
601 my $is_customer = 'customer' eq $::form->{vc};
604 $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
606 $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
609 my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
610 $details{discount_as_percent} = $cv->discount_as_percent;
611 $details{creditlimt} = $cv->creditlimit_as_number;
612 $details{business} = $cv->business->description if $cv->business;
613 $details{language} = $cv->language_obj->description if $cv->language_obj;
614 $details{delivery_terms} = $cv->delivery_term->description if $cv->delivery_term;
615 $details{payment_terms} = $cv->payment->description if $cv->payment;
616 $details{pricegroup} = $cv->pricegroup->pricegroup if $is_customer && $cv->pricegroup;
618 foreach my $entry (@{ $cv->shipto }) {
619 push @{ $details{SHIPTO} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
621 foreach my $entry (@{ $cv->contacts }) {
622 push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
625 $_[0]->render('common/show_vc_details', { layout => 0 },
626 is_customer => $is_customer,
631 # called if a unit in an existing item row is changed
632 sub action_unit_changed {
635 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
636 my $item = $self->order->items_sorted->[$idx];
638 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
639 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
642 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
643 $self->js_redisplay_line_values;
647 # add an item row for a new item entered in the input row
648 sub action_add_item {
651 delete $::form->{add_item}->{create_part_type};
653 my $form_attr = $::form->{add_item};
655 return unless $form_attr->{parts_id};
657 my $item = new_item($self->order, $form_attr);
659 $self->order->add_items($item);
661 $self->get_item_cvpartnumber($item);
663 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
664 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
670 if ($::form->{insert_before_item_id}) {
672 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
675 ->append('#row_table_id', $row_as_html);
678 if ( $item->part->is_assortment ) {
679 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
680 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
681 my $attr = { parts_id => $assortment_item->parts_id,
682 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
683 unit => $assortment_item->unit,
684 description => $assortment_item->part->description,
686 my $item = new_item($self->order, $attr);
688 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
689 $item->discount(1) unless $assortment_item->charge;
691 $self->order->add_items( $item );
692 $self->get_item_cvpartnumber($item);
693 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
694 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
699 if ($::form->{insert_before_item_id}) {
701 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
704 ->append('#row_table_id', $row_as_html);
710 ->val('.add_item_input', '')
711 ->run('kivi.Order.init_row_handlers')
712 ->run('kivi.Order.renumber_positions')
713 ->focus('#add_item_parts_id_name');
715 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
720 # add item rows for multiple items at once
721 sub action_add_multi_items {
724 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
725 return $self->js->render() unless scalar @form_attr;
728 foreach my $attr (@form_attr) {
729 my $item = new_item($self->order, $attr);
731 if ( $item->part->is_assortment ) {
732 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
733 my $attr = { parts_id => $assortment_item->parts_id,
734 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
735 unit => $assortment_item->unit,
736 description => $assortment_item->part->description,
738 my $item = new_item($self->order, $attr);
740 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
741 $item->discount(1) unless $assortment_item->charge;
746 $self->order->add_items(@items);
748 foreach my $item (@items) {
749 $self->get_item_cvpartnumber($item);
750 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
751 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
757 if ($::form->{insert_before_item_id}) {
759 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
762 ->append('#row_table_id', $row_as_html);
767 ->run('kivi.Part.close_picker_dialogs')
768 ->run('kivi.Order.init_row_handlers')
769 ->run('kivi.Order.renumber_positions')
770 ->focus('#add_item_parts_id_name');
772 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
777 sub action_update_exchangerate {
781 is_standard => $self->order->currency_id == $::instance_conf->get_currency_id,
782 currency_name => $self->order->currency->name,
785 $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
788 # redisplay item rows if they are sorted by an attribute
789 sub action_reorder_items {
793 partnumber => sub { $_[0]->part->partnumber },
794 description => sub { $_[0]->description },
795 qty => sub { $_[0]->qty },
796 sellprice => sub { $_[0]->sellprice },
797 discount => sub { $_[0]->discount },
798 cvpartnumber => sub { $_[0]->{cvpartnumber} },
801 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
803 my $method = $sort_keys{$::form->{order_by}};
804 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
805 if ($::form->{sort_dir}) {
806 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
807 @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
809 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
812 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
813 @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
815 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
819 ->run('kivi.Order.redisplay_items', \@to_sort)
823 # show the popup to choose a price/discount source
824 sub action_price_popup {
827 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
828 my $item = $self->order->items_sorted->[$idx];
830 $self->render_price_dialog($item);
833 # save the order in a session variable and redirect to the part controller
834 sub action_create_part {
837 my $previousform = $::auth->save_form_in_session(non_scalars => 1);
839 my $callback = $self->url_for(
840 action => 'return_from_create_part',
841 type => $self->type, # type is needed for check_auth on return
842 previousform => $previousform,
845 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.'));
847 my @redirect_params = (
848 controller => 'Part',
850 part_type => $::form->{add_item}->{create_part_type},
851 callback => $callback,
855 $self->redirect_to(@redirect_params);
858 sub action_return_from_create_part {
861 $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
863 $::auth->restore_form_from_session(delete $::form->{previousform});
865 # set item ids to new fake id, to identify them as new items
866 foreach my $item (@{$self->order->items_sorted}) {
867 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
870 $self->get_unalterable_data();
873 # trigger rendering values for second row/longdescription as hidden,
874 # because they are loaded only on demand. So we need to keep the values
876 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
877 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
880 'delivery_order/form',
881 title => $self->get_title_for('edit'),
882 %{$self->{template_args}}
887 # load the second row for one or more items
889 # This action gets the html code for all items second rows by rendering a template for
890 # the second row and sets the html code via client js.
891 sub action_load_second_rows {
894 foreach my $item_id (@{ $::form->{item_ids} }) {
895 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
896 my $item = $self->order->items_sorted->[$idx];
898 $self->js_load_second_row($item, $item_id, 0);
901 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
906 # update description, notes and sellprice from master data
907 sub action_update_row_from_master_data {
910 foreach my $item_id (@{ $::form->{item_ids} }) {
911 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
912 my $item = $self->order->items_sorted->[$idx];
913 my $texts = get_part_texts($item->part, $self->order->language_id);
915 $item->description($texts->{description});
916 $item->longdescription($texts->{longdescription});
918 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
921 if ($item->part->is_assortment) {
922 # add assortment items with price 0, as the components carry the price
923 $price_src = $price_source->price_from_source("");
924 $price_src->price(0);
926 $price_src = $price_source->best_price
927 ? $price_source->best_price
928 : $price_source->price_from_source("");
929 $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
930 $price_src->price(0) if !$price_source->best_price;
934 $item->sellprice($price_src->price);
935 $item->active_price_source($price_src);
938 ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
939 ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
940 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
941 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
943 if ($self->search_cvpartnumber) {
944 $self->get_item_cvpartnumber($item);
945 $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
949 $self->js_redisplay_line_values;
954 sub action_transfer_stock {
957 if ($self->order->delivered) {
958 return $self->js->flash("error", t8('The parts for this order have already been transferred'))->render;
961 my $errors = $self->save;
964 $self->js->flash('error', $_) for @$errors;
965 return $self->js->render;
968 my $order = $self->order;
970 # TODO move to type data
971 my $trans_type = $self->type_data->properties('transfer') eq 'in'
972 ? SL::DB::Manager::TransferType->find_by(direction => "id", description => "stock")
973 : SL::DB::Manager::TransferType->find_by(direction => "out", deescription => "shipped");
975 my @transfer_requests;
977 for my $item (@{ $order->items_sorted }) {
978 for my $stock (@{ $item->delivery_order_stock_entries }) {
979 my $transfer = SL::DB::Inventory->new_from($stock);
980 $transfer->trans_type($trans_type);
982 push @transfer_requests, $transfer;
986 if (!@transfer_requests) {
987 $self->js->flash("error", t8("No stock to transfer"))->render;
990 SL::DB->with_transaction(sub {
991 $_->save for @transfer_requests;
992 $self->order->update_attributes(deliverd => 1);
995 $self->js->flash("info", t8("Stock transfered"))->render;
998 sub js_load_second_row {
999 my ($self, $item, $item_id, $do_parse) = @_;
1002 # Parse values from form (they are formated while rendering (template)).
1003 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1004 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
1005 foreach my $var (@{ $item->cvars_by_config }) {
1006 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1008 $item->parse_custom_variable_values;
1011 my $row_as_html = $self->p->render('delivery_order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
1014 ->html('#second_row_' . $item_id, $row_as_html)
1015 ->data('#second_row_' . $item_id, 'loaded', 1);
1018 sub js_redisplay_line_values {
1021 my $is_sales = $self->order->is_sales;
1023 # sales orders with margins
1028 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1029 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
1030 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
1031 ]} @{ $self->order->items_sorted };
1035 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1036 ]} @{ $self->order->items_sorted };
1040 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
1043 sub js_redisplay_cvpartnumbers {
1046 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1048 my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
1051 ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
1054 sub js_reset_order_and_item_ids_after_save {
1058 ->val('#id', $self->order->id)
1059 ->val('#converted_from_oe_id', '')
1060 ->val('#order_' . $self->nr_key(), $self->order->number);
1063 foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
1064 next if !$self->order->items_sorted->[$idx]->id;
1065 next if $form_item_id !~ m{^new};
1067 ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
1068 ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
1069 ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
1073 $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
1083 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
1084 die "Not a valid type for delivery order";
1087 $self->type($::form->{type});
1093 return $self->type_data->customervendor;
1096 sub init_search_cvpartnumber {
1099 my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1100 my $search_cvpartnumber;
1101 $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
1102 $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel() if $self->cv eq 'vendor';
1104 return $search_cvpartnumber;
1107 sub init_show_update_button {
1110 !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1121 sub init_all_price_factors {
1122 SL::DB::Manager::PriceFactor->get_all;
1125 sub init_part_picker_classification_ids {
1128 return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => $self->type_data->part_classification_query) } ];
1134 $::auth->assert($self->type_data->access || 'DOES_NOT_EXIST');
1137 # build the selection box for contacts
1139 # Needed, if customer/vendor changed.
1140 sub build_contact_select {
1143 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1144 value_key => 'cp_id',
1145 title_key => 'full_name_dep',
1146 default => $self->order->cp_id,
1148 style => 'width: 300px',
1152 # build the selection box for shiptos
1154 # Needed, if customer/vendor changed.
1155 sub build_shipto_select {
1158 select_tag('order.shipto_id',
1159 [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
1160 value_key => 'shipto_id',
1161 title_key => 'displayable_id',
1162 default => $self->order->shipto_id,
1164 style => 'width: 300px',
1168 # build the inputs for the cusom shipto dialog
1170 # Needed, if customer/vendor changed.
1171 sub build_shipto_inputs {
1174 my $content = $self->p->render('common/_ship_to_dialog',
1175 vc_obj => $self->order->customervendor,
1176 cs_obj => $self->order->custom_shipto,
1177 cvars => $self->order->custom_shipto->cvars_by_config,
1178 id_selector => '#order_shipto_id');
1180 div_tag($content, id => 'shipto_inputs');
1183 # render the info line for business
1185 # Needed, if customer/vendor changed.
1186 sub build_business_info_row
1188 $_[0]->p->render('delivery_order/tabs/_business_info_row', SELF => $_[0]);
1195 return if !$::form->{id};
1197 $self->order(SL::DB::DeliveryOrder->new(id => $::form->{id})->load);
1199 # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
1200 # You need a custom shipto object to call cvars_by_config to get the cvars.
1201 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
1203 return $self->order;
1206 # load or create a new order object
1208 # And assign changes from the form to this object.
1209 # If the order is loaded from db, check if items are deleted in the form,
1210 # remove them form the object and collect them for removing from db on saving.
1211 # Then create/update items from form (via make_item) and add them.
1215 # add_items adds items to an order with no items for saving, but they cannot
1216 # be retrieved via items until the order is saved. Adding empty items to new
1217 # order here solves this problem.
1219 $order = SL::DB::DeliveryOrder->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
1220 $order ||= SL::DB::DeliveryOrder->new(orderitems => [], currency_id => $::instance_conf->get_currency_id(), order_type => $self->type_data->validate($::form->{type}));
1222 my $cv_id_method = $self->cv . '_id';
1223 if (!$::form->{id} && $::form->{$cv_id_method}) {
1224 $order->$cv_id_method($::form->{$cv_id_method});
1225 setup_order_from_cv($order);
1228 my $form_orderitems = delete $::form->{order}->{orderitems};
1230 $order->assign_attributes(%{$::form->{order}});
1232 $self->setup_custom_shipto_from_form($order, $::form);
1234 # remove deleted items
1235 $self->item_ids_to_delete([]);
1236 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1237 my $item = $order->orderitems->[$idx];
1238 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1239 splice @{$order->orderitems}, $idx, 1;
1240 push @{$self->item_ids_to_delete}, $item->id;
1246 foreach my $form_attr (@{$form_orderitems}) {
1247 my $item = make_item($order, $form_attr);
1248 $item->position($pos);
1252 $order->add_items(grep {!$_->id} @items);
1257 # create or update items from form
1259 # Make item objects from form values. For items already existing read from db.
1260 # Create a new item else. And assign attributes.
1262 my ($record, $attr) = @_;
1265 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1267 my $is_new = !$item;
1269 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1270 # they cannot be retrieved via custom_variables until the order/orderitem is
1271 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1272 $item ||= SL::DB::DeliveryOrderItem->new(custom_variables => []);
1274 $item->assign_attributes(%$attr);
1277 my $texts = get_part_texts($item->part, $record->language_id);
1278 $item->longdescription($texts->{longdescription}) if !defined $attr->{longdescription};
1279 $item->project_id($record->globalproject_id) if !defined $attr->{project_id};
1280 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1288 # This is used to add one item
1290 my ($record, $attr) = @_;
1292 my $item = SL::DB::DeliveryOrderItem->new;
1294 # Remove attributes where the user left or set the inputs empty.
1295 # So these attributes will be undefined and we can distinguish them
1296 # from zero later on.
1297 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1298 delete $attr->{$_} if $attr->{$_} eq '';
1301 $item->assign_attributes(%$attr);
1303 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1304 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1306 $item->unit($part->unit) if !$item->unit;
1309 if ( $part->is_assortment ) {
1310 # add assortment items with price 0, as the components carry the price
1311 $price_src = $price_source->price_from_source("");
1312 $price_src->price(0);
1313 } elsif (defined $item->sellprice) {
1314 $price_src = $price_source->price_from_source("");
1315 $price_src->price($item->sellprice);
1317 $price_src = $price_source->best_price
1318 ? $price_source->best_price
1319 : $price_source->price_from_source("");
1320 $price_src->price(0) if !$price_source->best_price;
1324 if (defined $item->discount) {
1325 $discount_src = $price_source->discount_from_source("");
1326 $discount_src->discount($item->discount);
1328 $discount_src = $price_source->best_discount
1329 ? $price_source->best_discount
1330 : $price_source->discount_from_source("");
1331 $discount_src->discount(0) if !$price_source->best_discount;
1335 $new_attr{part} = $part;
1336 $new_attr{description} = $part->description if ! $item->description;
1337 $new_attr{qty} = 1.0 if ! $item->qty;
1338 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1339 $new_attr{sellprice} = $price_src->price;
1340 $new_attr{discount} = $discount_src->discount;
1341 $new_attr{active_price_source} = $price_src;
1342 $new_attr{active_discount_source} = $discount_src;
1343 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1344 $new_attr{project_id} = $record->globalproject_id;
1345 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1347 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1348 # they cannot be retrieved via custom_variables until the order/orderitem is
1349 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1350 $new_attr{custom_variables} = [];
1352 my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
1354 $item->assign_attributes(%new_attr, %{ $texts });
1359 sub setup_order_from_cv {
1362 $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
1364 $order->intnotes($order->customervendor->notes);
1366 if ($order->is_sales) {
1367 $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
1368 $order->taxincluded(defined($order->customer->taxincluded_checked)
1369 ? $order->customer->taxincluded_checked
1370 : $::myconfig{taxincluded_checked});
1375 # setup custom shipto from form
1377 # The dialog returns form variables starting with 'shipto' and cvars starting
1378 # with 'shiptocvar_'.
1379 # Mark it to be deleted if a shipto from master data is selected
1380 # (i.e. order has a shipto).
1381 # Else, update or create a new custom shipto. If the fields are empty, it
1382 # will not be saved on save.
1383 sub setup_custom_shipto_from_form {
1384 my ($self, $order, $form) = @_;
1386 if ($order->shipto) {
1387 $self->is_custom_shipto_to_delete(1);
1389 my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1391 my $shipto_cvars = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
1392 my $shipto_attrs = {map { $_ => delete $form->{$_}} grep { m{^shipto} } keys %$form};
1394 $custom_shipto->assign_attributes(%$shipto_attrs);
1395 $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
1399 # get data for saving, printing, ..., that is not changed in the form
1401 # Only cvars for now.
1402 sub get_unalterable_data {
1405 foreach my $item (@{ $self->order->items }) {
1406 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1407 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1408 foreach my $var (@{ $item->cvars_by_config }) {
1409 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1411 $item->parse_custom_variable_values;
1417 # And remove related files in the spool directory
1422 my $db = $self->order->db;
1424 $db->with_transaction(
1426 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1427 $self->order->delete;
1428 my $spool = $::lx_office_conf{paths}->{spool};
1429 unlink map { "$spool/$_" } @spoolfiles if $spool;
1431 $self->save_history('DELETED');
1434 }) || push(@{$errors}, $db->error);
1441 # And delete items that are deleted in the form.
1446 my $db = $self->order->db;
1448 $db->with_transaction(sub {
1449 # delete custom shipto if it is to be deleted or if it is empty
1450 if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
1451 $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
1452 $self->order->custom_shipto(undef);
1455 SL::DB::DeliveryOrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
1456 $self->order->save(cascade => 1);
1459 if ($::form->{converted_from_oe_id}) {
1460 my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
1461 foreach my $converted_from_oe_id (@converted_from_oe_ids) {
1462 my $src = SL::DB::DeliveryOrder->new(id => $converted_from_oe_id)->load;
1463 $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
1464 $src->link_to_record($self->order);
1466 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1468 foreach (@{ $self->order->items_sorted }) {
1469 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1471 SL::DB::RecordLink->new(from_table => 'orderitems',
1472 from_id => $from_id,
1473 to_table => 'orderitems',
1481 $self->save_history('SAVED');
1484 }) || push(@{$errors}, $db->error);
1489 sub workflow_sales_or_request_for_quotation {
1493 my $errors = $self->save();
1495 if (scalar @{ $errors }) {
1496 $self->js->flash('error', $_) for @{ $errors };
1497 return $self->js->render();
1500 my $destination_type = $self->type_data->workflow("to_quotation_type");
1502 $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
1503 $self->{converted_from_oe_id} = delete $::form->{id};
1505 # set item ids to new fake id, to identify them as new items
1506 foreach my $item (@{$self->order->items_sorted}) {
1507 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1511 $::form->{type} = $destination_type;
1512 $self->type($self->init_type);
1513 $self->cv ($self->init_cv);
1516 $self->get_unalterable_data();
1517 $self->pre_render();
1519 # trigger rendering values for second row as hidden, because they
1520 # are loaded only on demand. So we need to keep the values from the
1522 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1525 'delivery_order/form',
1526 title => $self->get_title_for('edit'),
1527 %{$self->{template_args}}
1531 sub workflow_sales_or_purchase_order {
1535 my $errors = $self->save();
1537 if (scalar @{ $errors }) {
1538 $self->js->flash('error', $_) foreach @{ $errors };
1539 return $self->js->render();
1542 my $destination_type = $self->type_data->workflow("to_order_type");
1544 # check for direct delivery
1545 # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
1547 if ($self->type_data->workflow("to_order_copy_shipto") && $::form->{use_shipto} && $self->order->shipto) {
1548 $custom_shipto = $self->order->shipto->clone('SL::DB::DeliveryOrder');
1551 $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
1552 $self->{converted_from_oe_id} = delete $::form->{id};
1554 # set item ids to new fake id, to identify them as new items
1555 foreach my $item (@{$self->order->items_sorted}) {
1556 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1559 if ($self->type_data->workflow("to_order_copy_shipto")) {
1560 if ($::form->{use_shipto}) {
1561 $self->order->custom_shipto($custom_shipto) if $custom_shipto;
1563 # remove any custom shipto if not wanted
1564 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1569 $::form->{type} = $destination_type;
1570 $self->type($self->init_type);
1571 $self->cv ($self->init_cv);
1574 $self->get_unalterable_data();
1575 $self->pre_render();
1577 # trigger rendering values for second row as hidden, because they
1578 # are loaded only on demand. So we need to keep the values from the
1580 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1583 'delivery_order/form',
1584 title => $self->get_title_for('edit'),
1585 %{$self->{template_args}}
1592 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1593 $self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
1594 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1595 $self->{all_languages} = SL::DB::Manager::Language->get_all_sorted();
1596 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1599 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1602 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1604 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1605 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1606 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1607 $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
1609 my $print_form = Form->new('');
1610 $print_form->{type} = $self->type;
1611 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1612 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1613 form => $print_form,
1614 options => {dialog_name_prefix => 'print_options.',
1618 no_opendocument => 0,
1622 foreach my $item (@{$self->order->orderitems}) {
1623 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1624 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1625 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1628 if ($self->order->${\ $self->type_data->nr_key } && $::instance_conf->get_webdav) {
1629 my $webdav = SL::Webdav->new(
1630 type => $self->type,
1631 number => $self->order->number,
1633 my @all_objects = $webdav->get_all_objects;
1634 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1636 link => File::Spec->catfile($_->full_filedescriptor),
1640 $self->{template_args}{inout} = $self->type_data->properties('transfer');
1642 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1644 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.DeliveryOrder kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
1645 calculate_qty kivi.Validator follow_up show_history);
1646 $self->setup_edit_action_bar;
1649 sub setup_edit_action_bar {
1650 my ($self, %params) = @_;
1652 my $deletion_allowed = $self->type_data->show_menu("delete");
1654 for my $bar ($::request->layout->get('actionbar')) {
1659 call => [ 'kivi.DeliveryOrder.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
1660 $::instance_conf->get_order_warn_no_deliverydate,
1665 call => [ 'kivi.DeliveryOrder.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
1666 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1668 ], # end of combobox "Save"
1675 t8('Save and Quotation'),
1676 submit => [ '#order_form', { action => "DeliveryOrder/sales_quotation" } ],
1677 only_if => $self->type_data->show_menu("save_and_quotation"),
1681 submit => [ '#order_form', { action => "DeliveryOrder/request_for_quotation" } ],
1682 only_if => $self->type_data->show_menu("save_and_rfq"),
1685 t8('Save and Sales Order'),
1686 submit => [ '#order_form', { action => "DeliveryOrder/sales_order" } ],
1687 only_if => $self->type_data->show_menu("save_and_sales_order"),
1690 t8('Save and Purchase Order'),
1691 call => [ 'kivi.DeliveryOrder.purchase_order_check_for_direct_delivery' ],
1692 only_if => $self->type_data->show_menu("save_and_purchase_order"),
1695 t8('Save and Delivery Order'),
1696 call => [ 'kivi.DeliveryOrder.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
1697 $::instance_conf->get_order_warn_no_deliverydate,
1699 only_if => $self->type_data->show_menu("save_and_delivery_order"),
1702 t8('Save and Invoice'),
1703 call => [ 'kivi.DeliveryOrder.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1704 only_if => $self->type_data->show_menu("save_and_invoice"),
1707 t8('Save and AP Transaction'),
1708 call => [ 'kivi.DeliveryOrder.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
1709 only_if => $self->type_data->show_menu("save_and_ap_transaction"),
1712 ], # end of combobox "Workflow"
1719 t8('Save and preview PDF'),
1720 call => [ 'kivi.DeliveryOrder.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
1721 $::instance_conf->get_order_warn_no_deliverydate,
1725 t8('Save and print'),
1726 call => [ 'kivi.DeliveryOrder.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
1727 $::instance_conf->get_order_warn_no_deliverydate,
1731 t8('Save and E-mail'),
1732 id => 'save_and_email_action',
1733 call => [ 'kivi.DeliveryOrder.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
1734 $::instance_conf->get_order_warn_no_deliverydate,
1736 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1739 t8('Download attachments of all parts'),
1740 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1741 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1742 only_if => $::instance_conf->get_doc_storage,
1744 ], # end of combobox "Export"
1748 call => [ 'kivi.DeliveryOrder.delete_order' ],
1749 confirm => $::locale->text('Do you really want to delete this object?'),
1750 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1751 only_if => $self->type_data->show_menu("delete"),
1757 submit => [ '#order_form', { action => "DeliveryOrder/transfer_stock" } ],
1758 disabled => $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
1759 only_if => $self->type_data->properties('transfer') eq 'out',
1760 confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
1764 submit => [ '#order_form', { action => "DeliveryOrder/transfer_stock" } ],
1765 disabled => $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
1766 only_if => $self->type_data->properties('transfer') eq 'in',
1767 confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
1777 call => [ 'kivi.DeliveryOrder.follow_up_window' ],
1778 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1779 only_if => $::auth->assert('productivity', 1),
1783 call => [ 'set_history_window', $self->order->id, 'id' ],
1784 disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
1786 ], # end of combobox "more"
1792 my ($order, $pdf_ref, $params) = @_;
1796 my $print_form = Form->new('');
1797 $print_form->{type} = $order->type;
1798 $print_form->{formname} = $params->{formname} || $order->type;
1799 $print_form->{format} = $params->{format} || 'pdf';
1800 $print_form->{media} = $params->{media} || 'file';
1801 $print_form->{groupitems} = $params->{groupitems};
1802 $print_form->{printer_id} = $params->{printer_id};
1803 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
1805 $order->language($params->{language});
1806 $order->flatten_to_form($print_form, format_amounts => 1);
1810 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
1811 $template_ext = 'odt';
1812 $template_type = 'OpenDocument';
1815 # search for the template
1816 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1817 name => $print_form->{formname},
1818 extension => $template_ext,
1819 email => $print_form->{media} eq 'email',
1820 language => $params->{language},
1821 printer_id => $print_form->{printer_id},
1824 if (!defined $template_file) {
1825 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);
1828 return @errors if scalar @errors;
1830 $print_form->throw_on_error(sub {
1832 $print_form->prepare_for_printing;
1834 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1835 format => $print_form->{format},
1836 template_type => $template_type,
1837 template => $template_file,
1838 variables => $print_form,
1839 variable_content_types => {
1840 longdescription => 'html',
1841 partnotes => 'html',
1846 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
1852 sub get_files_for_email_dialog {
1855 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1857 return %files if !$::instance_conf->get_doc_storage;
1859 if ($self->order->id) {
1860 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
1861 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
1862 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
1863 $files{project_files} = [ SL::File->get_all( object_id => $self->order->globalproject_id, object_type => 'project', file_type => 'attachment') ];
1867 uniq_by { $_->{id} }
1869 +{ id => $_->part->id,
1870 partnumber => $_->part->partnumber }
1871 } @{$self->order->items_sorted};
1873 foreach my $part (@parts) {
1874 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1875 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1878 foreach my $key (keys %files) {
1879 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1886 my ($self, $action) = @_;
1888 return '' if none { lc($action)} qw(add edit);
1889 return $self->type_data->text($action);
1892 sub get_item_cvpartnumber {
1893 my ($self, $item) = @_;
1895 return if !$self->search_cvpartnumber;
1896 return if !$self->order->customervendor;
1898 if ($self->cv eq 'vendor') {
1899 my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
1900 $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
1901 } elsif ($self->cv eq 'customer') {
1902 my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
1903 $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
1907 sub get_part_texts {
1908 my ($part_or_id, $language_or_id, %defaults) = @_;
1910 my $part = ref($part_or_id) ? $part_or_id : SL::DB::Part->load_cached($part_or_id);
1911 my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
1913 description => $defaults{description} // $part->description,
1914 longdescription => $defaults{longdescription} // $part->notes,
1917 return $texts unless $language_id;
1919 my $translation = SL::DB::Manager::Translation->get_first(
1921 parts_id => $part->id,
1922 language_id => $language_id,
1925 $texts->{description} = $translation->translation if $translation && $translation->translation;
1926 $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
1932 return $_[0]->type_data->nr_key;
1935 sub save_and_redirect_to {
1936 my ($self, %params) = @_;
1938 my $errors = $self->save();
1940 if (scalar @{ $errors }) {
1941 $self->js->flash('error', $_) foreach @{ $errors };
1942 return $self->js->render();
1945 flash_later('info', $self->type_data->text("saved"));
1947 $self->redirect_to(%params, id => $self->order->id);
1951 my ($self, $addition) = @_;
1953 my $number_type = $self->nr_key;
1954 my $snumbers = $number_type . '_' . $self->order->$number_type;
1956 SL::DB::History->new(
1957 trans_id => $self->order->id,
1958 employee_id => SL::DB::Manager::Employee->current->id,
1959 what_done => $self->order->type,
1960 snumbers => $snumbers,
1961 addition => $addition,
1965 sub store_pdf_to_webdav_and_filemanagement {
1966 my($order, $content, $filename) = @_;
1970 # copy file to webdav folder
1971 if ($order->number && $::instance_conf->get_webdav_documents) {
1972 my $webdav = SL::Webdav->new(
1973 type => $order->type,
1974 number => $order->number,
1976 my $webdav_file = SL::Webdav::File->new(
1978 filename => $filename,
1981 $webdav_file->store(data => \$content);
1984 push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
1987 if ($order->id && $::instance_conf->get_doc_storage) {
1989 SL::File->save(object_id => $order->id,
1990 object_type => $order->type,
1991 mime_type => 'application/pdf',
1992 source => 'created',
1993 file_type => 'document',
1994 file_name => $filename,
1995 file_contents => $content);
1998 push @errors, t8('Storing PDF in storage backend failed: #1', $@);
2005 sub calculate_stock_in_out {
2006 my ($self, $item) = @_;
2008 return "" if !$item->part || !$item->part->unit || !$item->unit;
2010 my $in_out = $self->type_data->properties("transfer");
2012 my $do_qty = $item->qty;
2013 my $sum = sum0 map {
2014 $_->unit_obj->convert_to($_->qty, $item->unit_obj)
2015 } $item->delivery_order_stock_entries;
2017 my $matches = $do_qty == $sum;
2018 my $content = _format_number_units($sum, 2, $item->unit_obj, $item->part->unit_obj);
2023 sub init_type_data {
2024 SL::Controller::DeliveryOrder::TypeData->new($_[0]);
2027 sub init_valid_types {
2028 $_[0]->type_data->valid_types;
2039 SL::Controller::Order - controller for orders
2043 This is a new form to enter orders, completely rewritten with the use
2044 of controller and java script techniques.
2046 The aim is to provide the user a better experience and a faster workflow. Also
2047 the code should be more readable, more reliable and better to maintain.
2055 One input row, so that input happens every time at the same place.
2059 Use of pickers where possible.
2063 Possibility to enter more than one item at once.
2067 Item list in a scrollable area, so that the workflow buttons stay at
2072 Reordering item rows with drag and drop is possible. Sorting item rows is
2073 possible (by partnumber, description, qty, sellprice and discount for now).
2077 No C<update> is necessary. All entries and calculations are managed
2078 with ajax-calls and the page only reloads on C<save>.
2082 User can see changes immediately, because of the use of java script
2093 =item * C<SL/Controller/Order.pm>
2097 =item * C<template/webpages/delivery_order/form.html>
2101 =item * C<template/webpages/delivery_order/tabs/basic_data.html>
2103 Main tab for basic_data.
2105 This is the only tab here for now. "linked records" and "webdav" tabs are
2106 reused from generic code.
2110 =item * C<template/webpages/delivery_order/tabs/_business_info_row.html>
2112 For displaying information on business type
2114 =item * C<template/webpages/delivery_order/tabs/_item_input.html>
2116 The input line for items
2118 =item * C<template/webpages/delivery_order/tabs/_row.html>
2120 One row for already entered items
2122 =item * C<template/webpages/delivery_order/tabs/_tax_row.html>
2124 Displaying tax information
2128 =item * C<js/kivi.DeliveryOrder.js>
2130 java script functions
2140 =item * price sources: little symbols showing better price / better discount
2142 =item * select units in input row?
2144 =item * check for direct delivery (workflow sales order -> purchase order)
2146 =item * access rights
2148 =item * display weights
2152 =item * optional client/user behaviour
2154 (transactions has to be set - department has to be set -
2155 force project if enabled in client config - transport cost reminder)
2159 =head1 KNOWN BUGS AND CAVEATS
2165 Customer discount is not displayed as a valid discount in price source popup
2166 (this might be a bug in price sources)
2168 (I cannot reproduce this (Bernd))
2172 No indication that <shift>-up/down expands/collapses second row.
2176 Inline creation of parts is not currently supported
2180 Table header is not sticky in the scrolling area.
2184 Sorting does not include C<position>, neither does reordering.
2186 This behavior was implemented intentionally. But we can discuss, which behavior
2187 should be implemented.
2191 =head1 To discuss / Nice to have
2197 How to expand/collapse second row. Now it can be done clicking the icon or
2202 Possibility to select PriceSources in input row?
2206 This controller uses a (changed) copy of the template for the PriceSource
2207 dialog. Maybe there could be used one code source.
2211 Rounding-differences between this controller (PriceTaxCalculator) and the old
2212 form. This is not only a problem here, but also in all parts using the PTC.
2213 There exists a ticket and a patch. This patch should be testet.
2217 An indicator, if the actual inputs are saved (like in an
2218 editor or on text processing application).
2222 A warning when leaving the page without saveing unchanged inputs.
2229 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>