X-Git-Url: http://wagnertech.de/gitweb/gitweb.cgi/mfinanz.git/blobdiff_plain/ff159a4d47b9a2d10744dcfc23da2c63605c8a32..eeb5375ee7727c956cc357cc8f90b19d1bfe80b9:/SL/DB/Order.pm diff --git a/SL/DB/Order.pm b/SL/DB/Order.pm index fdaa1e8bf..07d58928d 100644 --- a/SL/DB/Order.pm +++ b/SL/DB/Order.pm @@ -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 = <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 other then C are passed to C. +=head2 C + +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.