Projekt

Allgemein

Profil

Herunterladen (28,2 KB) Statistiken
| Zweig: | Markierung: | Revision:
package SL::Controller::Invoice;

use strict;
use parent qw(SL::Controller::Base);

use SL::DB::Invoice;
use SL::Helper::Flash qw(flash flash_later);
use SL::HTML::Util;
use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
use SL::Locale::String qw(t8);
use SL::SessionFile::Random;
use SL::PriceSource;
use SL::File;
use SL::YAML;
use SL::DB::Helper::RecordLink qw(set_record_link_conversions RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
use SL::DB::Helper::TypeDataProxy;
use SL::DB::Helper::Record qw(get_object_name_from_type get_class_from_type);
use SL::Model::Record;
use SL::DB::Invoice::TypeData qw(:types);
use SL::DB::Order::TypeData qw(:types);
use SL::DB::DeliveryOrder::TypeData qw(:types);
use SL::DB::Reclamation::TypeData qw(:types);

use SL::Helper::CreatePDF qw(:all);
use SL::Helper::PrintOptions;
use SL::Helper::ShippedQty;
use SL::Helper::UserPreferences::DisplayPreferences;
use SL::Helper::UserPreferences::PositionsScrollbar;
use SL::Helper::UserPreferences::UpdatePositions;

use SL::Controller::Helper::GetModels;

use List::Util qw(first sum0);
use List::UtilsBy qw(sort_by uniq_by);
use List::MoreUtils qw(uniq any none pairwise first_index);
use File::Spec;
use Sort::Naturally;

use Rose::Object::MakeMethods::Generic
(
scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
'scalar --get_set_init' => [ qw(invoice valid_types type cv p all_price_factors
search_cvpartnumber show_update_button
part_picker_classification_ids
type_data) ],
);


# safety
__PACKAGE__->run_before('check_auth');
__PACKAGE__->run_before('check_auth_for_edit',
except => [ qw(edit price_popup load_second_rows) ]);

#
# actions
#

# add a newinvoice
sub action_add {
my ($self) = @_;

$self->invoice(SL::Model::Record->update_after_new($self->invoice));

$self->invoice->gldate($self->invoice->payment_terms ? $self->invoice->payment_terms->calc_date(reference_date => $self->invoice->transdate) : $self->invoice->transdate);

$self->pre_render();

if (!$::form->{form_validity_token}) {
$::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_SALES_INVOICE_POST())->token;
}

$self->render(
'invoice/form',
title => $self->type_data->text('add'),
%{$self->{template_args}}
);
}



#
# helpers
#

sub init_valid_types {
$_[0]->type_data->valid_types;
}

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

my $type = $self->invoice->record_type;
if (none { $type eq $_ } @{$self->valid_types}) {
die "Not a valid type for invoice";
}

$self->type($type);
}

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

return $self->type_data->properties('customervendor');
}

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

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

return $search_cvpartnumber;
}

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

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

sub init_p {
SL::Presenter->get;
}

sub init_invoice {
$_[0]->make_invoice;
}

sub init_all_price_factors {
SL::DB::Manager::PriceFactor->get_all;
}

sub init_part_picker_classification_ids {
my ($self) = @_;

return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(
where => $self->type_data->part_classification_query()) } ];
}

sub init_type_data {
my ($self) = @_;
SL::DB::Helper::TypeDataProxy->new('SL::DB::Invoice', $self->invoice->record_type);
}

sub check_auth {
my ($self) = @_;
$::auth->assert($self->type_data->rights('view'));
}

sub check_auth_for_edit {
my ($self) = @_;
$::auth->assert($self->type_data->rights('edit'));
}


#
# internal
#


sub pre_render {
my ($self) = @_;

$self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
$self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
$self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
$self->{all_languages} = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->invoice->language_id ] ] );
$self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->invoice->employee_id,
deleted => 0 ] ],
sort_by => 'name');
$self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->invoice->salesman_id,
deleted => 0 ] ],
sort_by => 'name');
$self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->invoice->payment_id,
obsolete => 0 ] ]);
$self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_valid($self->invoice->delivery_term_id);
$self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
$self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();

my $print_form = Form->new('');
$print_form->{type} = $self->type;
$print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
$self->{print_options} = SL::Helper::PrintOptions->get_print_options(
form => $print_form,
options => {dialog_name_prefix => 'print_options.',
show_headers => 1,
no_queue => 1,
no_postscript => 1,
no_opendocument => 0,
no_html => 0},
);

foreach my $item (@{$self->invoice->items}) {
my $price_source = SL::PriceSource->new(record_item => $item, record => $self->invoice);
$item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
$item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
}


# ?! TODO: does invoices need stock info?
# if (any { $self->type eq $_ } (INVOICE_TYPE(), INVOICE_FOR_ADVANCE_PAYMENT_TYPE(), INVOICE_FOR_ADVANCE_PAYMENT_STORNO_TYPE(), FINAL_INVOICE_TYPE(), INVOICE_STORNO_TYPE(), CREDIT_NOTE_TYPE(), CREDIT_NOTE_STORNO_TYPE())) {
# # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
# # Do not use write_to_objects to prevent order->delivered to be set, because this should be
# # the value from db, which can be set manually or is set when linked delivery orders are saved.
# SL::Helper::ShippedQty->new->calculate($self->record)->write_to(\@{$self->order->items});
# }

if ($self->invoice->number && $::instance_conf->get_webdav) {
my $webdav = SL::Webdav->new(
type => $self->type,
number => $self->invoice->number,
);
my @all_objects = $webdav->get_all_objects;
@{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
type => t8('File'),
link => File::Spec->catfile($_->full_filedescriptor),
} } @all_objects;
}

# if ( (any { $self->type eq $_ } (SALES_QUOTATION_TYPE(), SALES_ORDER_INTAKE_TYPE(), SALES_ORDER_TYPE()))
# && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
# $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
# }
$self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();

$self->get_item_cvpartnumber($_) for @{$self->invoice->items_sorted};

# $self->{template_args}->{num_phone_notes} = scalar @{ $self->order->phone_notes || [] };

$::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Invoice kivi.File
calculate_qty follow_up show_history);
$self->setup_edit_action_bar;
}


sub setup_edit_action_bar {
my ($self, %params) = @_;

my $change_never = $::instance_conf->get_is_changeable == 0;
my $change_on_same_day_only = $::instance_conf->get_is_changeable == 2 && $self->invoice->gldate->clone->truncate(to => 'day') != DateTime->today;
my $payments_balanced = 0; #($::form->{oldtotalpaid} == 0); # TODO: don't rely on form
my $has_storno = 0; # $self->invoice->linked_record('storno'); # TODO: linked record?
my $may_edit_create = $::auth->assert($self->type_data->rights('edit'), 'may fail');
my $factur_x_enabled = $self->invoice->customer && $self->invoice->customer->create_zugferd_invoices_for_this_customer;
my ($is_linked_bank_transaction, $warn_unlinked_delivery_order);
if ($::form->{id}
&& SL::DB::Default->get->payments_changeable != 0
&& SL::DB::Manager::BankTransactionAccTrans->find_by(ar_id => $::form->{id})) {

$is_linked_bank_transaction = 1;
}
if ($::instance_conf->get_warn_no_delivery_order_for_invoice && !$self->invoice->id) {
$warn_unlinked_delivery_order = 1 unless $::form->{convert_from_do_ids};
}

my $has_further_invoice_for_advance_payment;
if ($self->invoice->id && $self->invoice->is_type(INVOICE_FOR_ADVANCE_PAYMENT_TYPE())) {
my $lr = $self->invoice->linked_records(direction => 'to', to => ['Invoice']);
$has_further_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
}

my $has_final_invoice;
if ($self->invoice->id && $self->invoice->is_type(INVOICE_FOR_ADVANCE_PAYMENT_TYPE())) {
my $lr = $self->invoice->linked_records(direction => 'to', to => ['Invoice']);
$has_final_invoice = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->invoice_type} @$lr;
}

my $is_invoice_for_advance_payment_from_order;
if ($self->invoice->id && $self->invoice->is_type(INVOICE_FOR_ADVANCE_PAYMENT_TYPE())) {
my $lr = $self->invoice->linked_records(direction => 'from', from => ['Order']);
$is_invoice_for_advance_payment_from_order = scalar @$lr >= 1;
}

my $locked = 0; # TODO: get from... somewhere

# add readonly state in tmpl_vars
# $tmpl_var->{readonly} = !$may_edit_create ? 1
# : $form->{locked} ? 1
# : $form->{storno} ? 1
# : ($form->{id} && $change_never) ? 1
# : ($form->{id} && $change_on_same_day_only) ? 1
# : $is_linked_bank_transaction ? 1
# : 0;



for my $bar ($::request->layout->get('actionbar')) {
$bar->add(
action => [
t8('Update'),
submit => [ '#form', { action => "update" } ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $locked ? t8('The billing period has already been locked.')
: undef,
id => 'update_button',
accesskey => 'enter',
],

combobox => [
action => [
t8('Post'),
submit => [ '#form', { action => "post" } ],
checks => [ 'kivi.validate_form' ],
confirm => t8('The invoice is not linked with a sales delivery order. Post anyway?') x !!$warn_unlinked_delivery_order,
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $locked ? t8('The billing period has already been locked.')
: $self->invoice->storno ? t8('A canceled invoice cannot be posted.')
: $self->invoice->id && $change_never ? t8('Changing invoices has been disabled in the configuration.')
: $self->invoice->id && $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
: $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
: undef,
],
action => [
t8('Post and Close'),
submit => [ '#form', { action => "post_and_close" } ],
checks => [ 'kivi.validate_form' ],
confirm => t8('The invoice is not linked with a sales delivery order. Post anyway?') x !!$warn_unlinked_delivery_order,
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $locked ? t8('The billing period has already been locked.')
: $self->invoice->storno ? t8('A canceled invoice cannot be posted.')
: $self->invoice->id && $change_never ? t8('Changing invoices has been disabled in the configuration.')
: $self->invoice->id && $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
: $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
: undef,
],
action => [
t8('Post Payment'),
submit => [ '#form', { action => "post_payment" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
: undef,
only_if => $self->invoice->record_type ne "invoice_for_advance_payment",
],
action => [ t8('Mark as paid'),
submit => [ '#form', { action => "mark_as_paid" } ],
confirm => t8('This will remove the invoice from showing as unpaid even if the unpaid amount does not match the amount. Proceed?'),
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: undef,
only_if => ($::instance_conf->get_is_show_mark_as_paid && $self->invoice->record_type ne "invoice_for_advance_payment")
|| $self->invoice->record_type eq 'final_invoice',
],
], # end of combobox "Post"

combobox => [
action => [ t8('Storno'),
submit => [ '#form', { action => "storno" } ],
confirm => t8('Do you really want to cancel this invoice?'),
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $locked ? t8('The billing period has already been locked.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $self->invoice->storno ? t8('Cannot storno storno invoice!')
: !$payments_balanced ? t8('Cancelling is disallowed. Either undo or balance the current payments until the open amount matches the invoice amount')
: undef,
],
action => [ t8('Delete'),
submit => [ '#form', { action => "delete" } ],
confirm => t8('Do you really want to delete this object?'),
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $locked ? t8('The billing period has already been locked.')
: $change_never ? t8('Changing invoices has been disabled in the configuration.')
: $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
: $has_storno ? t8('Can only delete the "Storno zu" part of the cancellation pair.')
: undef,
],
], # end of combobox "Storno"

'separator',

combobox => [
action => [ t8('Workflow') ],
action => [
t8('Use As New'),
submit => [ '#form', { action => "use_as_new" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: undef,
],
action => [
t8('Further Invoice for Advance Payment'),
submit => [ '#form', { action => "further_invoice_for_advance_payment" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $has_further_invoice_for_advance_payment ? t8('This invoice has already a further invoice for advanced payment.')
: $has_final_invoice ? t8('This invoice has already a final invoice.')
: $is_invoice_for_advance_payment_from_order ? t8('This invoice was added from an order. See there.')
: undef,
only_if => $self->invoice->record_type eq "invoice_for_advance_payment",
],
action => [
t8('Final Invoice'),
submit => [ '#form', { action => "final_invoice" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $has_further_invoice_for_advance_payment ? t8('This invoice has a further invoice for advanced payment.')
: $has_final_invoice ? t8('This invoice has already a final invoice.')
: $is_invoice_for_advance_payment_from_order ? t8('This invoice was added from an order. See there.')
: undef,
only_if => $self->invoice->is_type("invoice_for_advance_payment"),
],
action => [
t8('Credit Note'),
submit => [ '#form', { action => "credit_note" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $self->invoice->is_type("credit_note") ? t8('Credit notes cannot be converted into other credit notes.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $self>invocie->storno ? t8('A canceled invoice cannot be used. Please undo the cancellation first.')
: undef,
],
action => [
t8('Sales Order'),
submit => [ '#form', { action => "order" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
],
action => [
t8('Reclamation'),
submit => ['#form', { action => "sales_reclamation" }], # can't call Reclamation directly
disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
only_if => ($self->invoice->is_type('invoice') && !$::form->{storno}),
],
], # end of combobox "Workflow"

combobox => [
action => [ t8('Export') ],
action => [
($self->invoice->id ? t8('Print') : t8('Preview')),
call => [ 'kivi.SalesPurchase.show_print_dialog', $self->invoice->id ? 'print' : 'preview' ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not print this invoice.')
: !$self->invoice->id && $locked ? t8('The billing period has already been locked.')
: undef,
],
action => [ t8('Print and Post'),
call => [ 'kivi.SalesPurchase.show_print_dialog', 'print_and_post' ],
checks => [ 'kivi.validate_form' ],
confirm => t8('The invoice is not linked with a sales delivery order. Post anyway?') x !!$warn_unlinked_delivery_order,
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $locked ? t8('The billing period has already been locked.')
: $self->invoice->storno ? t8('A canceled invoice cannot be posted.')
: ($self->invoice->id && $change_never) ? t8('Changing invoices has been disabled in the configuration.')
: ($self->invoice->id && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.')
: $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
: undef,
],
action => [ t8('E Mail'),
call => [ 'kivi.SalesPurchase.show_email_dialog' ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not print this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $self->invoice->customer->postal_invoice ? t8('This customer wants a postal invoices.')
: undef,
],
action => [ t8('Factur-X/ZUGFeRD'),
submit => [ '#form', { action => "download_factur_x_xml" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not print this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: !$factur_x_enabled ? t8('Creating Factur-X/ZUGFeRD invoices is not enabled for this customer.')
: undef,
],
], # end of combobox "Export"

combobox => [
action => [ t8('more') ],
action => [
t8('History'),
call => [ 'set_history_window', $self->invoice->id * 1, 'glid' ],
disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
],
action => [
t8('Follow-Up'),
call => [ 'follow_up_window' ],
disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
],
action => [
t8('Drafts'),
call => [ 'kivi.Draft.popup', 'is', 'invoice', $::form->{draft_id}, $::form->{draft_description} ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $self->invoice->id ? t8('This invoice has already been posted.')
: $locked ? t8('The billing period has already been locked.')
: undef,
],
], # end of combobox "more"
);
}
}

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

# add_items adds items to an order with no items for saving, but they cannot
# be retrieved via items until the order is saved. Adding empty items to new
# order here solves this problem.
my $invoice;
$invoice = SL::DB::Invoice->new(id => $::form->{id})->load(with => [ 'invoiceitems', 'invoiceitems.part' ]) if $::form->{id};
$invoice ||= SL::DB::Invoice->new(invoiceitems => [],
record_type => $::form->{type},
currency_id => $::instance_conf->get_currency_id(),);

my $cv_id_method = $invoice->type_data->properties('customervendor'). '_id';
if (!$::form->{id} && $::form->{$cv_id_method}) {
$invoice->$cv_id_method($::form->{$cv_id_method});
$invoice = SL::Model::Record->update_after_customer_vendor_change($invoice);
}

my $form_invoiceitems = delete $::form->{invoice}->{invoiceitems};

$invoice->assign_attributes(%{$::form->{invoice}});

$self->setup_custom_shipto_from_form($invoice, $::form);

# remove deleted items
$self->item_ids_to_delete([]);
foreach my $idx (reverse 0..$#{$invoice->invoiceitems}) {
my $item = $invoice->invoiceitems->[$idx];
if (none { $item->id == $_->{id} } @{$form_invoiceitems}) {
splice @{$invoice->invoiceitems}, $idx, 1;
push @{$self->item_ids_to_delete}, $item->id;
}
}

my @items;
my $pos = 1;
foreach my $form_attr (@{$form_invoiceitems}) {
my $item = make_item($invoice, $form_attr);
$item->position($pos);
push @items, $item;
$pos++;
}
$invoice->add_items(grep {!$_->id} @items);

return $invoice;
}

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

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

my $is_new = !$item;

# add_custom_variables adds cvars to an orderitem with no cvars for saving, but
# they cannot be retrieved via custom_variables until the order/orderitem is
# saved. Adding empty custom_variables to new orderitem here solves this problem.
$item ||= SL::DB::InvoiceItem->new(custom_variables => []);

$item->assign_attributes(%$attr);

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

return $item;
}

# setup custom shipto from form
#
# The dialog returns form variables starting with 'shipto' and cvars starting
# with 'shiptocvar_'.
# Mark it to be deleted if a shipto from master data is selected
# (i.e. order has a shipto).
# Else, update or create a new custom shipto. If the fields are empty, it
# will not be saved on save.
sub setup_custom_shipto_from_form {
my ($self, $record, $form) = @_;

if ($record->shipto) {
$self->is_custom_shipto_to_delete(1);
} else {
my $custom_shipto = $record->custom_shipto || $record->custom_shipto(SL::DB::Shipto->new(module => 'AR', custom_variables => []));

my $shipto_cvars = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
my $shipto_attrs = {map { $_ => delete $form->{$_}} grep { m{^shipto} } keys %$form};

$custom_shipto->assign_attributes(%$shipto_attrs);
$custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
}
}

1;
(36-36/85)