8 use List::Util qw(max);
9 use List::MoreUtils qw(any);
12 use SL::DB::PurchaseBasketItem;
13 use SL::DB::MetaSetup::Order;
14 use SL::DB::Manager::Order;
15 use SL::DB::Helper::Attr;
16 use SL::DB::Helper::AttrHTML;
17 use SL::DB::Helper::AttrSorted;
18 use SL::DB::Helper::FlattenToForm;
19 use SL::DB::Helper::LinkedRecords;
20 use SL::DB::Helper::PriceTaxCalculator;
21 use SL::DB::Helper::PriceUpdater;
22 use SL::DB::Helper::TypeDataProxy;
23 use SL::DB::Helper::TransNumberGenerator;
24 use SL::DB::Helper::Payment qw(forex);
25 use SL::DB::Helper::RecordLink qw(RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
26 use SL::Helper::Flash;
27 use SL::Locale::String qw(t8);
29 use Rose::DB::Object::Helpers qw(as_tree strip);
31 use SL::DB::Order::TypeData qw(:types validate_type);
32 use SL::DB::Reclamation::TypeData qw(:types);
34 __PACKAGE__->meta->add_relationship(
36 type => 'one to many',
37 class => 'SL::DB::OrderItem',
38 column_map => { id => 'trans_id' },
40 with_objects => [ 'part' ]
43 periodic_invoices_config => {
45 class => 'SL::DB::PeriodicInvoicesConfig',
46 column_map => { id => 'oe_id' },
50 class => 'SL::DB::Shipto',
51 column_map => { id => 'trans_id' },
52 query_args => [ module => 'OE' ],
56 class => 'SL::DB::Exchangerate',
57 column_map => { currency_id => 'currency_id', transdate => 'transdate' },
60 type => 'one to many',
61 class => 'SL::DB::Note',
62 column_map => { id => 'trans_id' },
63 query_args => [ trans_module => 'oe' ],
65 with_objects => [ 'employee' ],
66 sort_by => 'notes.itime',
70 type => 'one to many',
71 class => 'SL::DB::OrderVersion',
72 column_map => { id => 'oe_id' },
76 SL::DB::Helper::Attr::make(__PACKAGE__, daily_exchangerate => 'numeric');
78 __PACKAGE__->meta->initialize;
80 __PACKAGE__->attr_html('notes');
81 __PACKAGE__->attr_sorted('items');
83 __PACKAGE__->before_save('_before_save_set_ord_quo_number');
84 __PACKAGE__->before_save('_before_save_create_new_project');
85 __PACKAGE__->before_save('_before_save_remove_empty_custom_shipto');
86 __PACKAGE__->before_save('_before_save_set_custom_shipto_module');
87 __PACKAGE__->after_save('_after_save_link_records');
88 __PACKAGE__->after_save('_after_save_close_reachable_intakes'); # uses linked records (order matters)
89 __PACKAGE__->before_save('_before_save_delete_from_purchase_basket');
93 sub _before_save_set_ord_quo_number {
96 # ordnumber is 'NOT NULL'. Therefore make sure it's always set to at
97 # least an empty string, even if we're saving a quotation.
98 $self->ordnumber('') if !$self->ordnumber;
100 $self->create_trans_number if !$self->record_number;
104 sub _before_save_create_new_project {
107 # force new project, if not set yet
108 if ($::instance_conf->get_order_always_project && !$self->globalproject_id && ($self->type eq SALES_ORDER_TYPE())) {
110 die t8("Error while creating project with project number of new order number, project number #1 already exists!", $self->ordnumber)
111 if SL::DB::Manager::Project->find_by(projectnumber => $self->ordnumber);
114 my $new_project = SL::DB::Project->new(
115 projectnumber => $self->ordnumber,
116 description => $self->customer->name,
117 customer_id => $self->customer->id,
119 project_type_id => $::instance_conf->get_project_type_id,
120 project_status_id => $::instance_conf->get_project_status_id,
123 $self->globalproject_id($new_project->id);
124 } or die t8('Could not create new project #1', $@);
130 sub _before_save_remove_empty_custom_shipto {
133 $self->custom_shipto(undef) if $self->custom_shipto && $self->custom_shipto->is_empty;
138 sub _before_save_set_custom_shipto_module {
141 $self->custom_shipto->module('OE') if $self->custom_shipto;
146 sub _after_save_link_records {
149 my @allowed_record_sources = qw(SL::DB::Reclamation SL::DB::Order SL::DB::EmailJournal);
150 my @allowed_item_sources = qw(SL::DB::ReclamationItem SL::DB::OrderItem);
152 SL::DB::Helper::RecordLink::link_records(
154 \@allowed_record_sources,
155 \@allowed_item_sources,
161 sub _after_save_close_reachable_intakes {
164 # Close reachable sales order intakes in the from-workflow if this is a sales order
165 if (SALES_ORDER_TYPE() eq $self->type) {
166 my $lr = $self->linked_records(direction => 'from', recursive => 1);
167 $lr = [grep { 'SL::DB::Order' eq ref $_ && !$_->closed && $_->is_type(SALES_ORDER_INTAKE_TYPE()) } @$lr];
169 SL::DB::Manager::Order->update_all(set => {closed => 1},
170 where => [id => [map {$_->id} @$lr]]);
177 sub _before_save_delete_from_purchase_basket {
180 my @basket_item_ids =
181 grep { defined($_) && $_ ne ''}
182 map { $_->{basket_item_id} }
184 return 1 unless scalar @basket_item_ids;
186 # check if all items are still in the basket
187 my $basket_item_count = SL::DB::Manager::PurchaseBasketItem->get_all_count(
188 where => [ id => \@basket_item_ids ]
190 if ($basket_item_count != scalar @basket_item_ids) {
191 die "Error while saving order: some items are not in the purchase basket anymore.";
194 if (scalar @basket_item_ids) {
195 SL::DB::Manager::PurchaseBasketItem->delete_all(
196 where => [ id => \@basket_item_ids]
205 sub items { goto &orderitems; }
206 sub add_items { goto &add_orderitems; }
207 sub record_number { goto &number; }
211 SL::DB::Order::TypeData::validate_type($self->record_type);
212 return $self->record_type;
216 return shift->type eq shift;
220 my $type = $_[0]->type();
221 any { $type eq $_ } (
222 SALES_ORDER_INTAKE_TYPE(),
223 SALES_QUOTATION_TYPE(),
224 REQUEST_QUOTATION_TYPE(),
225 PURCHASE_QUOTATION_INTAKE_TYPE(),
230 my $type = $_[0]->type();
231 any { $type eq $_ } (
232 SALES_ORDER_INTAKE_TYPE(),
233 PURCHASE_QUOTATION_INTAKE_TYPE(),
238 # oe doesn't have deliverydate, but it does have reqdate.
239 # But this has a different meaning for sales quotations.
240 # deliverydate can be used to determine tax if tax_point isn't set.
242 return $_[0]->reqdate if $_[0]->type ne SALES_QUOTATION_TYPE();
245 sub effective_tax_point {
248 return $self->tax_point || $self->deliverydate || $self->transdate;
251 sub displayable_type {
253 return $self->type_data->text('type');
256 sub displayable_name {
257 join ' ', grep $_, map $_[0]->$_, qw(displayable_type record_number);
261 croak 'not an accessor' if @_ > 1;
262 $_[0]->type_data->properties('is_customer');
265 sub daily_exchangerate {
266 my ($self, $val) = @_;
268 return 1 if $self->currency_id == $::instance_conf->get_currency_id;
270 my $rate = (any { $self->is_type($_) } (SALES_QUOTATION_TYPE(), SALES_ORDER_TYPE())) ? 'buy'
271 : (any { $self->is_type($_) } (REQUEST_QUOTATION_TYPE(), PURCHASE_ORDER_TYPE())) ? 'sell'
276 croak t8('exchange rate has to be positive') if $val <= 0;
277 if (!$self->exchangerate_obj) {
278 $self->exchangerate_obj(SL::DB::Exchangerate->new(
279 currency_id => $self->currency_id,
280 transdate => $self->transdate,
283 } elsif (!defined $self->exchangerate_obj->$rate) {
284 $self->exchangerate_obj->$rate($val);
286 croak t8('exchange rate already exists, no update allowed');
289 return $self->exchangerate_obj->$rate if $self->exchangerate_obj;
296 if ($self->quotation) {
299 require SL::DB::Invoice;
300 return SL::DB::Manager::Invoice->get_all(
302 ordnumber => $self->ordnumber,
303 @{ $params{query} || [] },
309 sub displayable_state {
312 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
315 sub abschlag_invoices {
316 return shift()->invoices(query => [ abschlag => 1 ]);
320 return shift()->invoices(query => [ abschlag => 0 ]);
323 sub convert_to_invoice {
324 my ($self, %params) = @_;
326 croak("Conversion to invoices is only supported for sales records") unless $self->customer_id;
329 if (!$self->db->with_transaction(sub {
330 require SL::DB::Invoice;
331 $invoice = SL::DB::Invoice->new_from($self, %params)->post || die;
332 $self->update_attributes(closed => 1);
341 sub convert_to_delivery_order {
342 my ($self, @args) = @_;
345 if (!$self->db->with_transaction(sub {
346 require SL::DB::DeliveryOrder;
347 $delivery_order = SL::DB::DeliveryOrder->new_from($self, @args);
348 $delivery_order->save;
350 $self->update_attributes(delivered => 1) unless $::instance_conf->get_shipped_qty_require_stock_out;
356 return $delivery_order;
359 sub convert_to_reclamation {
360 my ($self, %params) = @_;
361 $params{destination_type} = $self->is_sales ? SALES_RECLAMATION_TYPE()
362 : PURCHASE_RECLAMATION_TYPE();
364 require SL::DB::Reclamation;
365 my $reclamation = SL::DB::Reclamation->new_from($self, %params);
370 sub _clone_orderitem_cvar {
373 my $cloned = $_->clone_and_reset;
374 $cloned->sub_module('orderitems');
379 sub create_from_purchase_basket {
380 my ($class, $basket_item_ids, $vendor_item_ids, $vendor_id) = @_;
382 my ($vendor, $employee);
383 $vendor = SL::DB::Manager::Vendor->find_by(id => $vendor_id);
384 $employee = SL::DB::Manager::Employee->current;
386 my @orderitem_maps = (); # part, qty, orderer_id
387 if ($basket_item_ids && scalar @{ $basket_item_ids}) {
388 my $basket_items = SL::DB::Manager::PurchaseBasketItem->get_all(
389 query => [ id => $basket_item_ids ],
390 with_objects => ['part'],
392 push @orderitem_maps, map {{
393 basket_item_id => $_->id,
396 orderer_id => $_->orderer_id,
399 if ($vendor_item_ids && scalar @{ $vendor_item_ids}) {
400 my $vendor_items = SL::DB::Manager::Part->get_all(
401 query => [ id => $vendor_item_ids ] );
402 push @orderitem_maps, map {{
403 basket_item_id => undef,
405 qty => $_->order_qty || 1,
406 orderer_id => $employee->id,
410 my $order = $class->new(
411 vendor_id => $vendor->id,
412 employee_id => $employee->id,
413 intnotes => $vendor->notes,
414 salesman_id => $employee->id,
415 payment_id => $vendor->payment_id,
416 delivery_term_id => $vendor->delivery_term_id,
417 taxzone_id => $vendor->taxzone_id,
418 currency_id => $vendor->currency_id,
419 transdate => DateTime->today_local,
420 record_type => PURCHASE_ORDER_TYPE(),
425 foreach my $orderitem_map (@orderitem_maps) {
427 my $part = $orderitem_map->{part};
428 my $qty = $orderitem_map->{qty};
429 my $orderer_id = $orderitem_map->{orderer_id};
431 my $order_item = SL::DB::OrderItem->new(
435 description => $part->description,
436 price_factor_id => $part->price_factor_id,
438 $part->price_factor_id ? $part->price_factor->factor
440 orderer_id => $orderer_id,
443 $order_item->{basket_item_id} = $orderitem_map->{basket_item_id};
445 my $price_source = SL::PriceSource->new(
446 record_item => $order_item, record => $order);
447 $order_item->sellprice(
448 $price_source->best_price ? $price_source->best_price->price
450 $order_item->active_price_source(
451 $price_source->best_price ? $price_source->best_price->source
453 push @order_items, $order_item;
456 $order->assign_attributes(orderitems => \@order_items);
458 $order->calculate_prices_and_taxes;
460 foreach my $item(@{ $order->orderitems }){
461 $item->parse_custom_variable_values;
462 $item->{custom_variables} = \@{ $item->cvars_by_config };
469 my ($class, $source, %params) = @_;
471 unless (any {ref($source) eq $_} qw(
475 croak("Unsupported source object type '" . ref($source) . "'");
477 croak("A destination type must be given as parameter") unless $params{destination_type};
479 my $destination_type = delete $params{destination_type};
482 { from => SALES_QUOTATION_TYPE(), to => SALES_ORDER_TYPE(), abbr => 'sqso' },
483 { from => REQUEST_QUOTATION_TYPE(), to => PURCHASE_ORDER_TYPE(), abbr => 'rqpo' },
484 { from => SALES_QUOTATION_TYPE(), to => SALES_QUOTATION_TYPE(), abbr => 'sqsq' },
485 { from => SALES_ORDER_TYPE(), to => SALES_ORDER_TYPE(), abbr => 'soso' },
486 { from => REQUEST_QUOTATION_TYPE(), to => REQUEST_QUOTATION_TYPE(), abbr => 'rqrq' },
487 { from => PURCHASE_ORDER_TYPE(), to => PURCHASE_ORDER_TYPE(), abbr => 'popo' },
488 { from => SALES_ORDER_TYPE(), to => PURCHASE_ORDER_TYPE(), abbr => 'sopo' },
489 { from => PURCHASE_ORDER_TYPE(), to => SALES_ORDER_TYPE(), abbr => 'poso' },
490 { from => SALES_ORDER_TYPE(), to => SALES_QUOTATION_TYPE(), abbr => 'sosq' },
491 { from => PURCHASE_ORDER_TYPE(), to => REQUEST_QUOTATION_TYPE(), abbr => 'porq' },
492 { from => REQUEST_QUOTATION_TYPE(), to => SALES_QUOTATION_TYPE(), abbr => 'rqsq' },
493 { from => REQUEST_QUOTATION_TYPE(), to => SALES_ORDER_TYPE(), abbr => 'rqso' },
494 { from => SALES_QUOTATION_TYPE(), to => REQUEST_QUOTATION_TYPE(), abbr => 'sqrq' },
495 { from => SALES_ORDER_TYPE(), to => REQUEST_QUOTATION_TYPE(), abbr => 'sorq' },
496 { from => SALES_RECLAMATION_TYPE(), to => SALES_ORDER_TYPE(), abbr => 'srso' },
497 { from => PURCHASE_RECLAMATION_TYPE(), to => PURCHASE_ORDER_TYPE(), abbr => 'prpo' },
498 { from => SALES_ORDER_INTAKE_TYPE(), to => SALES_ORDER_INTAKE_TYPE(), abbr => 'soisoi' },
499 { from => SALES_ORDER_INTAKE_TYPE(), to => SALES_QUOTATION_TYPE(), abbr => 'soisq' },
500 { from => SALES_ORDER_INTAKE_TYPE(), to => REQUEST_QUOTATION_TYPE(), abbr => 'soirq' },
501 { from => SALES_ORDER_INTAKE_TYPE(), to => SALES_ORDER_TYPE(), abbr => 'soiso' },
502 { from => SALES_ORDER_INTAKE_TYPE(), to => PURCHASE_ORDER_TYPE(), abbr => 'soipo' },
503 { from => SALES_QUOTATION_TYPE(), to => SALES_ORDER_INTAKE_TYPE(), abbr => 'sqsoi' },
504 { from => PURCHASE_QUOTATION_INTAKE_TYPE(), to => PURCHASE_QUOTATION_INTAKE_TYPE(), abbr => 'pqipqi' },
505 { from => PURCHASE_QUOTATION_INTAKE_TYPE(), to => SALES_QUOTATION_TYPE(), abbr => 'pqisq' },
506 { from => PURCHASE_QUOTATION_INTAKE_TYPE(), to => SALES_ORDER_TYPE(), abbr => 'pqiso' },
507 { from => PURCHASE_QUOTATION_INTAKE_TYPE(), to => PURCHASE_ORDER_TYPE(), abbr => 'pqipo' },
508 { from => REQUEST_QUOTATION_TYPE(), to => PURCHASE_QUOTATION_INTAKE_TYPE(), abbr => 'rqpqi' },
509 { from => PURCHASE_ORDER_CONFIRMATION_TYPE(), to => PURCHASE_ORDER_CONFIRMATION_TYPE(), abbr => 'pocpoc' },
510 { from => PURCHASE_ORDER_CONFIRMATION_TYPE(), to => SALES_QUOTATION_TYPE(), abbr => 'pocsq' },
511 { from => PURCHASE_ORDER_CONFIRMATION_TYPE(), to => SALES_ORDER_TYPE(), abbr => 'pocso' },
512 { from => PURCHASE_ORDER_CONFIRMATION_TYPE(), to => PURCHASE_ORDER_TYPE(), abbr => 'pocpo' },
513 { from => PURCHASE_ORDER_TYPE(), to => PURCHASE_ORDER_CONFIRMATION_TYPE(), abbr => 'popoc' },
515 my $from_to = (grep { $_->{from} eq $source->record_type && $_->{to} eq $destination_type} @from_tos)[0];
516 croak("Cannot convert from '" . $source->record_type . "' to '" . $destination_type . "'") if !$from_to;
518 my $is_abbr_any = sub {
522 if (any { $missing_abbr = $_; !grep { $_->{abbr} eq $missing_abbr } @from_tos } @abbrs) {
523 die "no such workflow abbreviation '$missing_abbr'";
526 any { $from_to->{abbr} eq $_ } @abbrs;
530 if (ref($source) eq 'SL::DB::Order') {
531 %args = ( map({ ( $_ => $source->$_ ) } qw(amount cp_id currency_id cusordnumber customer_id delivery_customer_id delivery_term_id delivery_vendor_id
532 department_id exchangerate globalproject_id intnotes marge_percent marge_total language_id netamount notes
533 ordnumber payment_id quonumber reqdate salesman_id shippingpoint shipvia taxincluded tax_point taxzone_id
534 transaction_description vendor_id billing_address_id
538 transdate => DateTime->today_local,
539 employee => SL::DB::Manager::Employee->current,
541 # reqdate in quotation is 'offer is valid until reqdate'
542 # reqdate in order is 'will be delivered until reqdate'
543 # both dates are setable (on|off)
544 # and may have a additional interval in days (+ n days)
545 # dies if this convention will change
546 $args{reqdate} = $from_to->{to} =~ m/_quotation$/
547 ? $::instance_conf->get_reqdate_on
548 ? DateTime->today_local->next_workday(extra_days => $::instance_conf->get_reqdate_interval)->to_kivitendo
550 : $from_to->{to} =~ m/_order$/
551 ? $::instance_conf->get_deliverydate_on
552 ? DateTime->today_local->next_workday(extra_days => $::instance_conf->get_delivery_date_interval)->to_kivitendo
554 : $from_to->{to} =~ m/^sales_order_intake$/
557 : $from_to->{to} =~ m/^purchase_quotation_intake$/
559 : $from_to->{to} =~ m/^purchase_order_confirmation$/
561 : die "Wrong state for reqdate";
562 } elsif ( ref($source) eq 'SL::DB::Reclamation') {
563 %args = ( map({ ( $_ => $source->$_ ) } qw(
564 amount billing_address_id currency_id customer_id delivery_term_id department_id
565 exchangerate globalproject_id intnotes language_id netamount
566 notes payment_id reqdate salesman_id shippingpoint shipvia taxincluded
567 tax_point taxzone_id transaction_description vendor_id
569 cp_id => $source->{contact_id},
572 transdate => DateTime->today_local,
573 employee => SL::DB::Manager::Employee->current,
577 if ( $is_abbr_any->(qw(soipo sopo poso rqso soisq sosq porq rqsq sqrq soirq sorq pqisq pqiso pocsq pocso)) ) {
578 $args{ordnumber} = undef;
579 $args{quonumber} = undef;
581 if ( $is_abbr_any->(qw(soipo sopo sqrq soirq sorq)) ) {
582 $args{customer_id} = undef;
583 $args{salesman_id} = undef;
584 $args{payment_id} = undef;
585 $args{delivery_term_id} = undef;
587 if ( $is_abbr_any->(qw(poso rqsq pqisq pqiso pocsq pocso)) ) {
588 $args{vendor_id} = undef;
590 if ( $is_abbr_any->(qw(soso)) ) {
591 if ($source->periodic_invoices_config) {
592 $args{periodic_invoices_config} = $source->periodic_invoices_config->clone_and_reset;
594 if ($args{periodic_invoices_config}->active == 1) {
595 $args{periodic_invoices_config}->active(0);
596 flash_later('info', $::locale->text('Periodic invoices config set to inactive.'));
600 if ( $is_abbr_any->(qw(sqrq soirq sorq)) ) {
601 $args{cusordnumber} = undef;
603 if ( $is_abbr_any->(qw(soiso pocpoc pocpo popoc)) ) {
604 $args{ordnumber} = undef;
606 if ( $is_abbr_any->(qw(rqpqi pqisq)) ) {
607 $args{quonumber} = undef;
610 # Custom shipto addresses (the ones specific to the sales/purchase
611 # record and not to the customer/vendor) are only linked from
612 # shipto → order. Meaning order.shipto_id
613 # will not be filled in that case.
614 if (!$source->shipto_id && $source->id) {
615 $args{custom_shipto} = $source->custom_shipto->clone($class) if $source->can('custom_shipto') && $source->custom_shipto;
618 $args{shipto_id} = $source->shipto_id;
621 $args{record_type} = $destination_type;
623 my $order = $class->new(%args);
624 $order->assign_attributes(%{ $params{attributes} }) if $params{attributes};
625 my $items = delete($params{items}) || $source->items_sorted;
628 my $source_item = $_;
629 my @custom_variables = map { _clone_orderitem_cvar($_) } @{ $source_item->custom_variables };
632 if (ref($source) eq 'SL::DB::Order') {
633 $current_oe_item = SL::DB::OrderItem->new(map({ ( $_ => $source_item->$_ ) }
634 qw(active_discount_source active_price_source base_qty cusordnumber
635 description discount lastcost longdescription
636 marge_percent marge_price_factor marge_total
637 ordnumber parts_id price_factor price_factor_id pricegroup_id
638 project_id qty reqdate sellprice serialnumber ship subtotal transdate unit
639 optional recurring_billing_mode position
641 custom_variables => \@custom_variables,
643 } elsif (ref($source) eq 'SL::DB::Reclamation') {
644 $current_oe_item = SL::DB::OrderItem->new(
645 map({ ( $_ => $source_item->$_ ) } qw(
646 active_discount_source active_price_source base_qty description
647 discount lastcost longdescription parts_id price_factor
648 price_factor_id pricegroup_id project_id qty reqdate sellprice
649 serialnumber unit position
651 custom_variables => \@custom_variables,
654 if ( $is_abbr_any->(qw(soipo sopo)) ) {
655 $current_oe_item->sellprice($source_item->lastcost);
656 $current_oe_item->discount(0);
658 if ( $is_abbr_any->(qw(poso rqsq rqso pqisq pqiso pocsq pocso)) ) {
659 $current_oe_item->lastcost($source_item->sellprice);
661 unless ($params{no_linked_records}) {
662 $current_oe_item->{ RECORD_ITEM_ID() } = $source_item->{id};
663 $current_oe_item->{ RECORD_ITEM_TYPE_REF() } = ref($source_item);
668 @items = grep { $params{item_filter}->($_) } @items if $params{item_filter};
669 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
670 @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
672 $order->items(\@items);
674 unless ($params{no_linked_records}) {
675 $order->{ RECORD_ID() } = $source->{id};
676 $order->{ RECORD_TYPE_REF() } = ref($source);
683 my ($class, $sources, %params) = @_;
685 croak("Unsupported object type in sources") if any { ref($_) !~ m{SL::DB::Order} } @$sources;
686 croak("Cannot create order for purchase records") if any { !$_->is_sales } @$sources;
687 croak("Cannot create order from source records of different customers") if any { $_->customer_id != $sources->[0]->customer_id } @$sources;
689 # bb: todo: check shipto: is it enough to check the ids or do we have to compare the entries?
690 if (delete $params{check_same_shipto}) {
691 die "check same shipto address is not implemented yet";
692 die "Source records do not have the same shipto" if 1;
696 if (defined $params{sort_sources_by}) {
697 my $sort_by = delete $params{sort_sources_by};
698 if ($sources->[0]->can($sort_by)) {
699 $sources = [ sort { $a->$sort_by cmp $b->$sort_by } @$sources ];
701 die "Cannot sort source records by $sort_by";
705 # set this entries to undef that yield different information
707 foreach my $attr (qw(ordnumber transdate reqdate tax_point taxincluded shippingpoint
708 shipvia notes closed delivered reqdate quonumber
709 cusordnumber proforma transaction_description
710 order_probability expected_billing_date)) {
711 $attributes{$attr} = undef if any { ($sources->[0]->$attr//'') ne ($_->$attr//'') } @$sources;
713 foreach my $attr (qw(cp_id currency_id salesman_id department_id
714 delivery_customer_id delivery_vendor_id shipto_id
715 globalproject_id exchangerate)) {
716 $attributes{$attr} = undef if any { ($sources->[0]->$attr||0) != ($_->$attr||0) } @$sources;
719 # set this entries from customer that yield different information
720 foreach my $attr (qw(language_id taxzone_id payment_id delivery_term_id)) {
721 $attributes{$attr} = $sources->[0]->customervendor->$attr if any { ($sources->[0]->$attr||0) != ($_->$attr||0) } @$sources;
723 $attributes{intnotes} = $sources->[0]->customervendor->notes if any { ($sources->[0]->intnotes//'') ne ($_->intnotes//'') } @$sources;
725 # no periodic invoice config for new order
726 $attributes{periodic_invoices_config} = undef;
728 # set emplyee to the current one
729 $attributes{employee} = SL::DB::Manager::Employee->current;
731 # copy global ordnumber, transdate, cusordnumber into item scope
732 # unless already present there
733 foreach my $attr (qw(ordnumber transdate cusordnumber)) {
734 foreach my $src (@$sources) {
735 foreach my $item (@{ $src->items_sorted }) {
736 $item->$attr($src->$attr) if !$item->$attr;
743 push @items, @{$_->items_sorted} for @$sources;
744 # make order from first source and all items
745 my $order = $class->new_from($sources->[0],
746 destination_type => SALES_ORDER_TYPE(),
747 attributes => \%attributes,
750 $order->{RECORD_ID()} = join ' ', map { $_->id } @$sources; # link all sources
758 my $nr_key = $self->type_data->properties('nr_key');
759 return $self->$nr_key(@_);
763 $_[0]->type_data->properties('is_customer') ? $_[0]->customer : $_[0]->vendor;
773 sprintf "%s %s %s (%s)",
775 $self->customervendor->name,
776 $self->amount_as_number,
777 $self->date->to_kivitendo;
780 sub current_version_number {
789 my ($current_version_number) = SL::DBUtils::selectfirst_array_query($::form, $self->db->dbh, $query, ($self->id));
790 die "Invalid State. No version linked" unless $current_version_number;
792 return $current_version_number;
795 sub is_final_version {
798 my $order_versions_count = SL::DB::Manager::OrderVersion->get_all_count(where => [ oe_id => $self->id, final_version => 0 ]);
799 die "Invalid version state" unless $order_versions_count < 2;
800 my $final_version = $order_versions_count == 1 ? 0 : 1;
802 return $final_version;
805 sub increment_version_number {
808 die t8('This sub-version is not yet finalized') if !$self->is_final_version;
810 my $current_version_number = $self->current_version_number;
811 my $new_version_number = $current_version_number + 1;
813 my $new_number = $self->number;
814 $new_number =~ s/-$current_version_number$//;
815 $self->number($new_number . '-' . $new_version_number);
816 $self->add_order_version(SL::DB::OrderVersion->new(version => $new_version_number));
819 sub netamount_base_currency {
822 return $self->netamount unless $self->forex;
824 if ( defined $self->exchangerate ) {
825 return $self->netamount * $self->exchangerate;
827 return $self->netamount * $self->daily_exchangerate;
831 sub preceding_purchase_orders {
836 @lrs = grep { $_->record_type eq PURCHASE_ORDER_TYPE() } @{$self->linked_records(from => 'SL::DB::Order')};
838 if ('SL::DB::Order' eq $self->{RECORD_TYPE_REF()}) {
839 my $order = SL::DB::Order->load_cached($self->{RECORD_ID()});
840 push @lrs, $order if $order->record_type eq PURCHASE_ORDER_TYPE();
848 SL::DB::Helper::TypeDataProxy->new(ref $_[0], $_[0]->type);
861 SL::DB::Order - Order Datenbank Objekt.
867 Returns one of the following string types:
875 =item sales_quotation
877 =item request_quotation
881 =head2 C<is_type TYPE>
883 Returns true if the order is of the given type.
885 =head2 C<daily_exchangerate $val>
887 Gets or sets the exchangerate object's value. This is the value from the
888 table C<exchangerate> depending on the order's currency, the transdate and
889 if it is a sales or purchase order.
891 The order object (respectively the table C<oe>) has an own column
892 C<exchangerate> which can be get or set with the accessor C<exchangerate>.
894 The idea is to drop the legacy table C<exchangerate> in the future and to
895 give all relevant tables it's own C<exchangerate> column.
897 So, this method is here if you need to access the "legacy" exchangerate via
904 (optional) If given, the exchangerate in the "legacy" table is set to this
905 value, depending on currency, transdate and sales or purchase.
909 =head2 C<convert_to_delivery_order %params>
911 Creates a new delivery order with C<$self> as the basis by calling
912 L<SL::DB::DeliveryOrder::new_from>. That delivery order is saved, and
913 C<$self> is linked to the new invoice via
914 L<SL::DB::RecordLink>. C<$self>'s C<delivered> attribute is set to
915 C<true>, and C<$self> is saved.
917 The arguments in C<%params> are passed to
918 L<SL::DB::DeliveryOrder::new_from>.
920 Returns C<undef> on failure. Otherwise the new delivery order will be
923 =head2 C<convert_to_invoice %params>
925 Creates a new invoice with C<$self> as the basis by calling
926 L<SL::DB::Invoice::new_from>. That invoice is posted, and C<$self> is
927 linked to the new invoice via L<SL::DB::RecordLink>. C<$self>'s
928 C<closed> attribute is set to C<true>, and C<$self> is saved.
930 The arguments in C<%params> are passed to L<SL::DB::Invoice::new_from>.
932 Returns the new invoice instance on success and C<undef> on
933 failure. The whole process is run inside a transaction. On failure
934 nothing is created or changed in the database.
936 At the moment only sales quotations and sales orders can be converted.
938 =head2 C<new_from $source, %params>
940 Creates a new C<SL::DB::Order> instance and copies as much
941 information from C<$source> as possible. At the moment only records with the
942 same destination type as the source type and sales orders from
943 sales quotations and purchase orders from requests for quotations can be
946 The C<transdate> field will be set to the current date.
948 The conversion copies the order items as well.
950 Returns the new order instance. The object returned is not
953 C<%params> can include the following options
954 (C<destination_type> is mandatory):
958 =item C<destination_type>
961 The type of the newly created object. Can be C<sales_quotation>,
962 C<sales_order>, C<purchase_quotation> or C<purchase_order> for now.
966 An optional array reference of RDBO instances for the items to use. If
967 missing then the method C<items_sorted> will be called on
968 C<$source>. This option can be used to override the sorting, to
969 exclude certain positions or to add additional ones.
971 =item C<skip_items_negative_qty>
973 If trueish then items with a negative quantity are skipped. Items with
974 a quantity of 0 are not affected by this option.
976 =item C<skip_items_zero_qty>
978 If trueish then items with a quantity of 0 are skipped.
982 An optional code reference that is called for each item with the item
983 as its sole parameter. Items for which the code reference returns a
984 falsish value will be skipped.
988 An optional hash reference. If it exists then it is passed to C<new>
989 allowing the caller to set certain attributes for the new delivery
994 =head2 C<new_from_multi $sources, %params>
996 Creates a new C<SL::DB::Order> instance from multiple sources and copies as
997 much information from C<$sources> as possible.
998 At the moment only sales orders can be combined and they must be of the same
1001 The new order is created from the first one using C<new_from> and the positions
1002 of all orders are added to the new order. The orders can be sorted with the
1003 parameter C<sort_sources_by>.
1005 The orders attributes are kept if they contain the same information for all
1006 source orders an will be set to empty if they contain different information.
1008 Returns the new order instance. The object returned is not
1011 C<params> other then C<sort_sources_by> are passed to C<new_from>.
1013 =head2 C<increment_version_number>
1015 Checks if the current version of the order is finalized, increments
1016 the version number and adds a new order_version to the order.
1017 Dies if the version is not final.
1025 Sven Schöling <s.schoeling@linet-services.de>