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 croak "Cannot insert order: Mandatory parameter $key is missing"
84 if (not defined $self->{created_by}) {
85 my $userenv = C4::Context->userenv;
87 $self->created_by($userenv->{number});
91 $self->quantityreceived(0) unless $self->quantityreceived;
92 $self->entrydate(dt_from_string) unless $self->entrydate;
94 $self->ordernumber(undef) unless $self->ordernumber;
95 $self = $self->SUPER::store( $self );
97 unless ( $self->parent_ordernumber ) {
98 $self->set( { parent_ordernumber => $self->ordernumber } );
99 $self = $self->SUPER::store( $self );
108 { [ reason => $reason,
109 delete_biblio => $delete_biblio ]
113 This method marks an order as cancelled, optionally using the I<reason> parameter.
114 As the order is cancelled, the (eventual) items linked to it are removed.
115 If I<delete_biblio> is passed, it will try to remove the linked biblio.
117 If either the items or biblio removal fails, an error message is added to the object
118 so the caller can take appropriate actions.
123 my ($self, $params) = @_;
125 my $delete_biblio = $params->{delete_biblio};
126 my $reason = $params->{reason};
128 # Delete the related items
129 my $items = $self->items;
130 while ( my $item = $items->next ) {
131 my $deleted = $item->safe_delete;
132 unless ( $deleted ) {
135 message => 'error_delitem',
136 payload => { item => $item, reason => @{$deleted->messages}[0]->message }
142 my $biblio = $self->biblio;
143 if ( $biblio and $delete_biblio ) {
146 $biblio->active_orders->search(
147 { ordernumber => { '!=' => $self->ordernumber } }
149 and $biblio->subscriptions->count == 0
150 and $biblio->items->count == 0
154 my $error = DelBiblio( $biblio->id );
157 message => 'error_delbiblio',
158 payload => { biblio => $biblio, reason => $error }
166 if ( $biblio->active_orders->search(
167 { ordernumber => { '!=' => $self->ordernumber } }
169 $message = 'error_delbiblio_active_orders';
171 elsif ( $biblio->subscriptions->count > 0 ) {
172 $message = 'error_delbiblio_subscriptions';
174 else { # $biblio->items->count > 0
175 $message = 'error_delbiblio_items';
181 payload => { biblio => $biblio }
187 # Update order status
190 cancellationreason => $reason,
191 datecancellationprinted => \'NOW()',
192 orderstatus => 'cancelled',
201 $order->add_item( $itemnumber );
203 Link an item to this order.
208 my ( $self, $itemnumber ) = @_;
210 my $schema = Koha::Database->new->schema;
211 my $rs = $schema->resultset('AqordersItem');
212 $rs->create({ ordernumber => $self->ordernumber, itemnumber => $itemnumber });
217 my $basket = $order->basket;
219 Returns the I<Koha::Acquisition::Basket> object for the basket associated
226 my $basket_rs = $self->_result->basket;
227 return Koha::Acquisition::Basket->_new_from_dbic( $basket_rs );
232 my $fund = $order->fund;
234 Returns the I<Koha::Acquisition::Fund> object for the fund (aqbudgets)
235 associated to the order.
241 my $fund_rs = $self->_result->fund;
242 return Koha::Acquisition::Fund->_new_from_dbic( $fund_rs );
247 my $invoice = $order->invoice;
249 Returns the I<Koha::Acquisition::Invoice> object for the invoice associated
252 It returns B<undef> if no linked invoice is found.
258 my $invoice_rs = $self->_result->invoice;
259 return unless $invoice_rs;
260 return Koha::Acquisition::Invoice->_new_from_dbic( $invoice_rs );
265 my $subscription = $order->subscription
267 Returns the I<Koha::Subscription> object for the subscription associated
270 It returns B<undef> if no linked subscription is found.
276 my $subscription_rs = $self->_result->subscription;
277 return unless $subscription_rs;
278 return Koha::Subscription->_new_from_dbic( $subscription_rs );
281 =head3 current_item_level_holds
283 my $holds = $order->current_item_level_holds;
285 Returns the current item-level holds associated to the order. It returns a I<Koha::Holds>
290 sub current_item_level_holds {
293 my $items_rs = $self->_result->aqorders_items;
294 my @item_numbers = $items_rs->get_column('itemnumber')->all;
295 my $biblio = $self->biblio;
297 unless ( $biblio and @item_numbers ) {
298 return Koha::Holds->new->empty;
301 return $biblio->current_holds->search(
304 -in => \@item_numbers
312 my $items = $order->items
314 Returns the items associated to the order.
320 # aqorders_items is not a join table
321 # There is no FK on items (may have been deleted)
322 my $items_rs = $self->_result->aqorders_items;
323 my @itemnumbers = $items_rs->get_column( 'itemnumber' )->all;
324 return Koha::Items->search({ itemnumber => \@itemnumbers });
329 my $biblio = $order->biblio
331 Returns the bibliographic record associated to the order
337 my $biblio_rs= $self->_result->biblio;
338 return unless $biblio_rs;
339 return Koha::Biblio->_new_from_dbic( $biblio_rs );
344 my $claims = $order->claims
346 Return the claims history for this order
352 my $claims_rs = $self->_result->aqorders_claims;
353 return Koha::Acquisition::Order::Claims->_new_from_dbic( $claims_rs );
358 my $claim = $order->claim
360 Do claim for this order
366 my $claim_rs = $self->_result->create_related('aqorders_claims', {});
367 return Koha::Acquisition::Order::Claim->_new_from_dbic($claim_rs);
372 my $nb_of_claims = $order->claims_count;
374 This is the equivalent of $order->claims->count. Keeping it for retrocompatibilty.
380 return $self->claims->count;
385 my $last_claim_date = $order->claimed_date;
387 This is the equivalent of $order->claims->last->claimed_on. Keeping it for retrocompatibilty.
393 my $last_claim = $self->claims->last;
394 return unless $last_claim;
395 return $last_claim->claimed_on;
400 my $duplicated_order = $order->duplicate_to($basket, [$default_values]);
402 Duplicate an existing order and attach it to a basket. $default_values can be specified as a hashref
403 that contain default values for the different order's attributes.
404 Items will be duplicated as well but barcodes will be set to null.
409 my ( $self, $basket, $default_values ) = @_;
411 $default_values //= {};
412 Koha::Database->schema->txn_do(
414 my $order_info = $self->unblessed;
415 undef $order_info->{ordernumber};
422 datecancellationprinted
431 undef $order_info->{$field};
433 $order_info->{placed_on} = dt_from_string;
434 $order_info->{entrydate} = dt_from_string;
435 $order_info->{orderstatus} = 'new';
436 $order_info->{quantityreceived} = 0;
437 while ( my ( $field, $value ) = each %$default_values ) {
438 $order_info->{$field} = $value;
441 my $userenv = C4::Context->userenv;
442 $order_info->{created_by} = $userenv->{number};
443 $order_info->{basketno} = $basket->basketno;
445 $new_order = Koha::Acquisition::Order->new($order_info)->store;
447 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
448 my $items = $self->items;
449 while ( my ($item) = $items->next ) {
450 my $item_info = $item->unblessed;
451 undef $item_info->{itemnumber};
452 undef $item_info->{barcode};
453 my $new_item = Koha::Item->new($item_info)->store;
454 $new_order->add_item( $new_item->itemnumber );
462 =head3 populate_with_prices_for_ordering
464 Sets calculated values for an order - all values are stored with full precision
465 regardless of rounding preference except for tax value which is calculated on
466 rounded values if requested
468 $order->populate_with_prices_for_ordering()
475 tax_value_on_ordering
479 sub populate_with_prices_for_ordering {
482 my $bookseller = $self->basket->bookseller;
483 return unless $bookseller;
485 my $discount = $self->discount || 0;
486 $discount /= 100 if $discount > 1;
488 if ( $bookseller->listincgst ) {
489 # The user entered the prices tax included
490 $self->unitprice($self->unitprice + 0);
491 $self->unitprice_tax_included($self->unitprice);
492 $self->rrp_tax_included($self->rrp);
494 # price tax excluded = price tax included / ( 1 + tax rate )
495 $self->unitprice_tax_excluded( $self->unitprice_tax_included / ( 1 + $self->tax_rate_on_ordering ) );
496 $self->rrp_tax_excluded( $self->rrp_tax_included / ( 1 + $self->tax_rate_on_ordering ) );
498 # ecost tax included = rrp tax included ( 1 - discount )
499 $self->ecost_tax_included($self->rrp_tax_included * ( 1 - $discount ));
501 # ecost tax excluded = rrp tax excluded * ( 1 - discount )
502 $self->ecost_tax_excluded($self->rrp_tax_excluded * ( 1 - $discount ));
504 # tax value = quantity * ecost tax excluded * tax rate
505 # we should use the unitprice if included
506 my $cost_tax_included = $self->unitprice_tax_included == 0 ? $self->ecost_tax_included : $self->unitprice_tax_included;
507 my $cost_tax_excluded = $self->unitprice_tax_excluded == 0 ? $self->ecost_tax_excluded : $self->unitprice_tax_excluded;
508 $self->tax_value_on_ordering( ( C4::Acquisition::get_rounded_price($cost_tax_included) - C4::Acquisition::get_rounded_price($cost_tax_excluded) ) * $self->quantity );
510 # The user entered the prices tax excluded
511 $self->unitprice_tax_excluded($self->unitprice);
512 $self->rrp_tax_excluded($self->rrp);
514 # price tax included = price tax excluded * ( 1 - tax rate )
515 $self->unitprice_tax_included($self->unitprice_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
516 $self->rrp_tax_included($self->rrp_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
518 # ecost tax excluded = rrp tax excluded * ( 1 - discount )
519 $self->ecost_tax_excluded($self->rrp_tax_excluded * ( 1 - $discount ));
521 # ecost tax included = rrp tax excluded * ( 1 + tax rate ) * ( 1 - discount ) = ecost tax excluded * ( 1 + tax rate )
522 $self->ecost_tax_included($self->ecost_tax_excluded * ( 1 + $self->tax_rate_on_ordering ));
524 # tax value = quantity * ecost tax included * tax rate
525 # we should use the unitprice if included
526 my $cost_tax_excluded = $self->unitprice_tax_excluded == 0 ? $self->ecost_tax_excluded : $self->unitprice_tax_excluded;
527 $self->tax_value_on_ordering($self->quantity * C4::Acquisition::get_rounded_price($cost_tax_excluded) * $self->tax_rate_on_ordering);
531 =head3 populate_with_prices_for_receiving
533 Sets calculated values for an order - all values are stored with full precision
534 regardless of rounding preference except for tax value which is calculated on
535 rounded values if requested
537 $order->populate_with_prices_for_receiving()
540 unitprice_tax_included
541 unitprice_tax_excluded
542 tax_value_on_receiving
544 Note: When receiving, if the rounded value of the unitprice matches the rounded
545 value of the ecost then then ecost (full precision) is used.
549 sub populate_with_prices_for_receiving {
552 my $bookseller = $self->basket->bookseller;
553 return unless $bookseller;
555 my $discount = $self->discount || 0;
556 $discount /= 100 if $discount > 1;
558 if ($bookseller->invoiceincgst) {
559 # Trick for unitprice. If the unit price rounded value is the same as the ecost rounded value
560 # we need to keep the exact ecost value
561 if ( Koha::Number::Price->new( $self->unitprice )->round == Koha::Number::Price->new( $self->ecost_tax_included )->round ) {
562 $self->unitprice($self->ecost_tax_included);
565 # The user entered the unit price tax included
566 $self->unitprice_tax_included($self->unitprice);
568 # unit price tax excluded = unit price tax included / ( 1 + tax rate )
569 $self->unitprice_tax_excluded($self->unitprice_tax_included / ( 1 + $self->tax_rate_on_receiving ));
571 # Trick for unitprice. If the unit price rounded value is the same as the ecost rounded value
572 # we need to keep the exact ecost value
573 if ( Koha::Number::Price->new($self->unitprice)->round == Koha::Number::Price->new($self->ecost_tax_excluded)->round ) {
574 $self->unitprice($self->ecost_tax_excluded);
577 # The user entered the unit price tax excluded
578 $self->unitprice_tax_excluded($self->unitprice);
581 # unit price tax included = unit price tax included * ( 1 + tax rate )
582 $self->unitprice_tax_included($self->unitprice_tax_excluded * ( 1 + $self->tax_rate_on_receiving ));
585 # tax value = quantity * unit price tax excluded * tax rate
586 $self->tax_value_on_receiving($self->quantity * C4::Acquisition::get_rounded_price($self->unitprice_tax_excluded) * $self->tax_rate_on_receiving);
589 =head3 to_api_mapping
591 This method returns the mapping for representing a Koha::Acquisition::Order object
598 basketno => 'basket_id',
599 biblionumber => 'biblio_id',
600 budget_id => 'fund_id',
601 budgetdate => undef, # unused
602 cancellationreason => 'cancellation_reason',
603 claimed_date => 'last_claim_date',
604 datecancellationprinted => 'cancellation_date',
605 datereceived => 'date_received',
606 discount => 'discount_rate',
607 entrydate => 'entry_date',
608 freight => 'shipping_cost',
609 invoiceid => 'invoice_id',
610 line_item_id => undef, # EDIFACT related
611 listprice => 'list_price',
612 order_internalnote => 'internal_note',
613 order_vendornote => 'vendor_note',
614 ordernumber => 'order_id',
615 orderstatus => 'status',
616 parent_ordernumber => 'parent_order_id',
617 purchaseordernumber => undef, # obsolete
618 quantityreceived => 'quantity_received',
619 replacementprice => 'replacement_price',
620 sort1 => 'statistics_1',
621 sort1_authcat => 'statistics_1_authcat',
622 sort2 => 'statistics_2',
623 sort2_authcat => 'statistics_2_authcat',
624 subscriptionid => 'subscription_id',
625 suppliers_reference_number => undef, # EDIFACT related
626 suppliers_reference_qualifier => undef, # EDIFACT related
627 suppliers_report => undef, # EDIFACT related
628 tax_rate_bak => undef, # unused
629 tax_value_bak => undef, # unused
630 uncertainprice => 'uncertain_price',
631 unitprice => 'unit_price',
632 unitprice_tax_excluded => 'unit_price_tax_excluded',
633 unitprice_tax_included => 'unit_price_tax_included'
637 =head2 Internal methods