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;
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 => {
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');
__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
# 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;
}
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);
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; }
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 {
}
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 {
sub is_sales {
croak 'not an accessor' if @_ > 1;
- return !!shift->customer_id;
+ $_[0]->type_data->properties('is_customer');
}
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;
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;
})) {
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;
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) = @_;
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
$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 };
$order->items(\@items);
+ unless ($params{no_linked_records}) {
+ $order->{ RECORD_ID() } = $source->{id};
+ $order->{ RECORD_TYPE_REF() } = ref($source);
+ }
+
return $order;
}
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;
}
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 {
$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__
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.