Merge pull request #19 from robert-scheck/capital-sharp-s
[kivitendo-erp.git] / SL / DB / Order.pm
index 28b1b57..6baa0e2 100644 (file)
@@ -9,6 +9,8 @@ use List::Util qw(max);
 
 use SL::DB::MetaSetup::Order;
 use SL::DB::Manager::Order;
 
 use SL::DB::MetaSetup::Order;
 use SL::DB::Manager::Order;
+use SL::DB::Helper::AttrHTML;
+use SL::DB::Helper::AttrSorted;
 use SL::DB::Helper::FlattenToForm;
 use SL::DB::Helper::LinkedRecords;
 use SL::DB::Helper::PriceTaxCalculator;
 use SL::DB::Helper::FlattenToForm;
 use SL::DB::Helper::LinkedRecords;
 use SL::DB::Helper::PriceTaxCalculator;
@@ -41,6 +43,9 @@ __PACKAGE__->meta->add_relationship(
 
 __PACKAGE__->meta->initialize;
 
 
 __PACKAGE__->meta->initialize;
 
+__PACKAGE__->attr_html('notes');
+__PACKAGE__->attr_sorted('items');
+
 __PACKAGE__->before_save('_before_save_set_ord_quo_number');
 
 # hooks
 __PACKAGE__->before_save('_before_save_set_ord_quo_number');
 
 # hooks
@@ -62,12 +67,7 @@ sub _before_save_set_ord_quo_number {
 
 sub items { goto &orderitems; }
 sub add_items { goto &add_orderitems; }
 
 sub items { goto &orderitems; }
 sub add_items { goto &add_orderitems; }
-
-sub items_sorted {
-  my ($self) = @_;
-
-  return [ sort {$a->position <=> $b->position } @{ $self->items } ];
-}
+sub record_number { goto &number; }
 
 sub type {
   my $self = shift;
 
 sub type {
   my $self = shift;
@@ -95,6 +95,9 @@ sub displayable_type {
   die 'invalid type';
 }
 
   die 'invalid type';
 }
 
+sub displayable_name {
+  join ' ', grep $_, map $_[0]->$_, qw(displayable_type record_number);
+};
 
 sub is_sales {
   croak 'not an accessor' if @_ > 1;
 
 sub is_sales {
   croak 'not an accessor' if @_ > 1;
@@ -154,25 +157,131 @@ sub convert_to_invoice {
 sub convert_to_delivery_order {
   my ($self, @args) = @_;
 
 sub convert_to_delivery_order {
   my ($self, @args) = @_;
 
-  my ($delivery_order, $custom_shipto);
+  my $delivery_order;
   if (!$self->db->with_transaction(sub {
     require SL::DB::DeliveryOrder;
   if (!$self->db->with_transaction(sub {
     require SL::DB::DeliveryOrder;
-    ($delivery_order, $custom_shipto) = SL::DB::DeliveryOrder->new_from($self, @args);
+    $delivery_order = SL::DB::DeliveryOrder->new_from($self, @args);
     $delivery_order->save;
     $delivery_order->save;
-    $custom_shipto->save if $custom_shipto;
     $self->link_to_record($delivery_order);
     $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);
     1;
   })) {
     $self->update_attributes(delivered => 1);
     1;
   })) {
-    return wantarray ? () : undef;
+    return undef;
   }
 
   }
 
-  return wantarray ? ($delivery_order, $custom_shipto) : $delivery_order;
+  return $delivery_order;
+}
+
+sub _clone_orderitem_cvar {
+  my ($cvar) = @_;
+
+  my $cloned = $_->clone_and_reset;
+  $cloned->sub_module('orderitems');
+
+  return $cloned;
+}
+
+sub new_from {
+  my ($class, $source, %params) = @_;
+
+  croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) eq 'SL::DB::Order';
+  croak("A destination type must be given parameter")            unless $params{destination_type};
+
+  my $destination_type  = delete $params{destination_type};
+  my $src_dst_allowed   = ('sales_quotation'   eq $source->type && 'sales_order'       eq $destination_type)
+                       || ('request_quotation' eq $source->type && 'purchase_order'    eq $destination_type)
+                       || ('sales_quotation'   eq $source->type && 'sales_quotation'   eq $destination_type)
+                       || ('sales_order'       eq $source->type && 'sales_order'       eq $destination_type)
+                       || ('request_quotation' eq $source->type && 'request_quotation' eq $destination_type)
+                       || ('purchase_order'    eq $source->type && 'purchase_order'    eq $destination_type);
+  croak("Cannot convert from '" . $source->type . "' to '" . $destination_type . "'") unless $src_dst_allowed;
+
+  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 %args = ( map({ ( $_ => $source->$_ ) } qw(amount cp_id currency_id cusordnumber customer_id delivery_customer_id delivery_term_id delivery_vendor_id
+                                                department_id employee_id globalproject_id intnotes marge_percent marge_total language_id netamount notes
+                                                ordnumber payment_id quonumber reqdate salesman_id shippingpoint shipvia taxincluded taxzone_id
+                                                transaction_description vendor_id
+                                             )),
+               quotation => !!($destination_type =~ m{quotation$}),
+               closed    => 0,
+               delivered => 0,
+               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 → order. Meaning order.shipto_id
+  # will not be filled in that case.
+  if (!$source->shipto_id && $source->id) {
+    $args{custom_shipto} = $source->custom_shipto->clone($class) if $source->can('custom_shipto') && $source->custom_shipto;
+
+  } else {
+    $args{shipto_id} = $source->shipto_id;
+  }
+
+  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
+                                                     )),
+                                                 custom_variables => \@custom_variables,
+    );
+    $current_oe_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
+    $current_oe_item;
+  } @{ $items };
+
+  @items = grep { $params{item_filter}->($_) } @items if $params{item_filter};
+  @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
+  @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
+
+  $order->items(\@items);
+
+  return $order;
 }
 
 sub number {
   my $self = shift;
 
 }
 
 sub number {
   my $self = shift;
 
+  return if !$self->type;
+
   my %number_method = (
     sales_order       => 'ordnumber',
     sales_quotation   => 'quonumber',
   my %number_method = (
     sales_order       => 'ordnumber',
     sales_quotation   => 'quonumber',
@@ -191,10 +300,24 @@ sub date {
   goto &transdate;
 }
 
   goto &transdate;
 }
 
+sub digest {
+  my ($self) = @_;
+
+  sprintf "%s %s %s (%s)",
+    $self->number,
+    $self->customervendor->name,
+    $self->amount_as_number,
+    $self->date->to_kivitendo;
+}
+
 1;
 
 __END__
 
 1;
 
 __END__
 
+=pod
+
+=encoding utf8
+
 =head1 NAME
 
 SL::DB::Order - Order Datenbank Objekt.
 =head1 NAME
 
 SL::DB::Order - Order Datenbank Objekt.
@@ -232,16 +355,8 @@ C<true>, and C<$self> is saved.
 The arguments in C<%params> are passed to
 L<SL::DB::DeliveryOrder::new_from>.
 
 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.
+Returns C<undef> on failure. Otherwise the new delivery order will be
+returned.
 
 =head2 C<convert_to_invoice %params>
 
 
 =head2 C<convert_to_invoice %params>
 
@@ -258,6 +373,62 @@ nothing is created or changed in the database.
 
 At the moment only sales quotations and sales orders can be converted.
 
 
 At the moment only sales quotations and sales orders can be converted.
 
+=head2 C<new_from $source, %params>
+
+Creates a new C<SL::DB::Order> instance and copies as much
+information from C<$source> as possible. At the moment only records with the
+same destination type as the source type and sales orders from
+sales quotations and purchase orders from requests for quotations can be
+created.
+
+The C<transdate> field will be set to the current date.
+
+The conversion copies the order items as well.
+
+Returns the new order instance. The object returned is not
+saved.
+
+C<%params> can include the following options
+(C<destination_type> is mandatory):
+
+=over 4
+
+=item C<destination_type>
+
+(mandatory)
+The type of the newly created object. Can be C<sales_quotation>,
+C<sales_order>, C<purchase_quotation> or C<purchase_order> for now.
+
+=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_negative_qty>
+
+If trueish then items with a negative quantity are skipped. Items with
+a quantity of 0 are not affected by this option.
+
+=item C<skip_items_zero_qty>
+
+If trueish then items with a quantity of 0 are skipped.
+
+=item C<item_filter>
+
+An optional code reference that is called for each item with the item
+as its sole parameter. Items for which the code reference returns a
+falsish value will be 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
+
 =head2 C<create_sales_process>
 
 Creates and saves a new sales process. Can only be called for sales
 =head2 C<create_sales_process>
 
 Creates and saves a new sales process. Can only be called for sales