]> wagnertech.de Git - mfinanz.git/blobdiff - SL/DB/Order.pm
kivitendo 3.9.2-0.2
[mfinanz.git] / SL / DB / Order.pm
index fdaa1e8bf2f69ce65546e01ee6c71c5fcbd8a0b5..07d58928d54a218872788c861f94c4be4e1cbe89 100644 (file)
@@ -8,6 +8,8 @@ use DateTime;
 use List::Util qw(max);
 use List::MoreUtils qw(any);
 
+use SL::DBUtils ();
+use SL::DB::PurchaseBasketItem;
 use SL::DB::MetaSetup::Order;
 use SL::DB::Manager::Order;
 use SL::DB::Helper::Attr;
@@ -17,10 +19,17 @@ use SL::DB::Helper::FlattenToForm;
 use SL::DB::Helper::LinkedRecords;
 use SL::DB::Helper::PriceTaxCalculator;
 use SL::DB::Helper::PriceUpdater;
+use SL::DB::Helper::TypeDataProxy;
 use SL::DB::Helper::TransNumberGenerator;
+use SL::DB::Helper::Payment qw(forex);
+use SL::DB::Helper::RecordLink qw(RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
+use SL::Helper::Flash;
 use SL::Locale::String qw(t8);
 use SL::RecordLinks;
-use Rose::DB::Object::Helpers qw(as_tree);
+use Rose::DB::Object::Helpers qw(as_tree strip);
+
+use SL::DB::Order::TypeData qw(:types validate_type);
+use SL::DB::Reclamation::TypeData qw(:types);
 
 __PACKAGE__->meta->add_relationship(
   orderitems => {
@@ -57,6 +66,11 @@ __PACKAGE__->meta->add_relationship(
       sort_by      => 'notes.itime',
     }
   },
+  order_version => {
+    type                   => 'one to many',
+    class                  => 'SL::DB::OrderVersion',
+    column_map             => { id => 'oe_id' },
+  },
 );
 
 SL::DB::Helper::Attr::make(__PACKAGE__, daily_exchangerate => 'numeric');
@@ -70,6 +84,9 @@ __PACKAGE__->before_save('_before_save_set_ord_quo_number');
 __PACKAGE__->before_save('_before_save_create_new_project');
 __PACKAGE__->before_save('_before_save_remove_empty_custom_shipto');
 __PACKAGE__->before_save('_before_save_set_custom_shipto_module');
+__PACKAGE__->after_save('_after_save_link_records');
+__PACKAGE__->after_save('_after_save_close_reachable_intakes'); # uses linked records (order matters)
+__PACKAGE__->before_save('_before_save_delete_from_purchase_basket');
 
 # hooks
 
@@ -80,8 +97,7 @@ sub _before_save_set_ord_quo_number {
   # least an empty string, even if we're saving a quotation.
   $self->ordnumber('') if !$self->ordnumber;
 
-  my $field = $self->quotation ? 'quonumber' : 'ordnumber';
-  $self->create_trans_number if !$self->$field;
+  $self->create_trans_number if !$self->record_number;
 
   return 1;
 }
@@ -89,7 +105,7 @@ sub _before_save_create_new_project {
   my ($self) = @_;
 
   # force new project, if not set yet
-  if ($::instance_conf->get_order_always_project && !$self->globalproject_id && ($self->type eq 'sales_order')) {
+  if ($::instance_conf->get_order_always_project && !$self->globalproject_id && ($self->type eq SALES_ORDER_TYPE())) {
 
     die t8("Error while creating project with project number of new order number, project number #1 already exists!", $self->ordnumber)
       if SL::DB::Manager::Project->find_by(projectnumber => $self->ordnumber);
@@ -127,6 +143,63 @@ sub _before_save_set_custom_shipto_module {
   return 1;
 }
 
+sub _after_save_link_records {
+  my ($self) = @_;
+
+  my @allowed_record_sources = qw(SL::DB::Reclamation SL::DB::Order SL::DB::EmailJournal);
+  my @allowed_item_sources = qw(SL::DB::ReclamationItem SL::DB::OrderItem);
+
+  SL::DB::Helper::RecordLink::link_records(
+    $self,
+    \@allowed_record_sources,
+    \@allowed_item_sources,
+  );
+
+  return 1;
+}
+
+sub _after_save_close_reachable_intakes {
+  my ($self) = @_;
+
+  # Close reachable sales order intakes in the from-workflow if this is a sales order
+  if (SALES_ORDER_TYPE() eq $self->type) {
+    my $lr = $self->linked_records(direction => 'from', recursive => 1);
+    $lr    = [grep { 'SL::DB::Order' eq ref $_ && !$_->closed && $_->is_type(SALES_ORDER_INTAKE_TYPE()) } @$lr];
+    if (@$lr) {
+      SL::DB::Manager::Order->update_all(set   => {closed => 1},
+                                         where => [id => [map {$_->id} @$lr]]);
+    }
+  }
+
+  return 1;
+}
+
+sub _before_save_delete_from_purchase_basket {
+  my ($self) = @_;
+
+  my @basket_item_ids =
+    grep { defined($_) && $_ ne ''}
+    map { $_->{basket_item_id} }
+    $self->orderitems;
+  return 1 unless scalar @basket_item_ids;
+
+  # check if all items are still in the basket
+  my $basket_item_count = SL::DB::Manager::PurchaseBasketItem->get_all_count(
+    where => [ id => \@basket_item_ids ]
+  );
+  if ($basket_item_count != scalar @basket_item_ids) {
+    die "Error while saving order: some items are not in the purchase basket anymore.";
+  }
+
+  if (scalar @basket_item_ids) {
+    SL::DB::Manager::PurchaseBasketItem->delete_all(
+      where => [ id => \@basket_item_ids]
+    );
+  }
+
+  return 1;
+}
+
 # methods
 
 sub items { goto &orderitems; }
@@ -135,25 +208,38 @@ sub record_number { goto &number; }
 
 sub type {
   my $self = shift;
-
-  return 'sales_order'       if $self->customer_id && ! $self->quotation;
-  return 'purchase_order'    if $self->vendor_id   && ! $self->quotation;
-  return 'sales_quotation'   if $self->customer_id &&   $self->quotation;
-  return 'request_quotation' if $self->vendor_id   &&   $self->quotation;
-
-  return;
+  SL::DB::Order::TypeData::validate_type($self->record_type);
+  return $self->record_type;
 }
 
 sub is_type {
   return shift->type eq shift;
 }
 
+sub quotation {
+  my $type = $_[0]->type();
+  any { $type eq $_ } (
+    SALES_ORDER_INTAKE_TYPE(),
+    SALES_QUOTATION_TYPE(),
+    REQUEST_QUOTATION_TYPE(),
+    PURCHASE_QUOTATION_INTAKE_TYPE(),
+  );
+}
+
+sub intake {
+  my $type = $_[0]->type();
+  any { $type eq $_ } (
+    SALES_ORDER_INTAKE_TYPE(),
+    PURCHASE_QUOTATION_INTAKE_TYPE(),
+  );
+}
+
 sub deliverydate {
   # oe doesn't have deliverydate, but it does have reqdate.
   # But this has a different meaning for sales quotations.
   # deliverydate can be used to determine tax if tax_point isn't set.
 
-  return $_[0]->reqdate if $_[0]->type ne 'sales_quotation';
+  return $_[0]->reqdate if $_[0]->type ne SALES_QUOTATION_TYPE();
 }
 
 sub effective_tax_point {
@@ -163,14 +249,8 @@ sub effective_tax_point {
 }
 
 sub displayable_type {
-  my $type = shift->type;
-
-  return $::locale->text('Sales quotation')   if $type eq 'sales_quotation';
-  return $::locale->text('Request quotation') if $type eq 'request_quotation';
-  return $::locale->text('Sales Order')       if $type eq 'sales_order';
-  return $::locale->text('Purchase Order')    if $type eq 'purchase_order';
-
-  die 'invalid type';
+  my ($self) = @_;
+  return $self->type_data->text('type');
 }
 
 sub displayable_name {
@@ -179,7 +259,7 @@ sub displayable_name {
 
 sub is_sales {
   croak 'not an accessor' if @_ > 1;
-  return !!shift->customer_id;
+  $_[0]->type_data->properties('is_customer');
 }
 
 sub daily_exchangerate {
@@ -187,8 +267,8 @@ sub daily_exchangerate {
 
   return 1 if $self->currency_id == $::instance_conf->get_currency_id;
 
-  my $rate = (any { $self->is_type($_) } qw(sales_quotation sales_order))      ? 'buy'
-           : (any { $self->is_type($_) } qw(request_quotation purchase_order)) ? 'sell'
+  my $rate = (any { $self->is_type($_) } (SALES_QUOTATION_TYPE(), SALES_ORDER_TYPE()))      ? 'buy'
+           : (any { $self->is_type($_) } (REQUEST_QUOTATION_TYPE(), PURCHASE_ORDER_TYPE())) ? 'sell'
            : undef;
   return if !$rate;
 
@@ -249,22 +329,6 @@ sub convert_to_invoice {
   if (!$self->db->with_transaction(sub {
     require SL::DB::Invoice;
     $invoice = SL::DB::Invoice->new_from($self, %params)->post || die;
-    $self->link_to_record($invoice);
-    # TODO extend link_to_record for items, otherwise long-term no d.r.y.
-    foreach my $item (@{ $invoice->items }) {
-      foreach (qw(orderitems)) {
-        if ($item->{"converted_from_${_}_id"}) {
-          die unless $item->{id};
-          RecordLinks->create_links('mode'       => 'ids',
-                                    'from_table' => $_,
-                                    'from_ids'   => $item->{"converted_from_${_}_id"},
-                                    'to_table'   => 'invoice',
-                                    'to_id'      => $item->{id},
-          ) || die;
-          delete $item->{"converted_from_${_}_id"};
-        }
-      }
-    }
     $self->update_attributes(closed => 1);
     1;
   })) {
@@ -282,23 +346,6 @@ sub convert_to_delivery_order {
     require SL::DB::DeliveryOrder;
     $delivery_order = SL::DB::DeliveryOrder->new_from($self, @args);
     $delivery_order->save;
-    $self->link_to_record($delivery_order);
-    # TODO extend link_to_record for items, otherwise long-term no d.r.y.
-    foreach my $item (@{ $delivery_order->items }) {
-      foreach (qw(orderitems)) {    # expand if needed (delivery_order_items)
-        if ($item->{"converted_from_${_}_id"}) {
-          die unless $item->{id};
-          RecordLinks->create_links('dbh'        => $self->db->dbh,
-                                    'mode'       => 'ids',
-                                    'from_table' => $_,
-                                    'from_ids'   => $item->{"converted_from_${_}_id"},
-                                    'to_table'   => 'delivery_order_items',
-                                    'to_id'      => $item->{id},
-          ) || die;
-          delete $item->{"converted_from_${_}_id"};
-        }
-      }
-    }
 
     $self->update_attributes(delivered => 1) unless $::instance_conf->get_shipped_qty_require_stock_out;
     1;
@@ -309,6 +356,17 @@ sub convert_to_delivery_order {
   return $delivery_order;
 }
 
+sub convert_to_reclamation {
+  my ($self, %params) = @_;
+  $params{destination_type} = $self->is_sales ? SALES_RECLAMATION_TYPE()
+                                              : PURCHASE_RECLAMATION_TYPE();
+
+  require SL::DB::Reclamation;
+  my $reclamation = SL::DB::Reclamation->new_from($self, %params);
+
+  return $reclamation;
+}
+
 sub _clone_orderitem_cvar {
   my ($cvar) = @_;
 
@@ -318,73 +376,235 @@ sub _clone_orderitem_cvar {
   return $cloned;
 }
 
+sub create_from_purchase_basket {
+  my ($class, $basket_item_ids, $vendor_item_ids, $vendor_id) = @_;
+
+  my ($vendor, $employee);
+  $vendor   = SL::DB::Manager::Vendor->find_by(id => $vendor_id);
+  $employee = SL::DB::Manager::Employee->current;
+
+  my @orderitem_maps = (); # part, qty, orderer_id
+  if ($basket_item_ids && scalar @{ $basket_item_ids}) {
+    my $basket_items = SL::DB::Manager::PurchaseBasketItem->get_all(
+      query => [ id => $basket_item_ids ],
+      with_objects => ['part'],
+    );
+    push @orderitem_maps, map {{
+        basket_item_id => $_->id,
+        part       => $_->part,
+        qty        => $_->qty,
+        orderer_id => $_->orderer_id,
+      }} @{$basket_items};
+  }
+  if ($vendor_item_ids && scalar @{ $vendor_item_ids}) {
+    my $vendor_items = SL::DB::Manager::Part->get_all(
+      query => [ id => $vendor_item_ids ] );
+    push @orderitem_maps, map {{
+        basket_item_id => undef,
+        part       => $_,
+        qty        => $_->order_qty || 1,
+        orderer_id => $employee->id,
+      }} @{$vendor_items};
+  }
+
+  my $order = $class->new(
+    vendor_id               => $vendor->id,
+    employee_id             => $employee->id,
+    intnotes                => $vendor->notes,
+    salesman_id             => $employee->id,
+    payment_id              => $vendor->payment_id,
+    delivery_term_id        => $vendor->delivery_term_id,
+    taxzone_id              => $vendor->taxzone_id,
+    currency_id             => $vendor->currency_id,
+    transdate               => DateTime->today_local,
+    record_type             => PURCHASE_ORDER_TYPE(),
+  );
+
+  my @order_items;
+  my $i = 0;
+  foreach my $orderitem_map (@orderitem_maps) {
+    $i++;
+    my $part = $orderitem_map->{part};
+    my $qty = $orderitem_map->{qty};
+    my $orderer_id = $orderitem_map->{orderer_id};
+
+    my $order_item = SL::DB::OrderItem->new(
+      part                => $part,
+      qty                 => $qty,
+      unit                => $part->unit,
+      description         => $part->description,
+      price_factor_id     => $part->price_factor_id,
+      price_factor        =>
+        $part->price_factor_id ? $part->price_factor->factor
+                               : '',
+      orderer_id          => $orderer_id,
+      position            => $i,
+    );
+    $order_item->{basket_item_id} = $orderitem_map->{basket_item_id};
+
+    my $price_source  = SL::PriceSource->new(
+      record_item => $order_item, record => $order);
+    $order_item->sellprice(
+      $price_source->best_price ? $price_source->best_price->price
+                                : 0);
+    $order_item->active_price_source(
+      $price_source->best_price ? $price_source->best_price->source
+                                : '');
+    push @order_items, $order_item;
+  }
+
+  $order->assign_attributes(orderitems => \@order_items);
+
+  $order->calculate_prices_and_taxes;
+
+  foreach my $item(@{ $order->orderitems }){
+    $item->parse_custom_variable_values;
+    $item->{custom_variables} = \@{ $item->cvars_by_config };
+  }
+
+  return $order;
+}
+
 sub new_from {
   my ($class, $source, %params) = @_;
 
-  croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) eq 'SL::DB::Order';
+  unless (any {ref($source) eq $_} qw(
+    SL::DB::Order
+    SL::DB::Reclamation
+  )) {
+    croak("Unsupported source object type '" . ref($source) . "'");
+  }
   croak("A destination type must be given as parameter")         unless $params{destination_type};
 
   my $destination_type  = delete $params{destination_type};
 
   my @from_tos = (
-    { from => 'sales_quotation',   to => 'sales_order',       abbr => 'sqso' },
-    { from => 'request_quotation', to => 'purchase_order',    abbr => 'rqpo' },
-    { from => 'sales_quotation',   to => 'sales_quotation',   abbr => 'sqsq' },
-    { from => 'sales_order',       to => 'sales_order',       abbr => 'soso' },
-    { from => 'request_quotation', to => 'request_quotation', abbr => 'rqrq' },
-    { from => 'purchase_order',    to => 'purchase_order',    abbr => 'popo' },
-    { from => 'sales_order',       to => 'purchase_order',    abbr => 'sopo' },
-    { from => 'purchase_order',    to => 'sales_order',       abbr => 'poso' },
-    { from => 'sales_order',       to => 'sales_quotation',   abbr => 'sosq' },
-    { from => 'purchase_order',    to => 'request_quotation', abbr => 'porq' },
+    { from => SALES_QUOTATION_TYPE(),             to => SALES_ORDER_TYPE(),                 abbr => 'sqso'   },
+    { from => REQUEST_QUOTATION_TYPE(),           to => PURCHASE_ORDER_TYPE(),              abbr => 'rqpo'   },
+    { from => SALES_QUOTATION_TYPE(),             to => SALES_QUOTATION_TYPE(),             abbr => 'sqsq'   },
+    { from => SALES_ORDER_TYPE(),                 to => SALES_ORDER_TYPE(),                 abbr => 'soso'   },
+    { from => REQUEST_QUOTATION_TYPE(),           to => REQUEST_QUOTATION_TYPE(),           abbr => 'rqrq'   },
+    { from => PURCHASE_ORDER_TYPE(),              to => PURCHASE_ORDER_TYPE(),              abbr => 'popo'   },
+    { from => SALES_ORDER_TYPE(),                 to => PURCHASE_ORDER_TYPE(),              abbr => 'sopo'   },
+    { from => PURCHASE_ORDER_TYPE(),              to => SALES_ORDER_TYPE(),                 abbr => 'poso'   },
+    { from => SALES_ORDER_TYPE(),                 to => SALES_QUOTATION_TYPE(),             abbr => 'sosq'   },
+    { from => PURCHASE_ORDER_TYPE(),              to => REQUEST_QUOTATION_TYPE(),           abbr => 'porq'   },
+    { from => REQUEST_QUOTATION_TYPE(),           to => SALES_QUOTATION_TYPE(),             abbr => 'rqsq'   },
+    { from => REQUEST_QUOTATION_TYPE(),           to => SALES_ORDER_TYPE(),                 abbr => 'rqso'   },
+    { from => SALES_QUOTATION_TYPE(),             to => REQUEST_QUOTATION_TYPE(),           abbr => 'sqrq'   },
+    { from => SALES_ORDER_TYPE(),                 to => REQUEST_QUOTATION_TYPE(),           abbr => 'sorq'   },
+    { from => SALES_RECLAMATION_TYPE(),           to => SALES_ORDER_TYPE(),                 abbr => 'srso'   },
+    { from => PURCHASE_RECLAMATION_TYPE(),        to => PURCHASE_ORDER_TYPE(),              abbr => 'prpo'   },
+    { from => SALES_ORDER_INTAKE_TYPE(),          to => SALES_ORDER_INTAKE_TYPE(),          abbr => 'soisoi' },
+    { from => SALES_ORDER_INTAKE_TYPE(),          to => SALES_QUOTATION_TYPE(),             abbr => 'soisq'  },
+    { from => SALES_ORDER_INTAKE_TYPE(),          to => REQUEST_QUOTATION_TYPE(),           abbr => 'soirq'  },
+    { from => SALES_ORDER_INTAKE_TYPE(),          to => SALES_ORDER_TYPE(),                 abbr => 'soiso'  },
+    { from => SALES_ORDER_INTAKE_TYPE(),          to => PURCHASE_ORDER_TYPE(),              abbr => 'soipo'  },
+    { from => SALES_QUOTATION_TYPE(),             to => SALES_ORDER_INTAKE_TYPE(),          abbr => 'sqsoi'  },
+    { from => PURCHASE_QUOTATION_INTAKE_TYPE(),   to => PURCHASE_QUOTATION_INTAKE_TYPE(),   abbr => 'pqipqi' },
+    { from => PURCHASE_QUOTATION_INTAKE_TYPE(),   to => SALES_QUOTATION_TYPE(),             abbr => 'pqisq'  },
+    { from => PURCHASE_QUOTATION_INTAKE_TYPE(),   to => SALES_ORDER_TYPE(),                 abbr => 'pqiso'  },
+    { from => PURCHASE_QUOTATION_INTAKE_TYPE(),   to => PURCHASE_ORDER_TYPE(),              abbr => 'pqipo'  },
+    { from => REQUEST_QUOTATION_TYPE(),           to => PURCHASE_QUOTATION_INTAKE_TYPE(),   abbr => 'rqpqi'  },
+    { from => PURCHASE_ORDER_CONFIRMATION_TYPE(), to => PURCHASE_ORDER_CONFIRMATION_TYPE(), abbr => 'pocpoc' },
+    { from => PURCHASE_ORDER_CONFIRMATION_TYPE(), to => SALES_QUOTATION_TYPE(),             abbr => 'pocsq' },
+    { from => PURCHASE_ORDER_CONFIRMATION_TYPE(), to => SALES_ORDER_TYPE(),                 abbr => 'pocso' },
+    { from => PURCHASE_ORDER_CONFIRMATION_TYPE(), to => PURCHASE_ORDER_TYPE(),              abbr => 'pocpo' },
+    { from => PURCHASE_ORDER_TYPE(),              to => PURCHASE_ORDER_CONFIRMATION_TYPE(), abbr => 'popoc' },
   );
-  my $from_to = (grep { $_->{from} eq $source->type && $_->{to} eq $destination_type} @from_tos)[0];
-  croak("Cannot convert from '" . $source->type . "' to '" . $destination_type . "'") if !$from_to;
+  my $from_to = (grep { $_->{from} eq $source->record_type && $_->{to} eq $destination_type} @from_tos)[0];
+  croak("Cannot convert from '" . $source->record_type . "' to '" . $destination_type . "'") if !$from_to;
 
   my $is_abbr_any = sub {
-    any { $from_to->{abbr} eq $_ } @_;
-  };
+    my (@abbrs) = @_;
 
-  my ($item_parent_id_column, $item_parent_column);
+    my $missing_abbr;
+    if (any { $missing_abbr = $_; !grep { $_->{abbr} eq $missing_abbr } @from_tos } @abbrs) {
+      die "no such workflow abbreviation '$missing_abbr'";
+    }
+
+    any { $from_to->{abbr} eq $_ } @abbrs;
+  };
 
+  my %args;
   if (ref($source) eq 'SL::DB::Order') {
-    $item_parent_id_column = 'trans_id';
-    $item_parent_column    = 'order';
+    %args = ( map({ ( $_ => $source->$_ ) } qw(amount cp_id currency_id cusordnumber customer_id delivery_customer_id delivery_term_id delivery_vendor_id
+                                               department_id exchangerate globalproject_id intnotes marge_percent marge_total language_id netamount notes
+                                               ordnumber payment_id quonumber reqdate salesman_id shippingpoint shipvia taxincluded tax_point taxzone_id
+                                               transaction_description vendor_id billing_address_id
+                                            )),
+                 closed    => 0,
+                 delivered => 0,
+                 transdate => DateTime->today_local,
+                 employee  => SL::DB::Manager::Employee->current,
+              );
+    # reqdate in quotation is 'offer is valid    until reqdate'
+    # reqdate in order     is 'will be delivered until reqdate'
+    # both dates are setable (on|off)
+    # and may have a additional interval in days (+ n days)
+    # dies if this convention will change
+    $args{reqdate} = $from_to->{to} =~ m/_quotation$/
+                   ? $::instance_conf->get_reqdate_on
+                   ? DateTime->today_local->next_workday(extra_days => $::instance_conf->get_reqdate_interval)->to_kivitendo
+                   : undef
+                   : $from_to->{to} =~ m/_order$/
+                   ? $::instance_conf->get_deliverydate_on
+                   ? DateTime->today_local->next_workday(extra_days => $::instance_conf->get_delivery_date_interval)->to_kivitendo
+                   : undef
+                   : $from_to->{to} =~ m/^sales_order_intake$/
+                   # ? $source->reqdate
+                   ? undef
+                   : $from_to->{to} =~ m/^purchase_quotation_intake$/
+                   ? $source->reqdate
+                   : $from_to->{to} =~ m/^purchase_order_confirmation$/
+                   ? $source->reqdate
+                   : die "Wrong state for reqdate";
+  } elsif ( ref($source) eq 'SL::DB::Reclamation') {
+    %args = ( map({ ( $_ => $source->$_ ) } qw(
+        amount billing_address_id currency_id customer_id delivery_term_id department_id
+        exchangerate globalproject_id intnotes language_id netamount
+        notes payment_id  reqdate salesman_id shippingpoint shipvia taxincluded
+        tax_point taxzone_id transaction_description vendor_id
+      )),
+      cp_id     => $source->{contact_id},
+      closed    => 0,
+      delivered => 0,
+      transdate => DateTime->today_local,
+      employee  => SL::DB::Manager::Employee->current,
+   );
   }
 
-  my %args = ( map({ ( $_ => $source->$_ ) } qw(amount cp_id currency_id cusordnumber customer_id delivery_customer_id delivery_term_id delivery_vendor_id
-                                                department_id exchangerate globalproject_id intnotes marge_percent marge_total language_id netamount notes
-                                                ordnumber payment_id quonumber reqdate salesman_id shippingpoint shipvia taxincluded tax_point taxzone_id
-                                                transaction_description vendor_id billing_address_id
-                                             )),
-               quotation => !!($destination_type =~ m{quotation$}),
-               closed    => 0,
-               delivered => 0,
-               transdate => DateTime->today_local,
-               employee  => SL::DB::Manager::Employee->current,
-            );
-
-  if ( $is_abbr_any->(qw(sopo poso)) ) {
+  if ( $is_abbr_any->(qw(soipo sopo poso rqso soisq sosq porq rqsq sqrq soirq sorq pqisq pqiso pocsq pocso)) ) {
     $args{ordnumber} = undef;
     $args{quonumber} = undef;
-    $args{reqdate}   = DateTime->today_local->next_workday();
   }
-  if ( $is_abbr_any->(qw(sopo)) ) {
+  if ( $is_abbr_any->(qw(soipo sopo sqrq soirq sorq)) ) {
     $args{customer_id}      = undef;
     $args{salesman_id}      = undef;
     $args{payment_id}       = undef;
     $args{delivery_term_id} = undef;
   }
-  if ( $is_abbr_any->(qw(poso)) ) {
+  if ( $is_abbr_any->(qw(poso rqsq pqisq pqiso pocsq pocso)) ) {
     $args{vendor_id} = undef;
   }
   if ( $is_abbr_any->(qw(soso)) ) {
-    $args{periodic_invoices_config} = $source->periodic_invoices_config->clone_and_reset if $source->periodic_invoices_config;
+    if ($source->periodic_invoices_config) {
+      $args{periodic_invoices_config} = $source->periodic_invoices_config->clone_and_reset;
+
+      if ($args{periodic_invoices_config}->active == 1) {
+        $args{periodic_invoices_config}->active(0);
+        flash_later('info', $::locale->text('Periodic invoices config set to inactive.'));
+      }
+    }
   }
-  if ( $is_abbr_any->(qw(sosq porq)) ) {
+  if ( $is_abbr_any->(qw(sqrq soirq sorq)) ) {
+    $args{cusordnumber} = undef;
+  }
+  if ( $is_abbr_any->(qw(soiso pocpoc pocpo popoc)) ) {
     $args{ordnumber} = undef;
+  }
+  if ( $is_abbr_any->(qw(rqpqi pqisq)) ) {
     $args{quonumber} = undef;
-    $args{reqdate}   = DateTime->today_local->next_workday();
   }
 
   # Custom shipto addresses (the ones specific to the sales/purchase
@@ -398,38 +618,50 @@ sub new_from {
     $args{shipto_id} = $source->shipto_id;
   }
 
+  $args{record_type} = $destination_type;
+
   my $order = $class->new(%args);
   $order->assign_attributes(%{ $params{attributes} }) if $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};
-
-    my $current_oe_item = SL::DB::OrderItem->new(map({ ( $_ => $source_item->$_ ) }
-                                                     qw(active_discount_source active_price_source base_qty cusordnumber
-                                                        description discount lastcost longdescription
-                                                        marge_percent marge_price_factor marge_total
-                                                        ordnumber parts_id price_factor price_factor_id pricegroup_id
-                                                        project_id qty reqdate sellprice serialnumber ship subtotal transdate unit
-                                                        optional
-                                                     )),
-                                                 custom_variables => \@custom_variables,
-    );
-    if ( $is_abbr_any->(qw(sopo)) ) {
+    my $current_oe_item;
+    if (ref($source) eq 'SL::DB::Order') {
+      $current_oe_item = SL::DB::OrderItem->new(map({ ( $_ => $source_item->$_ ) }
+                                                       qw(active_discount_source active_price_source base_qty cusordnumber
+                                                          description discount lastcost longdescription
+                                                          marge_percent marge_price_factor marge_total
+                                                          ordnumber parts_id price_factor price_factor_id pricegroup_id
+                                                          project_id qty reqdate sellprice serialnumber ship subtotal transdate unit
+                                                          optional recurring_billing_mode position
+                                                       )),
+                                                   custom_variables => \@custom_variables,
+      );
+    } elsif (ref($source) eq 'SL::DB::Reclamation') {
+      $current_oe_item = SL::DB::OrderItem->new(
+        map({ ( $_ => $source_item->$_ ) } qw(
+          active_discount_source active_price_source base_qty description
+          discount lastcost longdescription parts_id price_factor
+          price_factor_id pricegroup_id project_id qty reqdate sellprice
+          serialnumber unit position
+        )),
+        custom_variables => \@custom_variables,
+      );
+    }
+    if ( $is_abbr_any->(qw(soipo sopo)) ) {
       $current_oe_item->sellprice($source_item->lastcost);
       $current_oe_item->discount(0);
     }
-    if ( $is_abbr_any->(qw(poso)) ) {
+    if ( $is_abbr_any->(qw(poso rqsq rqso pqisq pqiso pocsq pocso)) ) {
       $current_oe_item->lastcost($source_item->sellprice);
     }
-    $current_oe_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
+    unless ($params{no_linked_records}) {
+      $current_oe_item->{ RECORD_ITEM_ID() } = $source_item->{id};
+      $current_oe_item->{ RECORD_ITEM_TYPE_REF() } = ref($source_item);
+    }
     $current_oe_item;
   } @{ $items };
 
@@ -439,6 +671,11 @@ sub new_from {
 
   $order->items(\@items);
 
+  unless ($params{no_linked_records}) {
+    $order->{ RECORD_ID()       } = $source->{id};
+    $order->{ RECORD_TYPE_REF() } = ref($source);
+  }
+
   return $order;
 }
 
@@ -506,10 +743,11 @@ sub new_from_multi {
   push @items, @{$_->items_sorted} for @$sources;
   # make order from first source and all items
   my $order = $class->new_from($sources->[0],
-                               destination_type => 'sales_order',
+                               destination_type => SALES_ORDER_TYPE(),
                                attributes       => \%attributes,
                                items            => \@items,
                                %params);
+  $order->{RECORD_ID()} = join ' ', map { $_->id } @$sources; # link all sources
 
   return $order;
 }
@@ -517,20 +755,12 @@ sub new_from_multi {
 sub number {
   my $self = shift;
 
-  return if !$self->type;
-
-  my %number_method = (
-    sales_order       => 'ordnumber',
-    sales_quotation   => 'quonumber',
-    purchase_order    => 'ordnumber',
-    request_quotation => 'quonumber',
-  );
-
-  return $self->${ \ $number_method{$self->type} }(@_);
+  my $nr_key = $self->type_data->properties('nr_key');
+  return $self->$nr_key(@_);
 }
 
 sub customervendor {
-  $_[0]->is_sales ? $_[0]->customer : $_[0]->vendor;
+  $_[0]->type_data->properties('is_customer') ? $_[0]->customer : $_[0]->vendor;
 }
 
 sub date {
@@ -547,6 +777,77 @@ sub digest {
     $self->date->to_kivitendo;
 }
 
+sub current_version_number {
+  my ($self) = @_;
+
+  my $query = <<EOSQL;
+    SELECT max(version)
+    FROM oe_version
+    WHERE (oe_id = ?)
+EOSQL
+
+  my ($current_version_number) = SL::DBUtils::selectfirst_array_query($::form, $self->db->dbh, $query, ($self->id));
+  die "Invalid State. No version linked" unless $current_version_number;
+
+  return $current_version_number;
+}
+
+sub is_final_version {
+  my ($self) = @_;
+
+  my $order_versions_count = SL::DB::Manager::OrderVersion->get_all_count(where => [ oe_id => $self->id, final_version => 0 ]);
+  die "Invalid version state" unless $order_versions_count < 2;
+  my $final_version = $order_versions_count == 1 ? 0 : 1;
+
+  return $final_version;
+}
+
+sub increment_version_number {
+  my ($self) = @_;
+
+  die t8('This sub-version is not yet finalized') if !$self->is_final_version;
+
+  my $current_version_number = $self->current_version_number;
+  my $new_version_number     = $current_version_number + 1;
+
+  my $new_number = $self->number;
+  $new_number    =~ s/-$current_version_number$//;
+  $self->number($new_number . '-' . $new_version_number);
+  $self->add_order_version(SL::DB::OrderVersion->new(version => $new_version_number));
+}
+
+sub netamount_base_currency {
+  my ($self) = @_;
+
+  return $self->netamount unless $self->forex;
+
+  if ( defined $self->exchangerate ) {
+    return $self->netamount * $self->exchangerate;
+  } else {
+    return $self->netamount * $self->daily_exchangerate;
+  }
+}
+
+sub preceding_purchase_orders {
+  my ($self) = @_;
+
+  my @lrs = ();
+  if ($self->id) {
+    @lrs = grep { $_->record_type eq PURCHASE_ORDER_TYPE() } @{$self->linked_records(from => 'SL::DB::Order')};
+  } else {
+    if ('SL::DB::Order' eq $self->{RECORD_TYPE_REF()}) {
+      my $order = SL::DB::Order->load_cached($self->{RECORD_ID()});
+      push @lrs, $order if $order->record_type eq PURCHASE_ORDER_TYPE();
+    }
+  }
+
+  return \@lrs;
+}
+
+sub type_data {
+  SL::DB::Helper::TypeDataProxy->new(ref $_[0], $_[0]->type);
+}
+
 1;
 
 __END__
@@ -709,6 +1010,12 @@ saved.
 
 C<params> other then C<sort_sources_by> are passed to C<new_from>.
 
+=head2 C<increment_version_number>
+
+Checks if the current version of the order is finalized, increments
+the version number and adds a new order_version to the order.
+Dies if the version is not final.
+
 =head1 BUGS
 
 Nothing here yet.