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 );
25 use Koha::Acquisition::Baskets;
26 use Koha::Acquisition::Funds;
27 use Koha::Acquisition::Invoices;
28 use Koha::Acquisition::Order::Claims;
30 use Koha::DateUtils qw( dt_from_string );
31 use Koha::Exceptions::Object;
35 use Koha::Number::Price;
36 use Koha::Subscriptions;
38 use base qw(Koha::Object);
42 Koha::Acquisition::Order Object class
50 Overloaded I<new> method for backwards compatibility.
55 my ( $self, $params ) = @_;
57 my $schema = Koha::Database->new->schema;
58 my @columns = $schema->source('Aqorder')->columns;
61 { map { exists $params->{$_} ? ( $_ => $params->{$_} ) : () } @columns };
62 return $self->SUPER::new($values);
67 Overloaded I<store> method for backwards compatibility.
74 my $schema = Koha::Database->new->schema;
75 # Override quantity for standing orders
76 $self->quantity(1) if ( $self->basketno && $schema->resultset('Aqbasket')->find( $self->basketno )->is_standing );
78 # if these parameters are missing, we can't continue
79 for my $key (qw( basketno quantity biblionumber budget_id )) {
80 next if $key eq 'biblionumber' && ($self->orderstatus // q{}) eq 'cancelled'; # cancelled order might have biblionumber NULL
81 croak "Cannot insert order: Mandatory parameter $key is missing"
85 if (not defined $self->{created_by}) {
86 my $userenv = C4::Context->userenv;
88 $self->created_by($userenv->{number});
92 $self->quantityreceived(0) unless $self->quantityreceived;
93 $self->entrydate(dt_from_string) unless $self->entrydate;
95 $self->ordernumber(undef) unless $self->ordernumber;
96 $self = $self->SUPER::store( $self );
98 unless ( $self->parent_ordernumber ) {
99 $self->set( { parent_ordernumber => $self->ordernumber } );
100 $self = $self->SUPER::store( $self );
112 delete_biblio => $delete_biblio
117 This method marks an order as cancelled, optionally using the I<reason> parameter.
118 As the order is cancelled, the (eventual) items linked to it are removed.
119 If I<delete_biblio> is passed, it will try to remove the linked biblio.
121 If either the items or biblio removal fails, an error message is added to the object
122 so the caller can take appropriate actions.
127 my ($self, $params) = @_;
129 my $delete_biblio = $params->{delete_biblio};
130 my $reason = $params->{reason};
132 # Delete the related items
133 my $items = $self->items;
134 while ( my $item = $items->next ) {
135 my $deleted = $item->safe_delete;
136 unless ( $deleted ) {
139 message => 'error_delitem',
140 payload => { item => $item, reason => @{$deleted->messages}[0]->message }
146 my $biblio = $self->biblio;
147 if ( $biblio and $delete_biblio ) {
150 $biblio->active_orders->search(
151 { ordernumber => { '!=' => $self->ordernumber } }
153 and $biblio->subscriptions->count == 0
154 and $biblio->items->count == 0
158 my $error = DelBiblio( $biblio->id );
161 message => 'error_delbiblio',
162 payload => { biblio => $biblio, reason => $error }
165 $self->biblionumber(undef) unless $error; # constraint cleared biblionumber in db already
171 if ( $biblio->active_orders->search(
172 { ordernumber => { '!=' => $self->ordernumber } }
174 $message = 'error_delbiblio_active_orders';
176 elsif ( $biblio->subscriptions->count > 0 ) {
177 $message = 'error_delbiblio_subscriptions';
179 else { # $biblio->items->count > 0
180 $message = 'error_delbiblio_items';
186 payload => { biblio => $biblio }
192 # Update order status
195 cancellationreason => $reason,
196 datecancellationprinted => \'NOW()',
197 orderstatus => 'cancelled',
206 $order->add_item( $itemnumber );
208 Link an item to this order.
213 my ( $self, $itemnumber ) = @_;
215 my $schema = Koha::Database->new->schema;
216 my $rs = $schema->resultset('AqordersItem');
217 $rs->create({ ordernumber => $self->ordernumber, itemnumber => $itemnumber });
222 my $basket = $order->basket;
224 Returns the I<Koha::Acquisition::Basket> object for the basket associated
231 my $basket_rs = $self->_result->basket;
232 return Koha::Acquisition::Basket->_new_from_dbic( $basket_rs );
237 my $fund = $order->fund;
239 Returns the I<Koha::Acquisition::Fund> object for the fund (aqbudgets)
240 associated to the order.
246 my $fund_rs = $self->_result->fund;
247 return Koha::Acquisition::Fund->_new_from_dbic( $fund_rs );
252 my $invoice = $order->invoice;
254 Returns the I<Koha::Acquisition::Invoice> object for the invoice associated
257 It returns B<undef> if no linked invoice is found.
263 my $invoice_rs = $self->_result->invoice;
264 return unless $invoice_rs;
265 return Koha::Acquisition::Invoice->_new_from_dbic( $invoice_rs );
270 my $subscription = $order->subscription
272 Returns the I<Koha::Subscription> object for the subscription associated
275 It returns B<undef> if no linked subscription is found.
281 my $subscription_rs = $self->_result->subscription;
282 return unless $subscription_rs;
283 return Koha::Subscription->_new_from_dbic( $subscription_rs );
286 =head3 current_item_level_holds
288 my $holds = $order->current_item_level_holds;
290 Returns the current item-level holds associated to the order. It returns a I<Koha::Holds>
295 sub current_item_level_holds {
298 my $items_rs = $self->_result->aqorders_items;
299 my @item_numbers = $items_rs->get_column('itemnumber')->all;
300 my $biblio = $self->biblio;
302 unless ( $biblio and @item_numbers ) {
303 return Koha::Holds->new->empty;
306 return $biblio->current_holds->search(
309 -in => \@item_numbers
317 my $items = $order->items
319 Returns the items associated to the order.
325 # aqorders_items is not a join table
326 # There is no FK on items (may have been deleted)
327 my $items_rs = $self->_result->aqorders_items;
328 my @itemnumbers = $items_rs->get_column( 'itemnumber' )->all;
329 return Koha::Items->search({ itemnumber => \@itemnumbers });
334 my $biblio = $order->biblio
336 Returns the bibliographic record associated to the order
342 my $biblio_rs= $self->_result->biblio;
343 return unless $biblio_rs;
344 return Koha::Biblio->_new_from_dbic( $biblio_rs );
349 my $claims = $order->claims
351 Return the claims history for this order
357 my $claims_rs = $self->_result->aqorders_claims;
358 return Koha::Acquisition::Order::Claims->_new_from_dbic( $claims_rs );
363 my $claim = $order->claim
365 Do claim for this order
371 my $claim_rs = $self->_result->create_related('aqorders_claims', {});
372 return Koha::Acquisition::Order::Claim->_new_from_dbic($claim_rs);
377 my $nb_of_claims = $order->claims_count;
379 This is the equivalent of $order->claims->count. Keeping it for retrocompatibilty.
385 return $self->claims->count;
390 my $last_claim_date = $order->claimed_date;
392 This is the equivalent of $order->claims->last->claimed_on. Keeping it for retrocompatibilty.
398 my $last_claim = $self->claims->last;
399 return unless $last_claim;
400 return $last_claim->claimed_on;
405 my $duplicated_order = $order->duplicate_to($basket, [$default_values]);
407 Duplicate an existing order and attach it to a basket. $default_values can be specified as a hashref
408 that contain default values for the different order's attributes.
409 Items will be duplicated as well but barcodes will be set to null.
414 my ( $self, $basket, $default_values ) = @_;
416 $default_values //= {};
417 Koha::Database->schema->txn_do(
419 my $order_info = $self->unblessed;
420 undef $order_info->{ordernumber};
427 datecancellationprinted
436 undef $order_info->{$field};
438 $order_info->{placed_on} = dt_from_string;
439 $order_info->{entrydate} = dt_from_string;
440 $order_info->{orderstatus} = 'new';
441 $order_info->{quantityreceived} = 0;
442 while ( my ( $field, $value ) = each %$default_values ) {
443 $order_info->{$field} = $value;
446 my $userenv = C4::Context->userenv;
447 $order_info->{created_by} = $userenv->{number};
448 $order_info->{basketno} = $basket->basketno;
450 $new_order = Koha::Acquisition::Order->new($order_info)->store;
452 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
453 my $items = $self->items;
454 while ( my ($item) = $items->next ) {
455 my $item_info = $item->unblessed;
456 undef $item_info->{itemnumber};
457 undef $item_info->{barcode};
458 my $new_item = Koha::Item->new($item_info)->store;
459 $new_order->add_item( $new_item->itemnumber );
467 =head3 populate_with_prices_for_ordering
469 Sets calculated values for an order - all values are stored with full precision
470 regardless of rounding preference except for tax value which is calculated on
471 rounded values if requested
473 $order->populate_with_prices_for_ordering()
480 tax_value_on_ordering
484 sub populate_with_prices_for_ordering {
487 my $bookseller = $self->basket->bookseller;
488 return unless $bookseller;
490 my $discount = $self->discount || 0;
491 $discount /= 100 if $discount > 1;
493 if ( $bookseller->listincgst ) {
494 # The user entered the prices tax included
495 $self->unitprice($self->unitprice + 0);
496 $self->unitprice_tax_included($self->unitprice);
497 $self->rrp_tax_included($self->rrp);
499 # price tax excluded = price tax included / ( 1 + tax rate )
500 $self->unitprice_tax_excluded( $self->unitprice_tax_included / ( 1 + $self->tax_rate_on_ordering ) );
501 $self->rrp_tax_excluded( $self->rrp_tax_included / ( 1 + $self->tax_rate_on_ordering ) );
503 # ecost tax included = rrp tax included ( 1 - discount )
504 $self->ecost_tax_included($self->rrp_tax_included * ( 1 - $discount ));
506 # ecost tax excluded = rrp tax excluded * ( 1 - discount )
507 $self->ecost_tax_excluded($self->rrp_tax_excluded * ( 1 - $discount ));
509 # tax value = quantity * ecost tax excluded * tax rate
510 # we should use the unitprice if included
511 my $cost_tax_included = $self->unitprice_tax_included == 0 ? $self->ecost_tax_included : $self->unitprice_tax_included;
512 my $cost_tax_excluded = $self->unitprice_tax_excluded == 0 ? $self->ecost_tax_excluded : $self->unitprice_tax_excluded;
513 $self->tax_value_on_ordering( ( C4::Acquisition::get_rounded_price($cost_tax_included) - C4::Acquisition::get_rounded_price($cost_tax_excluded) ) * $self->quantity );
515 # The user entered the prices tax excluded
516 $self->unitprice_tax_excluded($self->unitprice);
517 $self->rrp_tax_excluded($self->rrp);
519 # price tax included = price tax excluded * ( 1 - tax rate )
520 $self->unitprice_tax_included($self->unitprice_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
521 $self->rrp_tax_included($self->rrp_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
523 # ecost tax excluded = rrp tax excluded * ( 1 - discount )
524 $self->ecost_tax_excluded($self->rrp_tax_excluded * ( 1 - $discount ));
526 # ecost tax included = rrp tax excluded * ( 1 + tax rate ) * ( 1 - discount ) = ecost tax excluded * ( 1 + tax rate )
527 $self->ecost_tax_included($self->ecost_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
529 # tax value = quantity * ecost tax included * tax rate
530 # we should use the unitprice if included
531 my $cost_tax_excluded = $self->unitprice_tax_excluded == 0 ? $self->ecost_tax_excluded : $self->unitprice_tax_excluded;
532 $self->tax_value_on_ordering($self->quantity * C4::Acquisition::get_rounded_price($cost_tax_excluded) * $self->tax_rate_on_ordering);
536 =head3 populate_with_prices_for_receiving
538 Sets calculated values for an order - all values are stored with full precision
539 regardless of rounding preference except for tax value which is calculated on
540 rounded values if requested
542 $order->populate_with_prices_for_receiving()
545 unitprice_tax_included
546 unitprice_tax_excluded
547 tax_value_on_receiving
549 Note: When receiving, if the rounded value of the unitprice matches the rounded
550 value of the ecost then then ecost (full precision) is used.
554 sub populate_with_prices_for_receiving {
557 my $bookseller = $self->basket->bookseller;
558 return unless $bookseller;
560 my $discount = $self->discount || 0;
561 $discount /= 100 if $discount > 1;
563 if ($bookseller->invoiceincgst) {
564 # Trick for unitprice. If the unit price rounded value is the same as the ecost rounded value
565 # we need to keep the exact ecost value
566 if ( Koha::Number::Price->new( $self->unitprice )->round == Koha::Number::Price->new( $self->ecost_tax_included )->round ) {
567 $self->unitprice($self->ecost_tax_included);
570 # The user entered the unit price tax included
571 $self->unitprice_tax_included($self->unitprice);
573 # unit price tax excluded = unit price tax included / ( 1 + tax rate )
574 $self->unitprice_tax_excluded($self->unitprice_tax_included / ( 1 + $self->tax_rate_on_receiving ));
576 # Trick for unitprice. If the unit price rounded value is the same as the ecost rounded value
577 # we need to keep the exact ecost value
578 if ( Koha::Number::Price->new($self->unitprice)->round == Koha::Number::Price->new($self->ecost_tax_excluded)->round ) {
579 $self->unitprice($self->ecost_tax_excluded);
582 # The user entered the unit price tax excluded
583 $self->unitprice_tax_excluded($self->unitprice);
586 # unit price tax included = unit price tax included * ( 1 + tax rate )
587 $self->unitprice_tax_included($self->unitprice_tax_excluded * ( 1 + $self->tax_rate_on_receiving ));
590 # tax value = quantity * unit price tax excluded * tax rate
591 $self->tax_value_on_receiving($self->quantity * C4::Acquisition::get_rounded_price($self->unitprice_tax_excluded) * $self->tax_rate_on_receiving);
594 =head3 to_api_mapping
596 This method returns the mapping for representing a Koha::Acquisition::Order object
603 basketno => 'basket_id',
604 biblionumber => 'biblio_id',
605 budget_id => 'fund_id',
606 budgetdate => undef, # unused
607 cancellationreason => 'cancellation_reason',
608 claimed_date => 'last_claim_date',
609 datecancellationprinted => 'cancellation_date',
610 datereceived => 'date_received',
611 discount => 'discount_rate',
612 entrydate => 'entry_date',
613 freight => 'shipping_cost',
614 invoiceid => 'invoice_id',
615 line_item_id => undef, # EDIFACT related
616 listprice => 'list_price',
617 order_internalnote => 'internal_note',
618 order_vendornote => 'vendor_note',
619 ordernumber => 'order_id',
620 orderstatus => 'status',
621 parent_ordernumber => 'parent_order_id',
622 purchaseordernumber => undef, # obsolete
623 quantityreceived => 'quantity_received',
624 replacementprice => 'replacement_price',
625 sort1 => 'statistics_1',
626 sort1_authcat => 'statistics_1_authcat',
627 sort2 => 'statistics_2',
628 sort2_authcat => 'statistics_2_authcat',
629 subscriptionid => 'subscription_id',
630 suppliers_reference_number => undef, # EDIFACT related
631 suppliers_reference_qualifier => undef, # EDIFACT related
632 suppliers_report => undef, # EDIFACT related
633 tax_rate_bak => undef, # unused
634 tax_value_bak => undef, # unused
635 uncertainprice => 'uncertain_price',
636 unitprice => 'unit_price',
637 unitprice_tax_excluded => 'unit_price_tax_excluded',
638 unitprice_tax_included => 'unit_price_tax_included',
639 invoice_unitprice => 'invoice_unit_price',
643 =head2 Internal methods