1 package Koha::Acquisition::Order;
3 # This file is part of Koha.
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use C4::Biblio qw( DelBiblio );
24 use C4::Suggestions qw( ModSuggestion );
26 use Koha::Acquisition::Baskets;
27 use Koha::Acquisition::Funds;
28 use Koha::Acquisition::Invoices;
29 use Koha::Acquisition::Order::Claims;
31 use Koha::DateUtils qw( dt_from_string );
32 use Koha::Exceptions::Object;
36 use Koha::Number::Price;
38 use Koha::Subscriptions;
40 use base qw(Koha::Object Koha::Object::Mixin::AdditionalFields);
44 Koha::Acquisition::Order Object class
52 Overloaded I<new> method for backwards compatibility.
57 my ( $self, $params ) = @_;
59 my $schema = Koha::Database->new->schema;
60 my @columns = $schema->source('Aqorder')->columns;
63 { map { exists $params->{$_} ? ( $_ => $params->{$_} ) : () } @columns };
64 return $self->SUPER::new($values);
69 Overloaded I<store> method for backwards compatibility.
76 my $schema = Koha::Database->new->schema;
77 # Override quantity for standing orders
78 $self->quantity(1) if ( $self->basketno && $schema->resultset('Aqbasket')->find( $self->basketno )->is_standing );
80 # if these parameters are missing, we can't continue
81 for my $key (qw( basketno quantity biblionumber budget_id )) {
82 next if $key eq 'biblionumber' && ($self->orderstatus // q{}) eq 'cancelled'; # cancelled order might have biblionumber NULL
83 croak "Cannot insert order: Mandatory parameter $key is missing"
87 if (not defined $self->{created_by}) {
88 my $userenv = C4::Context->userenv;
90 $self->created_by($userenv->{number});
94 $self->quantityreceived(0) unless $self->quantityreceived;
95 $self->entrydate(dt_from_string) unless $self->entrydate;
97 $self->ordernumber(undef) unless $self->ordernumber;
98 $self = $self->SUPER::store( $self );
100 unless ( $self->parent_ordernumber ) {
101 $self->set( { parent_ordernumber => $self->ordernumber } );
102 $self = $self->SUPER::store( $self );
114 delete_biblio => $delete_biblio
119 This method marks an order as cancelled, optionally using the I<reason> parameter.
120 As the order is cancelled, the (eventual) items linked to it are removed.
121 If I<delete_biblio> is passed, it will try to remove the linked biblio.
123 If either the items or biblio removal fails, an error message is added to the object
124 so the caller can take appropriate actions.
129 my ($self, $params) = @_;
131 my $delete_biblio = $params->{delete_biblio};
132 my $reason = $params->{reason};
134 # Delete the related items
135 my $items = $self->items;
136 while ( my $item = $items->next ) {
137 my $deleted = $item->safe_delete;
138 unless ( $deleted ) {
141 message => 'error_delitem',
142 payload => { item => $item, reason => @{$deleted->messages}[0]->message }
148 # If ordered from a suggestion, revert the suggestion status to ACCEPTED
149 my $suggestion = Koha::Suggestions->find({ biblionumber => $self->biblionumber, status => "ORDERED" });
150 if ( $suggestion and $suggestion->id ) {
153 suggestionid => $suggestion->id,
154 biblionumber => $self->biblionumber,
155 STATUS => 'ACCEPTED',
160 my $biblio = $self->biblio;
161 if ( $biblio and $delete_biblio ) {
164 $biblio->active_orders->search(
165 { ordernumber => { '!=' => $self->ordernumber } }
167 and $biblio->subscriptions->count == 0
168 and $biblio->items->count == 0
172 my $error = DelBiblio( $biblio->id );
175 message => 'error_delbiblio',
176 payload => { biblio => $biblio, reason => $error }
179 $self->biblionumber(undef) unless $error; # constraint cleared biblionumber in db already
185 if ( $biblio->active_orders->search(
186 { ordernumber => { '!=' => $self->ordernumber } }
188 $message = 'error_delbiblio_active_orders';
190 elsif ( $biblio->subscriptions->count > 0 ) {
191 $message = 'error_delbiblio_subscriptions';
193 else { # $biblio->items->count > 0
194 $message = 'error_delbiblio_items';
200 payload => { biblio => $biblio }
206 # Update order status
209 cancellationreason => $reason,
210 datecancellationprinted => \'NOW()',
211 orderstatus => 'cancelled',
220 $order->add_item( $itemnumber );
222 Link an item to this order.
227 my ( $self, $itemnumber ) = @_;
229 my $schema = Koha::Database->new->schema;
230 my $rs = $schema->resultset('AqordersItem');
231 $rs->create({ ordernumber => $self->ordernumber, itemnumber => $itemnumber });
236 my $basket = $order->basket;
238 Returns the I<Koha::Acquisition::Basket> object for the basket associated
245 my $basket_rs = $self->_result->basket;
246 return Koha::Acquisition::Basket->_new_from_dbic( $basket_rs );
251 my $fund = $order->fund;
253 Returns the I<Koha::Acquisition::Fund> object for the fund (aqbudgets)
254 associated to the order.
260 my $fund_rs = $self->_result->fund;
261 return Koha::Acquisition::Fund->_new_from_dbic( $fund_rs );
266 my $invoice = $order->invoice;
268 Returns the I<Koha::Acquisition::Invoice> object for the invoice associated
271 It returns B<undef> if no linked invoice is found.
277 my $invoice_rs = $self->_result->invoice;
278 return unless $invoice_rs;
279 return Koha::Acquisition::Invoice->_new_from_dbic( $invoice_rs );
284 my $subscription = $order->subscription
286 Returns the I<Koha::Subscription> object for the subscription associated
289 It returns B<undef> if no linked subscription is found.
295 my $subscription_rs = $self->_result->subscription;
296 return unless $subscription_rs;
297 return Koha::Subscription->_new_from_dbic( $subscription_rs );
300 =head3 current_item_level_holds
302 my $holds = $order->current_item_level_holds;
304 Returns the current item-level holds associated to the order. It returns a I<Koha::Holds>
309 sub current_item_level_holds {
312 my $items_rs = $self->_result->aqorders_items;
313 my @item_numbers = $items_rs->get_column('itemnumber')->all;
314 my $biblio = $self->biblio;
316 unless ( $biblio and @item_numbers ) {
317 return Koha::Holds->new->empty;
320 return $biblio->current_holds->search(
323 -in => \@item_numbers
331 my $items = $order->items
333 Returns the items associated to the order.
339 # aqorders_items is not a join table
340 # There is no FK on items (may have been deleted)
341 my $items_rs = $self->_result->aqorders_items;
342 my @itemnumbers = $items_rs->get_column( 'itemnumber' )->all;
343 return Koha::Items->search({ itemnumber => \@itemnumbers });
348 my $biblio = $order->biblio
350 Returns the bibliographic record associated to the order
356 my $biblio_rs= $self->_result->biblio;
357 return unless $biblio_rs;
358 return Koha::Biblio->_new_from_dbic( $biblio_rs );
363 my $claims = $order->claims
365 Return the claims history for this order
371 my $claims_rs = $self->_result->aqorders_claims;
372 return Koha::Acquisition::Order::Claims->_new_from_dbic( $claims_rs );
377 my $claim = $order->claim
379 Do claim for this order
385 my $claim_rs = $self->_result->create_related('aqorders_claims', {});
386 return Koha::Acquisition::Order::Claim->_new_from_dbic($claim_rs);
391 my $nb_of_claims = $order->claims_count;
393 This is the equivalent of $order->claims->count. Keeping it for retrocompatibilty.
399 return $self->claims->count;
404 my $last_claim_date = $order->claimed_date;
406 This is the equivalent of $order->claims->last->claimed_on. Keeping it for retrocompatibilty.
412 my $last_claim = $self->claims->last;
413 return unless $last_claim;
414 return $last_claim->claimed_on;
419 my $creator = $order->creator;
421 Retrieves patron that created this order.
427 my $creator_rs = $self->_result->creator;
428 return unless $creator_rs;
429 return Koha::Patron->_new_from_dbic( $creator_rs );
434 my $duplicated_order = $order->duplicate_to($basket, [$default_values]);
436 Duplicate an existing order and attach it to a basket. $default_values can be specified as a hashref
437 that contain default values for the different order's attributes.
438 Items will be duplicated as well but barcodes will be set to null.
443 my ( $self, $basket, $default_values ) = @_;
445 $default_values //= {};
446 Koha::Database->schema->txn_do(
448 my $order_info = $self->unblessed;
449 undef $order_info->{ordernumber};
456 datecancellationprinted
465 undef $order_info->{$field};
467 $order_info->{placed_on} = dt_from_string;
468 $order_info->{entrydate} = dt_from_string;
469 $order_info->{orderstatus} = 'new';
470 $order_info->{quantityreceived} = 0;
471 while ( my ( $field, $value ) = each %$default_values ) {
472 $order_info->{$field} = $value;
475 my $userenv = C4::Context->userenv;
476 $order_info->{created_by} = $userenv->{number};
477 $order_info->{basketno} = $basket->basketno;
479 $new_order = Koha::Acquisition::Order->new($order_info)->store;
481 if ( ! $self->subscriptionid && $self->basket->effective_create_items eq 'ordering') { # Do copy items if not a subscription order AND if items are created on ordering
482 my $items = $self->items;
483 while ( my ($item) = $items->next ) {
484 my $item_info = $item->unblessed;
485 undef $item_info->{itemnumber};
486 undef $item_info->{barcode};
487 my $new_item = Koha::Item->new($item_info)->store;
488 $new_order->add_item( $new_item->itemnumber );
496 =head3 populate_with_prices_for_ordering
498 Sets calculated values for an order - all values are stored with full precision
499 regardless of rounding preference except for tax value which is calculated on
500 rounded values if requested
502 $order->populate_with_prices_for_ordering()
509 tax_value_on_ordering
513 sub populate_with_prices_for_ordering {
516 my $bookseller = $self->basket->bookseller;
517 return unless $bookseller;
519 my $discount = $self->discount || 0;
520 $discount /= 100 if $discount > 1;
522 if ( $bookseller->listincgst ) {
523 # The user entered the prices tax included
524 $self->unitprice($self->unitprice + 0);
525 $self->unitprice_tax_included($self->unitprice);
526 $self->rrp_tax_included($self->rrp);
528 # price tax excluded = price tax included / ( 1 + tax rate )
529 $self->unitprice_tax_excluded( $self->unitprice_tax_included / ( 1 + $self->tax_rate_on_ordering ) );
530 $self->rrp_tax_excluded( $self->rrp_tax_included / ( 1 + $self->tax_rate_on_ordering ) );
532 # ecost tax included = rrp tax included ( 1 - discount )
533 $self->ecost_tax_included($self->rrp_tax_included * ( 1 - $discount ));
535 # ecost tax excluded = rrp tax excluded * ( 1 - discount )
536 $self->ecost_tax_excluded($self->rrp_tax_excluded * ( 1 - $discount ));
538 # tax value = quantity * ecost tax excluded * tax rate
539 # we should use the unitprice if included
540 my $cost_tax_included = $self->unitprice_tax_included == 0 ? $self->ecost_tax_included : $self->unitprice_tax_included;
541 my $cost_tax_excluded = $self->unitprice_tax_excluded == 0 ? $self->ecost_tax_excluded : $self->unitprice_tax_excluded;
542 $self->tax_value_on_ordering( ( C4::Acquisition::get_rounded_price($cost_tax_included) - C4::Acquisition::get_rounded_price($cost_tax_excluded) ) * $self->quantity );
544 # The user entered the prices tax excluded
545 $self->unitprice_tax_excluded($self->unitprice);
546 $self->rrp_tax_excluded($self->rrp);
548 # price tax included = price tax excluded * ( 1 - tax rate )
549 $self->unitprice_tax_included($self->unitprice_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
550 $self->rrp_tax_included($self->rrp_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
552 # ecost tax excluded = rrp tax excluded * ( 1 - discount )
553 $self->ecost_tax_excluded($self->rrp_tax_excluded * ( 1 - $discount ));
555 # ecost tax included = rrp tax excluded * ( 1 + tax rate ) * ( 1 - discount ) = ecost tax excluded * ( 1 + tax rate )
556 $self->ecost_tax_included($self->ecost_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
558 # tax value = quantity * ecost tax included * tax rate
559 # we should use the unitprice if included
560 my $cost_tax_excluded = $self->unitprice_tax_excluded == 0 ? $self->ecost_tax_excluded : $self->unitprice_tax_excluded;
561 $self->tax_value_on_ordering($self->quantity * C4::Acquisition::get_rounded_price($cost_tax_excluded) * $self->tax_rate_on_ordering);
565 =head3 populate_with_prices_for_receiving
567 Sets calculated values for an order - all values are stored with full precision
568 regardless of rounding preference except for tax value which is calculated on
569 rounded values if requested
571 $order->populate_with_prices_for_receiving()
574 unitprice_tax_included
575 unitprice_tax_excluded
576 tax_value_on_receiving
578 Note: When receiving, if the rounded value of the unitprice matches the rounded
579 value of the ecost then then ecost (full precision) is used.
583 sub populate_with_prices_for_receiving {
586 my $bookseller = $self->basket->bookseller;
587 return unless $bookseller;
589 my $discount = $self->discount || 0;
590 $discount /= 100 if $discount > 1;
592 if ($bookseller->invoiceincgst) {
593 # Trick for unitprice. If the unit price rounded value is the same as the ecost rounded value
594 # we need to keep the exact ecost value
595 if ( Koha::Number::Price->new( $self->unitprice )->round == Koha::Number::Price->new( $self->ecost_tax_included )->round ) {
596 $self->unitprice($self->ecost_tax_included);
599 # The user entered the unit price tax included
600 $self->unitprice_tax_included($self->unitprice);
602 # unit price tax excluded = unit price tax included / ( 1 + tax rate )
603 $self->unitprice_tax_excluded($self->unitprice_tax_included / ( 1 + $self->tax_rate_on_receiving ));
605 # Trick for unitprice. If the unit price rounded value is the same as the ecost rounded value
606 # we need to keep the exact ecost value
607 if ( Koha::Number::Price->new($self->unitprice)->round == Koha::Number::Price->new($self->ecost_tax_excluded)->round ) {
608 $self->unitprice($self->ecost_tax_excluded);
611 # The user entered the unit price tax excluded
612 $self->unitprice_tax_excluded($self->unitprice);
615 # unit price tax included = unit price tax included * ( 1 + tax rate )
616 $self->unitprice_tax_included($self->unitprice_tax_excluded * ( 1 + $self->tax_rate_on_receiving ));
619 # tax value = quantity * unit price tax excluded * tax rate
620 $self->tax_value_on_receiving($self->quantity * C4::Acquisition::get_rounded_price($self->unitprice_tax_excluded) * $self->tax_rate_on_receiving);
623 =head3 to_api_mapping
625 This method returns the mapping for representing a Koha::Acquisition::Order object
632 basketno => 'basket_id',
633 biblionumber => 'biblio_id',
634 deleted_biblionumber => 'deleted_biblio_id',
635 budget_id => 'fund_id',
636 budgetdate => undef, # unused
637 cancellationreason => 'cancellation_reason',
638 claimed_date => 'last_claim_date',
639 datecancellationprinted => 'cancellation_date',
640 datereceived => 'date_received',
641 discount => 'discount_rate',
642 entrydate => 'entry_date',
643 freight => 'shipping_cost',
644 invoiceid => 'invoice_id',
645 line_item_id => undef, # EDIFACT related
646 listprice => 'list_price',
647 order_internalnote => 'internal_note',
648 order_vendornote => 'vendor_note',
649 ordernumber => 'order_id',
650 orderstatus => 'status',
651 parent_ordernumber => 'parent_order_id',
652 purchaseordernumber => undef, # obsolete
653 quantityreceived => 'quantity_received',
654 replacementprice => 'replacement_price',
655 sort1 => 'statistics_1',
656 sort1_authcat => 'statistics_1_authcat',
657 sort2 => 'statistics_2',
658 sort2_authcat => 'statistics_2_authcat',
659 subscriptionid => 'subscription_id',
660 suppliers_reference_number => undef, # EDIFACT related
661 suppliers_reference_qualifier => undef, # EDIFACT related
662 suppliers_report => undef, # EDIFACT related
663 tax_rate_bak => undef, # unused
664 tax_value_bak => undef, # unused
665 uncertainprice => 'uncertain_price',
666 unitprice => 'unit_price',
667 unitprice_tax_excluded => 'unit_price_tax_excluded',
668 unitprice_tax_included => 'unit_price_tax_included',
669 invoice_unitprice => 'invoice_unit_price',
673 =head2 Internal methods