+
+sub _remove_billed_or_delivered_rows {
+ my (%params) = @_;
+
+ croak "Missing parameter 'quantities'" if !$params{quantities};
+
+ my @fields = map { s/_1$//; $_ } grep { m/_1$/ } keys %{ $::form };
+ my @new_rows;
+
+ my $make_key = sub {
+ my ($row) = @_;
+ return $::form->{"id_${row}"} unless $::form->{"serialnumber_${row}"};
+ my $key = $::form->{"id_${row}"} . ':' . $::form->{"serialnumber_${row}"};
+ return exists $params{quantities}->{$key} ? $key : $::form->{"id_${row}"};
+ };
+
+ my $removed_rows = 0;
+ my $row = 0;
+ while ($row < $::form->{rowcount}) {
+ $row++;
+ next unless $::form->{"id_$row"};
+
+ my $parts_id = $::form->{"id_$row"};
+ my $base_qty = $::form->parse_amount(\%::myconfig, $::form->{"qty_$row"}) * SL::DB::Manager::Unit->find_by(name => $::form->{"unit_$row"})->base_factor;
+
+ my $key = $make_key->($row);
+ my $sub_qty = min($base_qty, $params{quantities}->{$key});
+ $params{quantities}->{$key} -= $sub_qty;
+
+ if (!$sub_qty || ($sub_qty != $base_qty)) {
+ $::form->{"qty_${row}"} = $::form->format_amount(\%::myconfig, ($base_qty - $sub_qty) / SL::DB::Manager::Unit->find_by(name => $::form->{"unit_$row"})->base_factor);
+ push @new_rows, { map { $_ => $::form->{"${_}_${row}"} } @fields };
+
+ } else {
+ $removed_rows++;
+ }
+ }
+
+ $::form->redo_rows(\@fields, \@new_rows, scalar(@new_rows), $::form->{rowcount});
+ $::form->{rowcount} -= $removed_rows;
+}
+
+# TODO: both of these are makeshift so that price sources can operate on rdbo objects. if
+# this ever gets rewritten in controller style, throw this out
+sub _make_record_item {
+ my ($row, %params) = @_;
+
+ my $class = {
+ sales_order => 'OrderItem',
+ purchase_order => 'OrderItem',
+ sales_quotation => 'OrderItem',
+ request_quotation => 'OrderItem',
+ invoice => 'InvoiceItem',
+ credit_note => 'InvoiceItem',
+ purchase_invoice => 'InvoiceItem',
+ purchase_delivery_order => 'DeliveryOrderItem',
+ sales_delivery_order => 'DeliveryOrderItem',
+ }->{$::form->{type}};
+
+ return unless $class;
+
+ $class = 'SL::DB::' . $class;
+
+ my %translated_methods = (
+ 'SL::DB::OrderItem' => {
+ id => 'parts_id',
+ orderitems_id => 'id',
+ },
+ 'SL::DB::DeliveryOrderItem' => {
+ id => 'parts_id',
+ delivery_order_items_id => 'id',
+ },
+ 'SL::DB::InvoiceItem' => {
+ id => 'parts_id',
+ invoice_id => 'id',
+ },
+ );
+
+ eval "require $class";
+
+ my $obj = $::form->{"orderitems_id_$row"}
+ ? $class->meta->convention_manager->auto_manager_class_name->find_by(id => $::form->{"orderitems_id_$row"})
+ : $class->new;
+
+ for my $key (grep { /_$row$/ } keys %$::form) {
+ my $method = $key;
+ $method =~ s/_$row$//;
+ $method = $translated_methods{$class}{$method} // $method;
+ my $value = $::form->{$key};
+ if ($obj->meta->column($method)) {
+ if ($obj->meta->column($method)->isa('Rose::DB::Object::Metadata::Column::Date')) {
+ $obj->${\"$method\_as_date"}($value);
+ } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Numeric|Float|DoublePrecsion)$/) {
+ $obj->${\"$method\_as_number"}(($value // '') eq '' ? undef : $value);
+ } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::Boolean$/) {
+ $obj->$method(!!$value);
+ } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Big)?(?:Int(?:eger)?|Serial)$/) {
+ $obj->$method(($value // '') eq '' ? undef : $value * 1);
+ } else {
+ $obj->$method($value);
+ }
+
+ if ($method eq 'discount') {
+ $obj->discount($obj->discount / 100.0);
+ }
+
+ } else {
+ $obj->{__additional_form_attributes}{$method} = $value;
+ }
+ }
+
+ if ($::form->{"id_$row"}) {
+ $obj->part(SL::DB::Part->load_cached($::form->{"id_$row"}));
+ }
+
+ if ($obj->can('qty')) {
+ $obj->qty( $obj->qty * $params{factor});
+ $obj->base_qty($obj->base_qty * $params{factor});
+ }
+
+ return $obj;
+}
+
+sub _make_record {
+ my $class = {
+ sales_order => 'Order',
+ purchase_order => 'Order',
+ sales_quotation => 'Order',
+ request_quotation => 'Order',
+ purchase_delivery_order => 'DeliveryOrder',
+ sales_delivery_order => 'DeliveryOrder',
+ }->{$::form->{type}};
+
+ if ($::form->{type} =~ /invoice|credit_note/) {
+ $class = $::form->{vc} eq 'customer' ? 'Invoice'
+ : $::form->{vc} eq 'vendor' ? 'PurchaseInvoice'
+ : do { die 'unknown invoice type' };
+ }
+
+ my $factor = $::form->{type} =~ m{credit_note} ? -1 : 1;
+
+ return unless $class;
+
+ $class = 'SL::DB::' . $class;
+
+ eval "require $class";
+
+ my $obj = $::form->{id}
+ ? $class->meta->convention_manager->auto_manager_class_name->find_by(id => $::form->{id})
+ : $class->new;
+
+ for my $method (keys %$::form) {
+ next unless $obj->can($method);
+ next unless $obj->meta->column($method);
+
+ if ($obj->meta->column($method)->isa('Rose::DB::Object::Metadata::Column::Date')) {
+ $obj->${\"$method\_as_date"}($::form->{$method});
+ } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Numeric|Float|DoublePrecsion)$/) {
+ $obj->${\"$method\_as_number"}(($::form->{$method} // '') eq '' ? undef : $::form->{$method});
+ } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::Boolean$/) {
+ $obj->$method(!!$::form->{$method});
+ } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Big)?(?:Int(?:eger)?|Serial)$/) {
+ $obj->$method(($::form->{$method} // '') eq '' ? undef : $::form->{$method} * 1);
+ } else {
+ $obj->$method($::form->{$method});
+ }
+ }
+
+ my @items;
+ for my $i (1 .. $::form->{rowcount}) {
+ next unless $::form->{"id_$i"};
+ push @items, _make_record_item($i, factor => $factor);
+ }
+
+ $obj->items(@items) if @items;
+ $obj->is_sales(!!$obj->customer_id) if $class eq 'SL::DB::DeliveryOrder';
+
+ if ($class eq 'SL::DB::Invoice') {
+ my $paid = $factor *
+ sum
+ map { $::form->parse_amount(\%::myconfig, $::form->{$_}) }
+ grep { m{^paid_\d+$} }
+ keys %{ $::form };
+ $obj->paid($paid);
+ }
+
+ return $obj;
+}
+
+sub setup_sales_purchase_print_options {
+ my $print_form = Form->new('');
+ $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
+ $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
+
+ $print_form->{$_} = $::form->{$_} for qw(type media language_id printer_id storno formname groupitems);
+
+ return SL::Helper::PrintOptions->get_print_options(
+ form => $print_form,
+ options => {
+ show_headers => 1,
+ },
+ );
+}
+
+sub _get_files_for_email_dialog {
+ my %files = map { ($_ => []) } qw(versions files vc_files part_files);
+
+ return %files if !$::instance_conf->get_doc_storage;
+
+ if ($::form->{id}) {
+ $files{versions} = [ SL::File->get_all_versions(object_id => $::form->{id}, object_type => $::form->{type}, file_type => 'document') ];
+ $files{files} = [ SL::File->get_all( object_id => $::form->{id}, object_type => $::form->{type}, file_type => 'attachment') ];
+ $files{vc_files} = [ SL::File->get_all( object_id => $::form->{vc_id}, object_type => $::form->{vc}, file_type => 'attachment') ]
+ if $::form->{vc} && $::form->{"vc_id"};
+ }
+
+ my @parts =
+ uniq_by { $_->{id} }
+ grep { $_->{id} }
+ map {
+ +{ id => $::form->{"id_$_"},
+ partnumber => $::form->{"partnumber_$_"},
+ }
+ } (1 .. $::form->{rowcount});
+
+ foreach my $part (@parts) {
+ my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
+ push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
+ }
+
+ foreach my $key (keys %files) {
+ $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
+ }
+
+ return %files;
+}
+
+sub show_sales_purchase_email_dialog {
+ my $email = '';
+ my $email_cc = '';
+ my $record_email;
+ if ($::form->{cp_id}) {
+ $email = SL::DB::Contact->load_cached($::form->{cp_id})->cp_email;
+ }
+ # write a dispatch table if a third type enters
+ # check record mail for sales_invoice
+ if ($::form->{type} eq 'invoice' && (!$email || $::instance_conf->get_invoice_mail_settings ne 'cp')) {
+ # check for invoice_mail if defined (vc.invoice_email)
+ $record_email = SL::DB::Customer->load_cached($::form->{vc_id})->invoice_mail;
+ if ($record_email) {
+ # check if cc for contact is also wanted
+ $email_cc = $email if ($::instance_conf->get_invoice_mail_settings eq 'invoice_mail_cc_cp');
+ $email = $record_email;
+ }
+ }
+ # check record mail for sales_delivery_order
+ if ($::form->{type} eq 'sales_delivery_order') {
+ # check for deliver_order_mail if defined (vc.delivery_order_mail)
+ $record_email = SL::DB::Customer->load_cached($::form->{vc_id})->delivery_order_mail;
+ if ($record_email) {
+ # check if cc for contact is also wanted
+ $email_cc = $email; # always cc to cp
+ $email = $record_email;
+ }
+ }
+ # still no email? use general mail (vc.email)
+ if (!$email && $::form->{vc} && $::form->{vc_id}) {
+ $email = SL::DB::Customer->load_cached($::form->{vc_id})->email if 'customer' eq $::form->{vc};
+ $email = SL::DB::Vendor ->load_cached($::form->{vc_id})->email if 'vendor' eq $::form->{vc};
+ }
+
+ $email = '' if $::form->{type} eq 'purchase_delivery_order';
+
+ $::form->{language} = $::form->get_template_language(\%::myconfig);
+ $::form->{language} = "_" . $::form->{language};
+
+ my %body_params = (record_email => $record_email);
+ if (($::form->{type} eq 'invoice') && $::form->{direct_debit}) {
+ $body_params{translation_type} = "preset_text_invoice_direct_debit";
+ $body_params{fallback_translation_type} = "preset_text_invoice";
+ }
+
+ my $email_form = {
+ to => $email,
+ cc => $email_cc,
+ subject => $::form->generate_email_subject,
+ message => $::form->generate_email_body(%body_params),
+ attachment_filename => $::form->generate_attachment_filename,
+ js_send_function => 'kivi.SalesPurchase.send_email()',
+ };
+
+ my %files = _get_files_for_email_dialog();
+ my $html = $::form->parse_html_template("common/_send_email_dialog", {
+ email_form => $email_form,
+ show_bcc => $::auth->assert('email_bcc', 'may fail'),
+ FILES => \%files,
+ is_customer => $::form->{vc} eq 'customer',
+ is_invoice_mail => ($record_email && $::form->{type} eq 'invoice'),
+ });
+
+ print $::form->ajax_response_header, $html;
+}
+
+sub send_sales_purchase_email {
+ my $type = $::form->{type};
+ my $id = $::form->{id};
+ my $script = $type =~ m{sales_order|purchase_order|quotation} ? 'oe.pl'
+ : $type =~ m{delivery_} ? 'do.pl'
+ : 'is.pl';
+
+ my $email_form = delete $::form->{email_form};
+ my %field_names = (to => 'email');
+
+ $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
+
+ $::form->{media} = 'email';
+
+ if (($::form->{attachment_policy} // '') =~ m{^(?:old_file|no_file)$}) {
+ $::form->send_email(\%::myconfig, 'pdf');
+
+ } else {
+ print_form("return");
+ Common->save_email_status(\%::myconfig, $::form);
+ }
+
+ flash_later('info', $::locale->text('The email has been sent.'));
+
+ print $::form->redirect_header($script . '?action=edit&id=' . $::form->escape($id) . '&type=' . $::form->escape($type));
+}
+
+sub _maybe_attach_zugferd_data {
+ my ($form) = @_;
+
+ my $record = _make_record();
+
+ return if !$record
+ || !$record->can('customer')
+ || !$record->customer
+ || !$record->can('create_pdf_a_print_options')
+ || !$record->can('create_zugferd_data')
+ || !$record->customer->create_zugferd_invoices_for_this_customer;
+
+ eval {
+ my $xmlfile = File::Temp->new;
+ $xmlfile->print($record->create_zugferd_data);
+ $xmlfile->close;
+
+ $form->{TEMPLATE_DRIVER_OPTIONS}->{pdf_a} = $record->create_pdf_a_print_options(zugferd_xmp_data => $record->create_zugferd_xmp_data);
+ $form->{TEMPLATE_DRIVER_OPTIONS}->{pdf_attachments} = [
+ { source => $xmlfile,
+ name => 'ZUGFeRD-invoice.xml',
+ description => $::locale->text('ZUGFeRD invoice'),
+ relationship => '/Alternative',
+ mime_type => 'text/xml',
+ }
+ ];
+ };
+
+ if (my $e = SL::X::ZUGFeRDValidation->caught) {
+ $::form->error($e->message);
+ }
+}