Merge branch 'von-opendynamic'
authorMoritz Bunkus <m.bunkus@linet-services.de>
Tue, 4 Mar 2014 12:18:38 +0000 (13:18 +0100)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Tue, 4 Mar 2014 12:22:24 +0000 (13:22 +0100)
Conflicts:
SL/DB/MetaSetup/InvoiceItem.pm

29 files changed:
SL/DB/DeliveryOrder.pm
SL/DB/DeliveryOrderItem.pm
SL/DB/Helper/FlattenToForm.pm
SL/DB/Invoice.pm
SL/DB/InvoiceItem.pm
SL/DB/Manager/DeliveryOrder.pm
SL/DB/Manager/Invoice.pm
SL/DB/Manager/Order.pm
SL/DB/Manager/PurchaseInvoice.pm
SL/DB/MetaSetup/DeliveryOrderItem.pm
SL/DB/MetaSetup/InvoiceItem.pm
SL/DB/MetaSetup/OrderItem.pm
SL/DB/Order.pm
SL/DB/OrderItem.pm
SL/DB/PurchaseInvoice.pm
SL/DB/Unit.pm
SL/DO.pm
SL/Form.pm
SL/Helper/CreatePDF.pm [new file with mode: 0644]
SL/IS.pm
SL/Presenter.pm
SL/Template/Plugin/LxERP.pm
bin/mozilla/do.pl
bin/mozilla/io.pl
doc/dokumentation.xml
js/kivi.js
sql/Pg-upgrade2/invoice_add_donumber.sql [new file with mode: 0644]
sql/Pg-upgrade2/unit_foreign_key_for_line_items.sql [new file with mode: 0644]
templates/webpages/amcvar/render_inputs_block.html

index 372a5cc..75eab64 100644 (file)
@@ -4,8 +4,11 @@ use strict;
 
 use Carp;
 
+use Rose::DB::Object::Helpers ();
+
 use SL::DB::MetaSetup::DeliveryOrder;
 use SL::DB::Manager::DeliveryOrder;
+use SL::DB::Helper::FlattenToForm;
 use SL::DB::Helper::LinkedRecords;
 use SL::DB::Helper::TransNumberGenerator;
 
@@ -16,6 +19,12 @@ __PACKAGE__->meta->add_relationship(orderitems => { type         => 'one to many
                                                     column_map   => { id => 'delivery_order_id' },
                                                     manager_args => { with_objects => [ 'part' ] }
                                                   },
+                                    custom_shipto => {
+                                      type        => 'one to one',
+                                      class       => 'SL::DB::Shipto',
+                                      column_map  => { id => 'trans_id' },
+                                      query_args  => [ module => 'DO' ],
+                                    },
                                    );
 
 __PACKAGE__->meta->initialize;
@@ -74,4 +83,189 @@ sub date {
   goto &transdate;
 }
 
+sub _clone_orderitem_cvar {
+  my ($cvar) = @_;
+
+  my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
+  $cloned->sub_module('delivery_order_items');
+
+  return $cloned;
+}
+
+sub new_from {
+  my ($class, $source, %params) = @_;
+
+  croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) eq 'SL::DB::Order';
+
+  my ($item_parent_id_column, $item_parent_column);
+
+  if (ref($source) eq 'SL::DB::Order') {
+    $item_parent_id_column = 'trans_id';
+    $item_parent_column    = 'order';
+  }
+
+  my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms->terms_netto : 0;
+
+  my %args = ( map({ ( $_ => $source->$_ ) } qw(cp_id currency_id customer_id cusordnumber department_id employee_id globalproject_id intnotes language_id notes
+                                                ordnumber reqdate salesman_id shippingpoint shipvia taxincluded taxzone_id transaction_description vendor_id
+                                             )),
+               closed    => 0,
+               is_sales  => !!$source->customer_id,
+               delivered => 0,
+               terms     => $terms,
+               transdate => DateTime->today_local,
+            );
+
+  # Custom shipto addresses (the ones specific to the sales/purchase
+  # record and not to the customer/vendor) are only linked from
+  # shipto -> delivery_orders. Meaning delivery_orders.shipto_id
+  # will not be filled in that case. Therefore we have to return the
+  # new shipto object as a separate object so that the caller can
+  # save it, too.
+  my $custom_shipto;
+  if (!$source->shipto_id && $source->id) {
+    my $old = $source->custom_shipto;
+    if ($old) {
+      $custom_shipto = SL::DB::Shipto->new(
+        map  { +($_ => $old->$_) }
+        grep { !m{^ (?: itime | mtime | shipto_id | trans_id ) $}x }
+        map  { $_->name }
+        @{ $old->meta->columns }
+      );
+      $custom_shipto->module('DO');
+    }
+
+  } else {
+    $args{shipto_id} = $source->shipto_id;
+  }
+
+  my $delivery_order = $class->new(%args, %{ $params{attributes} || {} });
+  my $items          = delete($params{items}) || $source->items_sorted;
+  my %item_parents;
+
+  my @items = map {
+    my $source_item      = $_;
+    my $source_item_id   = $_->$item_parent_id_column;
+    my @custom_variables = map { _clone_orderitem_cvar($_) } @{ $source_item->custom_variables };
+
+    $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
+    my $item_parent                  = $item_parents{$source_item_id};
+
+    SL::DB::DeliveryOrderItem->new(map({ ( $_ => $source_item->$_ ) }
+                                         qw(base_qty cusordnumber description discount lastcost longdescription marge_price_factor parts_id price_factor price_factor_id
+                                            project_id qty reqdate sellprice serialnumber transdate unit
+                                         )),
+                                   custom_variables => \@custom_variables,
+                                   ordnumber        => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
+                                 );
+
+  } @{ $items };
+
+  @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
+
+  $delivery_order->items(\@items);
+
+  return ($delivery_order, $custom_shipto);
+}
+
 1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::DeliveryOrder - Rose model for delivery orders (table
+"delivery_orders")
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<date>
+
+An alias for C<transdate> for compatibility with other sales/purchase models.
+
+=item C<displayable_state>
+
+Returns a human-readable description of the state regarding being
+closed and delivered.
+
+=item C<items>
+
+An alias for C<deliver_orer_items> for compatibility with other
+sales/purchase models.
+
+=item C<items_sorted>
+
+Returns the delivery order items sorted by their ID (same order they
+appear in the frontend delivery order masks).
+
+=item C<new_from $source, %params>
+
+Creates a new C<SL::DB::DeliveryOrder> instance and copies as much
+information from C<$source> as possible. At the moment only instances
+of C<SL::DB::Order> (sales quotations, sales orders, requests for
+quotations and purchase orders) are supported as sources.
+
+The conversion copies order items into delivery order items. Dates are copied
+as appropriate, e.g. the C<transdate> field will be set to the current date.
+
+Returns one or two objects depending on the context. In list context
+the new delivery order instance and a shipto instance will be
+returned. In scalar instance only the delivery order instance is
+returned.
+
+Custom shipto addresses (the ones specific to the sales/purchase
+record and not to the customer/vendor) are only linked from C<shipto>
+to C<delivery_orders>. Meaning C<delivery_orders.shipto_id> will not
+be filled in that case. That's why a separate shipto object is created
+and returned.
+
+The objects returned are not saved.
+
+C<%params> can include the following options:
+
+=over 2
+
+=item C<items>
+
+An optional array reference of RDBO instances for the items to use. If
+missing then the method C<items_sorted> will be called on
+C<$source>. This option can be used to override the sorting, to
+exclude certain positions or to add additional ones.
+
+=item C<skip_items_zero_qty>
+
+If trueish then items with a quantity of 0 are skipped.
+
+=item C<attributes>
+
+An optional hash reference. If it exists then it is passed to C<new>
+allowing the caller to set certain attributes for the new delivery
+order.
+
+=back
+
+=item C<sales_order>
+
+TODO: Describe sales_order
+
+=item C<type>
+
+Returns a stringdescribing this record's type: either
+C<sales_delivery_order> or C<purchase_delivery_order>.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 29c85a2..638082c 100644 (file)
@@ -15,13 +15,6 @@ use SL::DB::Helper::CustomVariables (
 );
 
 __PACKAGE__->meta->make_manager_class;
-__PACKAGE__->meta->add_relationship(
-  unit_obj       => {
-    type         => 'many to one',
-    class        => 'SL::DB::Unit',
-    column_map   => { unit => 'name' },
-  },
-);
 
 __PACKAGE__->meta->initialize;
 
index 105cf02..570724b 100644 (file)
@@ -62,7 +62,7 @@ sub flatten_to_form {
     $form->{"partsgroup_${idx}"} = $item->part->partsgroup->partsgroup if _has($item->part, 'partsgroup_id');
     _copy($item->part,    $form, '',        "_${idx}", 0,               qw(id partnumber weight));
     _copy($item->part,    $form, '',        "_${idx}", $format_amounts, qw(listprice));
-    _copy($item,          $form, '',        "_${idx}", 0,               qw(description project_id ship serialnumber pricegroup_id ordnumber cusordnumber unit
+    _copy($item,          $form, '',        "_${idx}", 0,               qw(description project_id ship serialnumber pricegroup_id ordnumber donumber cusordnumber unit
                                                                            subtotal longdescription price_factor_id marge_price_factor approved_sellprice reqdate transdate));
     _copy($item,          $form, '',        "_${idx}", $format_amounts, qw(qty sellprice marge_total marge_percent lastcost));
     _copy($item,          $form, '',        "_${idx}", $format_notnull, qw(discount));
index 72844b4..9cf52a2 100644 (file)
@@ -7,7 +7,8 @@ use strict;
 
 use Carp;
 use List::Util qw(first);
-use List::MoreUtils qw(pairwise);
+
+use Rose::DB::Object::Helpers ();
 
 use SL::DB::MetaSetup::Invoice;
 use SL::DB::Manager::Invoice;
@@ -16,7 +17,6 @@ use SL::DB::Helper::LinkedRecords;
 use SL::DB::Helper::PriceTaxCalculator;
 use SL::DB::Helper::PriceUpdater;
 use SL::DB::Helper::TransNumberGenerator;
-use SL::DB::CustomVariable;
 
 __PACKAGE__->meta->add_relationship(
   invoiceitems => {
@@ -38,6 +38,12 @@ __PACKAGE__->meta->add_relationship(
     column_map      => { id => 'ar_id' },
     manager_args    => { with_objects => [ 'sepa_export' ] }
   },
+  custom_shipto     => {
+    type            => 'one to one',
+    class           => 'SL::DB::Shipto',
+    column_map      => { id => 'trans_id' },
+    query_args      => [ module => 'AR' ],
+  },
 );
 
 __PACKAGE__->meta->initialize;
@@ -107,6 +113,15 @@ sub closed {
   return $self->paid >= $self->amount;
 }
 
+sub _clone_orderitem_delivery_order_item_cvar {
+  my ($cvar) = @_;
+
+  my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
+  $cloned->sub_module('invoice');
+
+  return $cloned;
+}
+
 sub new_from {
   my ($class, $source, %params) = @_;
 
@@ -116,10 +131,24 @@ sub new_from {
   require SL::DB::Employee;
 
   my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms->terms_netto : 0;
+  my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
+
+  if (ref($source) eq 'SL::DB::Order') {
+    @columns      = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
+    @item_columns = qw(subtotal);
 
-  my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber quonumber
-                                                department_id cp_id language_id payment_id delivery_customer_id delivery_vendor_id taxzone_id shipto_id
-                                                globalproject_id transaction_description currency_id delivery_term_id)),
+    $item_parent_id_column = 'trans_id';
+    $item_parent_column    = 'order';
+
+  } else {
+    @columns      = qw(donumber);
+
+    $item_parent_id_column = 'delivery_order_id';
+    $item_parent_column    = 'delivery_order';
+  }
+
+  my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
+                                                cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
                transdate   => DateTime->today_local,
                gldate      => DateTime->today_local,
                duedate     => DateTime->today_local->add(days => $terms * 1),
@@ -137,25 +166,31 @@ sub new_from {
     $args{quodate}      = $source->transdate;
   }
 
-  my $invoice = $class->new(%args, %params);
+  my $invoice = $class->new(%args, %{ $params{attributes} || {} });
+  my $items   = delete($params{items}) || $source->items_sorted;
+  my %item_parents;
 
   my @items = map {
-    my $source_item = $_;
+    my $source_item      = $_;
+    my $source_item_id   = $_->$item_parent_id_column;
+    my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
+
+    $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
+    my $item_parent                  = $item_parents{$source_item_id};
+
     SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
-                                 qw(parts_id description qty sellprice discount project_id
-                                    serialnumber pricegroup_id ordnumber transdate cusordnumber unit
-                                    base_qty subtotal longdescription lastcost price_factor_id)),
-                            deliverydate => $source_item->reqdate,
-                            fxsellprice  => $source_item->sellprice,);
-  } @{ $source->items_sorted };
-
-  my $i = 0;
-  foreach my $item (@items) {
-    my $source_cvars = $source->items_sorted->[$i]->cvars_by_config;
-    my $target_cvars = $item->cvars_by_config;
-    pairwise { $a->value($b->value) } @{ $target_cvars }, @{ $source_cvars };
-    $i++;
-  }
+                                 qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
+                                    base_qty longdescription lastcost price_factor_id), @item_columns),
+                             deliverydate     => $source_item->reqdate,
+                             fxsellprice      => $source_item->sellprice,
+                             custom_variables => \@custom_variables,
+                             ordnumber        => ref($item_parent) eq 'SL::DB::Order'         ? $item_parent->ordnumber : $source_item->ordnumber,
+                             donumber         => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber  : $source_item->can('donumber') ? $source_item->donumber : '',
+                           );
+
+  } @{ $items };
+
+  @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
 
   $invoice->invoiceitems(\@items);
 
@@ -294,7 +329,7 @@ SL::DB::Invoice: Rose model for invoices (table "ar")
 
 =over 4
 
-=item C<new_from $source>
+=item C<new_from $source, %params>
 
 Creates a new C<SL::DB::Invoice> instance and copies as much
 information from C<$source> as possible. At the moment only sales
@@ -304,6 +339,29 @@ The conversion copies order items into invoice items. Dates are copied
 as appropriate, e.g. the C<transdate> field from an order will be
 copied into the invoice's C<orddate> field.
 
+C<%params> can include the following options:
+
+=over 2
+
+=item C<items>
+
+An optional array reference of RDBO instances for the items to use. If
+missing then the method C<items_sorted> will be called on
+C<$source>. This option can be used to override the sorting, to
+exclude certain positions or to add additional ones.
+
+=item C<skip_items_zero_qty>
+
+If trueish then items with a quantity of 0 are skipped.
+
+=item C<attributes>
+
+An optional hash reference. If it exists then it is passed to C<new>
+allowing the caller to set certain attributes for the new delivery
+order.
+
+=back
+
 Amounts, prices and taxes are not
 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
 can be used for this.
index 8f9a4dd..4374a6c 100644 (file)
@@ -14,16 +14,21 @@ use SL::DB::Helper::CustomVariables (
   },
 );
 
-__PACKAGE__->meta->add_relationship(
-  unit_obj       => {
-    type         => 'many to one',
-    class        => 'SL::DB::Unit',
-    column_map   => { unit => 'name' },
+__PACKAGE__->meta->make_manager_class;
+
+__PACKAGE__->meta->add_relationships(
+  invoice          => {
+    type           => 'one to one',
+    class          => 'SL::DB::Invoice',
+    column_map     => { trans_id => 'id' },
   },
-);
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-__PACKAGE__->meta->make_manager_class;
+  purchase_invoice => {
+    type           => 'one to one',
+    class          => 'SL::DB::PurchaseInvoice',
+    column_map     => { trans_id => 'id' },
+  },
+);
 
 __PACKAGE__->meta->initialize;
 
index fa67f66..793e88b 100644 (file)
@@ -32,8 +32,8 @@ sub _sort_spec {
     },
     columns                   => {
       SIMPLE                  => 'ALL',
-      customer                => 'customer.name',
-      vendor                  => 'vendor.name',
+      customer                => 'lower(customer.name)',
+      vendor                  => 'lower(vendor.name)',
       globalprojectnumber     => 'lower(globalproject.projectnumber)',
 
       # Bug in Rose::DB::Object: the next should be
index 493789e..03c2296 100644 (file)
@@ -34,7 +34,7 @@ sub _sort_spec {
     },
     columns                   => {
       SIMPLE                  => 'ALL',
-      customer                => 'customer.name',
+      customer                => 'lower(customer.name)',
       globalprojectnumber     => 'lower(globalproject.projectnumber)',
 
       # Bug in Rose::DB::Object: the next should be
index e03386d..d844683 100644 (file)
@@ -35,8 +35,8 @@ sub _sort_spec {
     },
     columns                   => {
       SIMPLE                  => 'ALL',
-      customer                => 'customer.name',
-      vendor                  => 'vendor.name',
+      customer                => 'lower(customer.name)',
+      vendor                  => 'lower(vendor.name)',
       globalprojectnumber     => 'lower(globalproject.projectnumber)',
 
       # Bug in Rose::DB::Object: the next should be
index 2dd3fad..ca0bb10 100644 (file)
@@ -32,7 +32,7 @@ sub _sort_spec {
     },
     columns                   => {
       SIMPLE                  => 'ALL',
-      vendor                  => 'vendor.name',
+      vendor                  => 'lower(vendor.name)',
       globalprojectnumber     => 'lower(globalproject.projectnumber)',
 
       # Bug in Rose::DB::Object: the next should be
index 00bce88..76c890a 100644 (file)
@@ -63,6 +63,11 @@ __PACKAGE__->meta->foreign_keys(
     class       => 'SL::DB::Project',
     key_columns => { project_id => 'id' },
   },
+
+  unit_obj => {
+    class       => 'SL::DB::Unit',
+    key_columns => { unit => 'name' },
+  },
 );
 
 1;
index e788b5f..7cd853e 100644 (file)
@@ -16,6 +16,7 @@ __PACKAGE__->meta->columns(
   deliverydate       => { type => 'date' },
   description        => { type => 'text' },
   discount           => { type => 'float', scale => 4 },
+  donumber           => { type => 'text' },
   fxsellprice        => { type => 'numeric', precision => 15, scale => 5 },
   id                 => { type => 'integer', not_null => 1, sequence => 'invoiceid' },
   itime              => { type => 'timestamp', default => 'now()' },
@@ -64,6 +65,11 @@ __PACKAGE__->meta->foreign_keys(
     class       => 'SL::DB::Project',
     key_columns => { project_id => 'id' },
   },
+
+  unit_obj => {
+    class       => 'SL::DB::Unit',
+    key_columns => { unit => 'name' },
+  },
 );
 
 1;
index 7a30dc8..85bf5be 100644 (file)
@@ -67,6 +67,11 @@ __PACKAGE__->meta->foreign_keys(
     class       => 'SL::DB::Project',
     key_columns => { project_id => 'id' },
   },
+
+  unit_obj => {
+    class       => 'SL::DB::Unit',
+    key_columns => { unit => 'name' },
+  },
 );
 
 1;
index 194431d..a605a36 100644 (file)
@@ -30,6 +30,12 @@ __PACKAGE__->meta->add_relationship(
     class                  => 'SL::DB::PeriodicInvoicesConfig',
     column_map             => { id => 'oe_id' },
   },
+  custom_shipto            => {
+    type                   => 'one to one',
+    class                  => 'SL::DB::Shipto',
+    column_map             => { id => 'trans_id' },
+    query_args             => [ module => 'OE' ],
+  },
 );
 
 __PACKAGE__->meta->initialize;
@@ -130,11 +136,12 @@ sub convert_to_invoice {
   croak("Conversion to invoices is only supported for sales records") unless $self->customer_id;
 
   my $invoice;
-  if (!$self->db->do_transaction(sub {
+  if (!$self->db->with_transaction(sub {
+    require SL::DB::Invoice;
     $invoice = SL::DB::Invoice->new_from($self)->post(%params) || die;
     $self->link_to_record($invoice);
     $self->update_attributes(closed => 1);
-    # die;
+    1;
   })) {
     return undef;
   }
@@ -142,6 +149,25 @@ sub convert_to_invoice {
   return $invoice;
 }
 
+sub convert_to_delivery_order {
+  my ($self, @args) = @_;
+
+  my ($delivery_order, $custom_shipto);
+  if (!$self->db->with_transaction(sub {
+    require SL::DB::DeliveryOrder;
+    ($delivery_order, $custom_shipto) = SL::DB::DeliveryOrder->new_from($self, @args);
+    $delivery_order->save;
+    $custom_shipto->save if $custom_shipto;
+    $self->link_to_record($delivery_order);
+    $self->update_attributes(delivered => 1);
+    1;
+  })) {
+    return wantarray ? () : undef;
+  }
+
+  return wantarray ? ($delivery_order, $custom_shipto) : $delivery_order;
+}
+
 sub number {
   my $self = shift;
 
@@ -189,6 +215,28 @@ Returns one of the following string types:
 
 Returns true if the order is of the given type.
 
+=head2 C<convert_to_delivery_order %params>
+
+Creates a new delivery order with C<$self> as the basis by calling
+L<SL::DB::DeliveryOrder::new_from>. That delivery order is saved, and
+C<$self> is linked to the new invoice via
+L<SL::DB::RecordLink>. C<$self>'s C<delivered> attribute is set to
+C<true>, and C<$self> is saved.
+
+The arguments in C<%params> are passed to
+L<SL::DB::DeliveryOrder::new_from>.
+
+Returns C<undef> on failure. Otherwise the return value depends on the
+context. In list context the new delivery order and a shipto instance
+will be returned. In scalar instance only the delivery order instance
+is returned.
+
+Custom shipto addresses (the ones specific to the sales/purchase
+record and not to the customer/vendor) are only linked from C<shipto>
+to C<delivery_orders>. Meaning C<delivery_orders.shipto_id> will not
+be filled in that case. That's why a separate shipto object is created
+and returned.
+
 =head2 C<convert_to_invoice %params>
 
 Creates a new invoice with C<$self> as the basis by calling
index 3752097..e75c78f 100644 (file)
@@ -17,14 +17,6 @@ use SL::DB::Helper::CustomVariables (
   },
 );
 
-__PACKAGE__->meta->add_relationship(
-  unit_obj       => {
-    type         => 'many to one',
-    class        => 'SL::DB::Unit',
-    column_map   => { unit => 'name' },
-  },
-);
-
 __PACKAGE__->meta->initialize;
 
 sub is_price_update_available {
index 61d9355..2457cde 100644 (file)
@@ -23,6 +23,12 @@ __PACKAGE__->meta->add_relationship(
     column_map      => { id => 'ap_id' },
     manager_args    => { with_objects => [ 'sepa_export' ] }
   },
+  custom_shipto     => {
+    type            => 'one to one',
+    class           => 'SL::DB::Shipto',
+    column_map      => { id => 'trans_id' },
+    query_args      => [ module => 'AP' ],
+  },
 );
 
 __PACKAGE__->meta->initialize;
index 927e31b..b133d8f 100644 (file)
@@ -27,10 +27,12 @@ sub unit_class {
 
 sub convertible_units {
   my $self = shift;
+  my $all_units = scalar(@_) && (ref($_[0]) eq 'ARRAY') ? $_[0] : \@_;
+  $all_units    = SL::DB::Manager::Unit->get_all if !@{ $all_units };
   return [
     sort { $a->sortkey <=> $b->sortkey }
     grep { $_->unit_class->name eq $self->unit_class->name }
-    @{ SL::DB::Manager::Unit->get_all }
+    @{ $all_units }
   ];
 }
 
index 31b01c4..a0087de 100644 (file)
--- a/SL/DO.pm
+++ b/SL/DO.pm
@@ -680,7 +680,7 @@ sub retrieve {
          doi.reqdate, doi.project_id, doi.serialnumber, doi.lastcost,
          doi.ordnumber, doi.transdate, doi.cusordnumber, doi.longdescription,
          doi.price_factor_id, doi.price_factor, doi.marge_price_factor, doi.pricegroup_id,
-         pr.projectnumber, dord.transdate AS dord_transdate,
+         pr.projectnumber, dord.transdate AS dord_transdate, dord.donumber,
          pg.partsgroup
        FROM delivery_order_items doi
        JOIN parts p ON (doi.parts_id = p.id)
index 2181724..875b726 100644 (file)
@@ -673,6 +673,7 @@ sub init_template {
      'COMPILE_EXT'  => '.tcc',
      'COMPILE_DIR'  => $::lx_office_conf{paths}->{userspath} . '/templates-cache',
      'ERROR'        => 'templates/webpages/generic/exception.html',
+     'ENCODING'     => 'utf8',
   })) || die;
 }
 
@@ -3326,6 +3327,13 @@ sub prepare_for_printing {
   # compatibility.
   $self->{$_} = $defaults->$_ for qw(company address taxnumber co_ustid duns sepa_creditor_id);
 
+  $self->{"myconfig_${_}"} = $::myconfig{$_} for grep { $_ ne 'dbpasswd' } keys %::myconfig;
+
+  if (!$self->{employee_id}) {
+    $self->{"employee_${_}"} = $::myconfig{$_} for qw(email tel fax name signature);
+    $self->{"employee_${_}"} = $defaults->$_   for qw(address businessnumber co_ustid company duns sepa_creditor_id taxnumber);
+  }
+
   # set shipto from billto unless set
   my $has_shipto = any { $self->{"shipto$_"} } qw(name street zipcode city country contact);
   if (!$has_shipto && ($self->{type} =~ m/^(?:purchase_order|request_quotation)$/)) {
@@ -3344,6 +3352,10 @@ sub prepare_for_printing {
     $output_longdates    = 1;
   }
 
+  $self->{myconfig_output_dateformat}   = $output_dateformat;
+  $self->{myconfig_output_longdates}    = $output_longdates;
+  $self->{myconfig_output_numberformat} = $output_numberformat;
+
   # Retrieve accounts for tax calculation.
   IC->retrieve_accounts(\%::myconfig, $self, map { $_ => $self->{"id_$_"} } 1 .. $self->{rowcount});
 
diff --git a/SL/Helper/CreatePDF.pm b/SL/Helper/CreatePDF.pm
new file mode 100644 (file)
index 0000000..8d36eb7
--- /dev/null
@@ -0,0 +1,287 @@
+package SL::Helper::CreatePDF;
+
+use strict;
+
+use Carp;
+use Cwd;
+use English qw(-no_match_vars);
+use File::Slurp ();
+use File::Temp ();
+use List::MoreUtils qw(uniq);
+use List::Util qw(first);
+use String::ShellQuote ();
+
+use SL::Form;
+use SL::Common;
+use SL::DB::Language;
+use SL::DB::Printer;
+use SL::MoreCommon;
+use SL::Template;
+use SL::Template::LaTeX;
+
+use Exporter 'import';
+our @EXPORT_OK = qw(create_pdf merge_pdfs find_template);
+our %EXPORT_TAGS = (
+  all => \@EXPORT_OK,
+);
+
+sub create_pdf {
+  my ($class, %params) = @_;
+
+  my $userspath       = $::lx_office_conf{paths}->{userspath};
+  my $form            = Form->new('');
+  $form->{format}     = 'pdf';
+  $form->{cwd}        = getcwd();
+  $form->{templates}  = $::instance_conf->get_templates;
+  $form->{IN}         = $params{template};
+  $form->{tmpdir}     = $form->{cwd} . '/' . $userspath;
+  my ($suffix)        = $params{template} =~ m{\.(.+)};
+
+  my $vars            = $params{variables} || {};
+  $form->{$_}         = $vars->{$_} for keys %{ $vars };
+
+  my $temp_fh;
+  ($temp_fh, $form->{tmpfile}) = File::Temp::tempfile(
+    'kivitendo-printXXXXXX',
+    SUFFIX => ".${suffix}",
+    DIR    => $userspath,
+    UNLINK => ($::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files})? 0 : 1,
+  );
+
+  my $parser = SL::Template::LaTeX->new(
+    $form->{IN},
+    $form,
+    \%::myconfig,
+    $userspath,
+  );
+
+  my $result = $parser->parse($temp_fh);
+
+  close $temp_fh;
+  chdir $form->{cwd};
+
+  if (!$result) {
+    $form->cleanup;
+    die $parser->get_error;
+  }
+
+  if (($params{return} || 'content') eq 'file_name') {
+    my $new_name = $userspath . '/keep-' . $form->{tmpfile};
+    rename $userspath . '/' . $form->{tmpfile}, $new_name;
+
+    $form->cleanup;
+
+    return $new_name;
+  }
+
+  my $pdf = File::Slurp::read_file($userspath . '/' . $form->{tmpfile});
+
+  $form->cleanup;
+
+  return $pdf;
+}
+
+sub merge_pdfs {
+  my ($class, %params) = @_;
+
+  return scalar(File::Slurp::read_file($params{file_names}->[0])) if scalar(@{ $params{file_names} }) < 2;
+
+  my ($temp_fh, $temp_name) = File::Temp::tempfile(
+    'kivitendo-printXXXXXX',
+    SUFFIX => '.pdf',
+    DIR    => $::lx_office_conf{paths}->{userspath},
+    UNLINK => ($::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files})? 0 : 1,
+  );
+  close $temp_fh;
+
+  my $input_names = join ' ', String::ShellQuote::shell_quote(@{ $params{file_names} });
+  my $exe         = $::lx_office_conf{applications}->{ghostscript} || 'gs';
+  my $output      = `$exe -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile=${temp_name} ${input_names} 2>&1`;
+
+  die "Executing gs failed: $ERRNO" if !defined $output;
+  die $output                       if $? != 0;
+
+  return scalar File::Slurp::read_file($temp_name);
+}
+
+sub find_template {
+  my ($class, %params) = @_;
+
+  $params{name} or croak "Missing parameter 'name'";
+
+  my $path                 = $::instance_conf->get_templates;
+  my $extension            = $params{extension} || "tex";
+  my ($printer, $language) = ('', '');
+
+  if ($params{printer} || $params{printer_id}) {
+    if ($params{printer} && !ref $params{printer}) {
+      $printer = '_' . $params{printer};
+    } else {
+      $printer = $params{printer} || SL::DB::Printer->new(id => $params{printer_id})->load;
+      $printer = $printer->template_code ? '_' . $printer->template_code : '';
+    }
+  }
+
+  if ($params{language} || $params{language_id}) {
+    if ($params{language} && !ref $params{language}) {
+      $language = '_' . $params{language};
+    } else {
+      $language = $params{language} || SL::DB::Language->new(id => $params{language_id})->load;
+      $language = $language->template_code ? '_' . $language->template_code : '';
+    }
+  }
+
+  my @template_files = (
+    $params{name} . "${language}${printer}",
+    $params{name} . "${language}",
+    $params{name},
+    "default",
+  );
+
+  if ($params{email}) {
+    unshift @template_files, (
+      $params{name} . "_email${language}${printer}",
+      $params{name} . "_email${language}",
+    );
+  }
+
+  @template_files = map { "${_}.${extension}" } uniq grep { $_ } @template_files;
+
+  my $template = first { -f ($path . "/$_") } @template_files;
+
+  return wantarray ? ($template, @template_files) : $template;
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Helper::CreatePDF - A helper for creating PDFs from template files
+
+=head1 SYNOPSIS
+
+  # Retrieve a sales order from the database and create a PDF for
+  # it:
+  my $order               = SL::DB::Order->new(id => …)->load;
+  my $print_form          = Form->new('');
+  $print_form->{type}     = 'invoice';
+  $print_form->{formname} = 'invoice',
+  $print_form->{format}   = 'pdf',
+  $print_form->{media}    = 'file';
+
+  $order->flatten_to_form($print_form, format_amounts => 1);
+  $print_form->prepare_for_printing;
+
+  my $pdf = SL::Helper::CreatePDF->create_pdf(
+    template  => 'sales_order',
+    variables => $print_form,
+  );
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<create_pdf %params>
+
+Parses a LaTeX template file, creates a PDF for it and returns either
+its content or its file name. The recognized parameters are:
+
+=over 2
+
+=item * C<template> – mandatory. The template file name relative to
+the users' templates directory. Must be an existing file name,
+e.g. one retrieved by L</find_template>.
+
+=item * C<variables> – optional hash reference containing variables
+available to the template.
+
+=item * C<return> – optional scalar containing either C<content> (the
+default) or C<file_name>. If it is set to C<file_name> then the file
+name of the temporary file containing the PDF is returned, and the
+caller is responsible for deleting it. Otherwise a scalar containing
+the PDF itself is returned and all temporary files have already been
+deleted by L</create_pdf>.
+
+=back
+
+=item C<find_template %params>
+
+Searches the user's templates directory for a template file name to
+use. The file names considered depend on the parameters; they can
+contain a template base name and suffixes for email, language and
+printers. As a fallback the name C<default.$extension> is also
+considered.
+
+The return value depends on the context. In scalar context the
+template file name that matches the given parameters is returned. It's
+a file name relative to the user's templates directory. If no template
+file is found then C<undef> is returned.
+
+In list context the first element is the same value as in scalar
+context. Additionally a list of considered template file names is
+returned.
+
+The recognized parameters are:
+
+=over 2
+
+=item * C<name> – mandatory. The template's file name basis
+without any additional suffix or extension, e.g. C<sales_quotation>.
+
+=item * C<extension> – optional file name extension to use without the
+dot. Defaults to C<tex>.
+
+=item * C<email> – optional flag indicating whether or not the
+template is to be sent via email. If set to true then template file
+names containing C<_email> are considered as well.
+
+=item * C<language> and C<language_id> – optional parameters
+indicating the language to be used. C<language> can be either a string
+containing the language code to use or an instance of
+C<SL::DB::Language>. C<language_id> can contain the ID of the
+C<SL::DB:Language> instance to load and use. If given template file
+names containing C<_language_template_code> are considered as well.
+
+=item * C<printer> and C<printer_id> – optional parameters indicating
+the printer to be used. C<printer> can be either a string containing
+the printer code to use or an instance of
+C<SL::DB::Printer>. C<printer_id> can contain the ID of the
+C<SL::DB:Printer> instance to load and use. If given template file
+names containing C<_printer_template_code> are considered as well.
+
+=back
+
+=item C<merge_pdfs %params>
+
+Merges two or more PDFs into a single PDF by using the external
+application ghostscript.
+
+The recognized parameters are:
+
+=over 2
+
+=item * C<file_names> – mandatory array reference containing the file
+names to merge.
+
+=back
+
+Note that this function relies on the presence of the external
+application ghostscript. The executable to use is configured via
+kivitendo's configuration file setting C<application.ghostscript>.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 9470f1a..c5fd882 100644 (file)
--- a/SL/IS.pm
+++ b/SL/IS.pm
@@ -149,7 +149,7 @@ sub invoice_details {
 
   my @arrays =
     qw(runningnumber number description longdescription qty ship unit bin
-       deliverydate_oe ordnumber_oe transdate_oe validuntil
+       deliverydate_oe ordnumber_oe donumber_do transdate_oe validuntil
        partnotes serialnumber reqdate sellprice listprice netprice
        discount p_discount discount_sub nodiscount_sub
        linetotal  nodiscount_linetotal tax_rate projectnumber projectdescription
@@ -210,6 +210,7 @@ sub invoice_details {
       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice} },         $form->{"sellprice_$i"};
       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice_nofmt} },   $form->parse_amount($myconfig, $form->{"sellprice_$i"});
       push @{ $form->{TEMPLATE_ARRAYS}->{ordnumber_oe} },      $form->{"ordnumber_$i"};
+      push @{ $form->{TEMPLATE_ARRAYS}->{donumber_do} },       $form->{"donumber_$i"};
       push @{ $form->{TEMPLATE_ARRAYS}->{transdate_oe} },      $form->{"transdate_$i"};
       push @{ $form->{TEMPLATE_ARRAYS}->{invnumber} },         $form->{"invnumber"};
       push @{ $form->{TEMPLATE_ARRAYS}->{invdate} },           $form->{"invdate"};
@@ -741,10 +742,10 @@ sub post_invoice {
         qq|INSERT INTO invoice (id, trans_id, parts_id, description, longdescription, qty,
                                 sellprice, fxsellprice, discount, allocated, assemblyitem,
                                 unit, deliverydate, project_id, serialnumber, pricegroup_id,
-                                ordnumber, transdate, cusordnumber, base_qty, subtotal,
+                                ordnumber, donumber, transdate, cusordnumber, base_qty, subtotal,
                                 marge_percent, marge_total, lastcost,
                                 price_factor_id, price_factor, marge_price_factor)
-           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
+           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
                    (SELECT factor FROM price_factors WHERE id = ?), ?)|;
 
       @values = ($invoice_id, conv_i($form->{id}), conv_i($form->{"id_$i"}),
@@ -753,7 +754,7 @@ sub post_invoice {
                  $form->{"discount_$i"}, $allocated, 'f',
                  $form->{"unit_$i"}, conv_date($form->{"reqdate_$i"}), conv_i($form->{"project_id_$i"}),
                  $form->{"serialnumber_$i"}, $pricegroup_id,
-                 $form->{"ordnumber_$i"}, conv_date($form->{"transdate_$i"}),
+                 $form->{"ordnumber_$i"}, $form->{"donumber_$i"}, conv_date($form->{"transdate_$i"}),
                  $form->{"cusordnumber_$i"}, $baseqty, $form->{"subtotal_$i"} ? 't' : 'f',
                  $form->{"marge_percent_$i"}, $form->{"marge_absolut_$i"},
                  $form->{"lastcost_$i"},
@@ -1652,7 +1653,7 @@ sub retrieve_invoice {
 
            i.id AS invoice_id,
            i.description, i.longdescription, i.qty, i.fxsellprice AS sellprice, i.discount, i.parts_id AS id, i.unit, i.deliverydate AS reqdate,
-           i.project_id, i.serialnumber, i.id AS invoice_pos, i.pricegroup_id, i.ordnumber, i.transdate, i.cusordnumber, i.subtotal, i.lastcost,
+           i.project_id, i.serialnumber, i.id AS invoice_pos, i.pricegroup_id, i.ordnumber, i.donumber, i.transdate, i.cusordnumber, i.subtotal, i.lastcost,
            i.price_factor_id, i.price_factor, i.marge_price_factor,
            p.partnumber, p.assembly, p.notes AS partnotes, p.inventory_accno_id AS part_inventory_accno_id, p.formel, p.listprice,
            pr.projectnumber, pg.partsgroup, prg.pricegroup
index 36186c4..23b8ad4 100644 (file)
@@ -116,6 +116,7 @@ sub get_template {
                     COMPILE_EXT  => '.tcc',
                     COMPILE_DIR  => $::lx_office_conf{paths}->{userspath} . '/templates-cache',
                     ERROR        => 'templates/webpages/generic/exception.html',
+                    ENCODING     => 'utf8',
                   }) || croak;
 
   return $self->{template};
index 40bf195..f935a59 100644 (file)
@@ -1,6 +1,7 @@
 package SL::Template::Plugin::LxERP;
 
 use base qw( Template::Plugin );
+use Scalar::Util qw();
 use Template::Plugin;
 
 use List::Util qw(min);
@@ -16,6 +17,16 @@ sub new {
   bless { }, $class;
 }
 
+sub is_rdbo {
+  my ($self, $obj, $wanted_class) = @_;
+
+  $wanted_class = !$wanted_class         ? 'Rose::DB::Object'
+                : $wanted_class =~ m{::} ? $wanted_class
+                :                          "SL::DB::${wanted_class}";
+
+  return Scalar::Util::blessed($obj) ? $obj->isa($wanted_class) : 0;
+}
+
 sub format_amount {
   my ($self, $var, $places, $skip_zero, $dash) = @_;
 
index e946e4e..06b8f65 100644 (file)
@@ -775,6 +775,7 @@ sub invoice {
       }
     }
     map { $form->{"${_}_${i}"} = $form->parse_amount(\%myconfig, $form->{"${_}_${i}"}) if $form->{"${_}_${i}"} } qw(ship qty sellprice listprice lastcost basefactor);
+    $form->{"donumber_$i"} = $form->{donumber};
   }
 
   $form->{type} = "invoice";
index e103891..d47168b 100644 (file)
@@ -50,6 +50,7 @@ use SL::IO;
 use SL::DB::Default;
 use SL::DB::Language;
 use SL::DB::Printer;
+use SL::Helper::CreatePDF;
 use SL::Helper::Flash;
 
 require "bin/mozilla/common.pl";
@@ -436,7 +437,7 @@ sub display_row {
           $cgi->hidden("-name" => "price_new_$i", "-value" => $form->format_amount(\%myconfig, $form->{"price_new_$i"})),
           map { ($cgi->hidden("-name" => $_, "-id" => $_, "-value" => $form->{$_})); } map { $_."_$i" }
             (qw(orderitems_id bo pricegroup_old price_old id inventory_accno bin partsgroup partnotes
-                income_accno expense_accno listprice assembly taxaccounts ordnumber transdate cusordnumber
+                income_accno expense_accno listprice assembly taxaccounts ordnumber donumber transdate cusordnumber
                 longdescription basefactor marge_absolut marge_percent marge_price_factor weight), @hidden_vars)
     );
 
@@ -749,7 +750,7 @@ sub remove_emptied_rows {
                 taxaccounts bin assembly weight projectnumber project_id
                 oldprojectnumber runningnumber serialnumber partsgroup payment_id
                 not_discountable shop ve gv buchungsgruppen_id language_values
-                sellprice_pg pricegroup_old price_old price_new unit_old ordnumber
+                sellprice_pg pricegroup_old price_old price_new unit_old ordnumber donumber
                 transdate longdescription basefactor marge_total marge_percent
                 marge_price_factor lastcost price_factor_id partnotes
                 stock_out stock_in has_sernumber reqdate);
@@ -1505,18 +1506,20 @@ sub print_form {
   }
 
   # search for the template
-  my @template_files;
-  push @template_files, "$form->{formname}_email$form->{language}$printer_code.$extension" if $form->{media} eq 'email';
-  push @template_files, "$form->{formname}$form->{language}$printer_code.$extension";
-  push @template_files, "$form->{formname}.$extension";
-  push @template_files, "default.$extension";
-  @template_files = uniq @template_files;
-  $form->{IN}     = first { -f ($defaults->templates . "/$_") } @template_files;
-
-  if (!defined $form->{IN}) {
+  my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
+    name        => $form->{formname},
+    email       => $form->{media} eq 'email',
+    language_id => $form->{language_id},
+    printer_id  => $form->{printer_id},
+    extension   => $extension,
+  );
+
+  if (!defined $template_file) {
     $::form->error($::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files));
   }
 
+  $form->{IN} = $template_file;
+
   delete $form->{OUT};
 
   if ($form->{media} eq 'printer') {
index 0f6fca5..aeeb56f 100644 (file)
@@ -3777,6 +3777,15 @@ ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</programlisting>
               </listitem>
             </varlistentry>
 
+            <varlistentry>
+              <term><varname>donumber_do</varname></term>
+
+              <listitem>
+                <para>Lieferscheinnummer desjenigen Lieferscheins, aus dem die Position stammt, sofern die Rechnung aus einem oder
+                mehreren Lieferscheinen erstellt wurde</para>
+              </listitem>
+            </varlistentry>
+
             <varlistentry>
               <term><varname>p_discount</varname></term>
 
index 81a978f..ac00872 100644 (file)
@@ -36,6 +36,10 @@ namespace("kivi", function(ns) {
       ns.run_once_for('input.part_autocomplete', 'part_picker', function(elt) {
         kivi.PartPicker($(elt));
       });
+
+    var func = kivi.get_function_by_name('local_reinit_widgets');
+    if (func)
+      func();
   };
 
   ns.submit_ajax_form = function(url, form_selector, additional_data) {
diff --git a/sql/Pg-upgrade2/invoice_add_donumber.sql b/sql/Pg-upgrade2/invoice_add_donumber.sql
new file mode 100644 (file)
index 0000000..4b3aa72
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: invoice_add_donumber
+-- @description: invoice_add_donumber
+-- @depends: release_3_0_0
+ALTER TABLE invoice ADD COLUMN donumber TEXT;
diff --git a/sql/Pg-upgrade2/unit_foreign_key_for_line_items.sql b/sql/Pg-upgrade2/unit_foreign_key_for_line_items.sql
new file mode 100644 (file)
index 0000000..d99e1a7
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: unit_foreign_key_for_line_items
+-- @description: Fremdschlüssel auf »unit« für Beleg-Positionstabellen
+-- @depends: release_3_0_0
+ALTER TABLE orderitems           ADD FOREIGN KEY (unit) REFERENCES units (name);
+ALTER TABLE delivery_order_items ADD FOREIGN KEY (unit) REFERENCES units (name);
+ALTER TABLE invoice              ADD FOREIGN KEY (unit) REFERENCES units (name);
index 33623f6..1117c1f 100644 (file)
@@ -1,45 +1,72 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE L %]
-[%- USE LxERP %]
-[%- BLOCK cvar_name %][% HTML.escape(cvar.name_prefix) _ "cvar_" _ HTML.escape(cvar.var.name) _ HTML.escape(cvar.name_postfix) -%][% END %]
+[%- USE LxERP %][%- USE P -%]
 [%- BLOCK cvar_inputs %]
-[% render_input_blocks__cvar_name = PROCESS cvar_name %]
-[%- %]
+[%- SET render_cvar_tag_options = {};
+    render_cvar_tag_options.import(cvar_tag_options) IF cvar_tag_options;
+    SET cvar_tag_name = HTML.escape(cvar.name_prefix) _ "cvar_" _ HTML.escape(cvar.var.name) _ HTML.escape(cvar.name_postfix);
+    IF !render_cvar_tag_options.id && (cvar.var.type != 'select');
+      SET render_cvar_tag_options.no_id = 1;
+    END;
+%]
 [%- IF cvar.hide_non_editable && !cvar.var.flag_editable %]
-<input type="hidden" name="[% PROCESS cvar_name %]" value="[% HTML.escape(cvar.var.value) %]">
+[%- L.hidden_tag(cvar_tag_name, cvar.var.value, render_cvar_tag_options) %]
 [%- ELSIF !cvar.valid %]
   [%- IF show_disabled_message %]
 <i>[% 'Element disabled' | $T8 %]</i>
   [%- END %]
+
 [%- ELSIF cvar.var.type == 'bool' %]
-<input type="checkbox" name="[% PROCESS cvar_name %]" value="1"[% IF cvar.value %] checked[% END %]>
+[%- render_cvar_tag_options.import(checked=cvar.value, value=1);
+    L.checkbox_tag(cvar_tag_name, render_cvar_tag_options) %]
+
 [%- ELSIF cvar.var.type == 'textfield' %]
-[% L.textarea_tag(render_input_blocks__cvar_name, cvar.value, cols=cvar.var.width, rows=cvar.var.height) %]
+[% render_cvar_tag_options.import(cols=cvar.var.width, rows=cvar.var.height);
+   L.textarea_tag(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
+
 [%- ELSIF cvar.var.type == 'date' %]
-[%- L.date_tag(render_input_blocks__cvar_name, cvar.value) %]
+[%- L.date_tag(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
 
 [%- ELSIF cvar.var.type == 'timestamp' %]
-<input name="[% PROCESS cvar_name %]" value="[% HTML.escape(cvar.value) %]">
+[%- L.input_tag(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
+
 [%- ELSIF cvar.var.type == 'select' %]
-<select name="[% PROCESS cvar_name %]">
- [%- FOREACH option = cvar.var.OPTIONS %]
- <option[% IF option.value == cvar.value %] selected[% END %]>[% HTML.escape(option.value) %]</option>
+[%- render_cvar_tag_options.name=cvar_tag_name %]
+<select [% P.stringify_attributes(render_cvar_tag_options) %]>
+ [%- IF LxERP.is_rdbo(cvar.var, 'CustomVariableConfig') %]
+  [%- FOREACH option = cvar.var.processed_options %]
+   [%- SET render_cvar_opts = {
+        value = option
+       };
+       render_cvar_opts.selected = 'selected' IF option == cvar.value;
+       L.html_tag('option', option, render_cvar_opts) %]
+  [%- END %]
+ [%- ELSE %]
+  [%- FOREACH option = cvar.var.OPTIONS %]
+   [%- SET render_cvar_opts = {
+        value = option.value
+       };
+       render_cvar_opts.selected = 'selected' IF option.value == cvar.value;
+       L.html_tag('option', option.value, render_cvar_opts) %]
+  [%- END %]
  [%- END %]
 </select>
+
 [%- ELSIF cvar.var.type == 'customer' %]
-[%- L.customer_picker(render_input_blocks__cvar_name, cvar.value) %]
+[%- L.customer_picker(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
 
 [%- ELSIF cvar.var.type == 'vendor' %]
-[% L.vendor_selector(render_input_blocks__cvar_name, cvar.value) %]
+[% L.vendor_selector(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
 
 [%- ELSIF cvar.var.type == 'part' %]
-[% L.part_selector(render_input_blocks__cvar_name, cvar.value) %]
+[% L.part_selector(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
 
 [%- ELSIF cvar.var.type == 'number' %]
-[%- L.input_tag(render_input_blocks__cvar_name, LxERP.format_amount(cvar.value, -2)) %]
+[%- L.input_tag(cvar_tag_name, LxERP.format_amount(cvar.value, -2), render_cvar_tag_options) %]
 
 [%- ELSE %]
-<input name="[% PROCESS cvar_name %]" value="[% HTML.escape(cvar.value) %]" [%- IF cvar.var.maxlength %] maxlength="[% HTML.escape(cvar.var.maxlength) %]"[% END -%]>
+[% render_cvar_tag_options.maxlength=cvar.var.maxlength IF cvar.var.maxlength;
+   L.input_tag(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
 [%- END %]
 [%- END %]