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 Koha::Patron->_new_from_dbic( $creator_rs );
433 my $duplicated_order = $order->duplicate_to($basket, [$default_values]);
435 Duplicate an existing order and attach it to a basket. $default_values can be specified as a hashref
436 that contain default values for the different order's attributes.
437 Items will be duplicated as well but barcodes will be set to null.
442 my ( $self, $basket, $default_values ) = @_;
444 $default_values //= {};
445 Koha::Database->schema->txn_do(
447 my $order_info = $self->unblessed;
448 undef $order_info->{ordernumber};
455 datecancellationprinted
464 undef $order_info->{$field};
466 $order_info->{placed_on} = dt_from_string;
467 $order_info->{entrydate} = dt_from_string;
468 $order_info->{orderstatus} = 'new';
469 $order_info->{quantityreceived} = 0;
470 while ( my ( $field, $value ) = each %$default_values ) {
471 $order_info->{$field} = $value;
474 my $userenv = C4::Context->userenv;
475 $order_info->{created_by} = $userenv->{number};
476 $order_info->{basketno} = $basket->basketno;
478 $new_order = Koha::Acquisition::Order->new($order_info)->store;
480 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
481 my $items = $self->items;
482 while ( my ($item) = $items->next ) {
483 my $item_info = $item->unblessed;
484 undef $item_info->{itemnumber};
485 undef $item_info->{barcode};
486 my $new_item = Koha::Item->new($item_info)->store;
487 $new_order->add_item( $new_item->itemnumber );
495 =head3 populate_with_prices_for_ordering
497 Sets calculated values for an order - all values are stored with full precision
498 regardless of rounding preference except for tax value which is calculated on
499 rounded values if requested
501 $order->populate_with_prices_for_ordering()
508 tax_value_on_ordering
512 sub populate_with_prices_for_ordering {
515 my $bookseller = $self->basket->bookseller;
516 return unless $bookseller;
518 my $discount = $self->discount || 0;
519 $discount /= 100 if $discount > 1;
521 if ( $bookseller->listincgst ) {
522 # The user entered the prices tax included
523 $self->unitprice($self->unitprice + 0);
524 $self->unitprice_tax_included($self->unitprice);
525 $self->rrp_tax_included($self->rrp);
527 # price tax excluded = price tax included / ( 1 + tax rate )
528 $self->unitprice_tax_excluded( $self->unitprice_tax_included / ( 1 + $self->tax_rate_on_ordering ) );
529 $self->rrp_tax_excluded( $self->rrp_tax_included / ( 1 + $self->tax_rate_on_ordering ) );
531 # ecost tax included = rrp tax included ( 1 - discount )
532 $self->ecost_tax_included($self->rrp_tax_included * ( 1 - $discount ));
534 # ecost tax excluded = rrp tax excluded * ( 1 - discount )
535 $self->ecost_tax_excluded($self->rrp_tax_excluded * ( 1 - $discount ));
537 # tax value = quantity * ecost tax excluded * tax rate
538 # we should use the unitprice if included
539 my $cost_tax_included = $self->unitprice_tax_included == 0 ? $self->ecost_tax_included : $self->unitprice_tax_included;
540 my $cost_tax_excluded = $self->unitprice_tax_excluded == 0 ? $self->ecost_tax_excluded : $self->unitprice_tax_excluded;
541 $self->tax_value_on_ordering( ( C4::Acquisition::get_rounded_price($cost_tax_included) - C4::Acquisition::get_rounded_price($cost_tax_excluded) ) * $self->quantity );
543 # The user entered the prices tax excluded
544 $self->unitprice_tax_excluded($self->unitprice);
545 $self->rrp_tax_excluded($self->rrp);
547 # price tax included = price tax excluded * ( 1 - tax rate )
548 $self->unitprice_tax_included($self->unitprice_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
549 $self->rrp_tax_included($self->rrp_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
551 # ecost tax excluded = rrp tax excluded * ( 1 - discount )
552 $self->ecost_tax_excluded($self->rrp_tax_excluded * ( 1 - $discount ));
554 # ecost tax included = rrp tax excluded * ( 1 + tax rate ) * ( 1 - discount ) = ecost tax excluded * ( 1 + tax rate )
555 $self->ecost_tax_included($self->ecost_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
557 # tax value = quantity * ecost tax included * tax rate
558 # we should use the unitprice if included
559 my $cost_tax_excluded = $self->unitprice_tax_excluded == 0 ? $self->ecost_tax_excluded : $self->unitprice_tax_excluded;
560 $self->tax_value_on_ordering($self->quantity * C4::Acquisition::get_rounded_price($cost_tax_excluded) * $self->tax_rate_on_ordering);
564 =head3 populate_with_prices_for_receiving
566 Sets calculated values for an order - all values are stored with full precision
567 regardless of rounding preference except for tax value which is calculated on
568 rounded values if requested
570 $order->populate_with_prices_for_receiving()
573 unitprice_tax_included
574 unitprice_tax_excluded
575 tax_value_on_receiving
577 Note: When receiving, if the rounded value of the unitprice matches the rounded
578 value of the ecost then then ecost (full precision) is used.
582 sub populate_with_prices_for_receiving {
585 my $bookseller = $self->basket->bookseller;
586 return unless $bookseller;
588 my $discount = $self->discount || 0;
589 $discount /= 100 if $discount > 1;
591 if ($bookseller->invoiceincgst) {
592 # Trick for unitprice. If the unit price rounded value is the same as the ecost rounded value
593 # we need to keep the exact ecost value
594 if ( Koha::Number::Price->new( $self->unitprice )->round == Koha::Number::Price->new( $self->ecost_tax_included )->round ) {
595 $self->unitprice($self->ecost_tax_included);
598 # The user entered the unit price tax included
599 $self->unitprice_tax_included($self->unitprice);
601 # unit price tax excluded = unit price tax included / ( 1 + tax rate )
602 $self->unitprice_tax_excluded($self->unitprice_tax_included / ( 1 + $self->tax_rate_on_receiving ));
604 # Trick for unitprice. If the unit price rounded value is the same as the ecost rounded value
605 # we need to keep the exact ecost value
606 if ( Koha::Number::Price->new($self->unitprice)->round == Koha::Number::Price->new($self->ecost_tax_excluded)->round ) {
607 $self->unitprice($self->ecost_tax_excluded);
610 # The user entered the unit price tax excluded
611 $self->unitprice_tax_excluded($self->unitprice);
614 # unit price tax included = unit price tax included * ( 1 + tax rate )
615 $self->unitprice_tax_included($self->unitprice_tax_excluded * ( 1 + $self->tax_rate_on_receiving ));
618 # tax value = quantity * unit price tax excluded * tax rate
619 $self->tax_value_on_receiving($self->quantity * C4::Acquisition::get_rounded_price($self->unitprice_tax_excluded) * $self->tax_rate_on_receiving);
622 =head3 to_api_mapping
624 This method returns the mapping for representing a Koha::Acquisition::Order object
631 basketno => 'basket_id',
632 biblionumber => 'biblio_id',
633 deleted_biblionumber => 'deleted_biblio_id',
634 budget_id => 'fund_id',
635 budgetdate => undef, # unused
636 cancellationreason => 'cancellation_reason',
637 claimed_date => 'last_claim_date',
638 datecancellationprinted => 'cancellation_date',
639 datereceived => 'date_received',
640 discount => 'discount_rate',
641 entrydate => 'entry_date',
642 freight => 'shipping_cost',
643 invoiceid => 'invoice_id',
644 line_item_id => undef, # EDIFACT related
645 listprice => 'list_price',
646 order_internalnote => 'internal_note',
647 order_vendornote => 'vendor_note',
648 ordernumber => 'order_id',
649 orderstatus => 'status',
650 parent_ordernumber => 'parent_order_id',
651 purchaseordernumber => undef, # obsolete
652 quantityreceived => 'quantity_received',
653 replacementprice => 'replacement_price',
654 sort1 => 'statistics_1',
655 sort1_authcat => 'statistics_1_authcat',
656 sort2 => 'statistics_2',
657 sort2_authcat => 'statistics_2_authcat',
658 subscriptionid => 'subscription_id',
659 suppliers_reference_number => undef, # EDIFACT related
660 suppliers_reference_qualifier => undef, # EDIFACT related
661 suppliers_report => undef, # EDIFACT related
662 tax_rate_bak => undef, # unused
663 tax_value_bak => undef, # unused
664 uncertainprice => 'uncertain_price',
665 unitprice => 'unit_price',
666 unitprice_tax_excluded => 'unit_price_tax_excluded',
667 unitprice_tax_included => 'unit_price_tax_included',
668 invoice_unitprice => 'invoice_unit_price',
672 =head2 Internal methods