Bug 27003: [20.05.x] Item creation log must be done after the item is created
[koha.git] / Koha / Item.pm
1 package Koha::Item;
2
3 # Copyright ByWater Solutions 2014
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use Carp;
23 use List::MoreUtils qw(any);
24 use Data::Dumper;
25 use Try::Tiny;
26
27 use Koha::Database;
28 use Koha::DateUtils qw( dt_from_string );
29
30 use C4::Context;
31 use C4::Circulation;
32 use C4::Reserves;
33 use C4::ClassSource; # FIXME We would like to avoid that
34 use C4::Log qw( logaction );
35
36 use Koha::Checkouts;
37 use Koha::CirculationRules;
38 use Koha::SearchEngine::Indexer;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
41 use Koha::Patrons;
42 use Koha::Plugins;
43 use Koha::Libraries;
44 use Koha::StockRotationItem;
45 use Koha::StockRotationRotas;
46
47 use base qw(Koha::Object);
48
49 =head1 NAME
50
51 Koha::Item - Koha Item object class
52
53 =head1 API
54
55 =head2 Class methods
56
57 =cut
58
59 =head3 store
60
61     $item->store;
62
63 $params can take an optional 'skip_record_index' parameter.
64 If set, the reindexation process will not happen (index_records not called)
65
66 NOTE: This is a temporary fix to answer a performance issue when lot of items
67 are added (or modified) at the same time.
68 The correct way to fix this is to make the ES reindexation process async.
69 You should not turn it on if you do not understand what it is doing exactly.
70
71 =cut
72
73 sub store {
74     my $self = shift;
75     my $params = @_ ? shift : {};
76
77     my $log_action = $params->{log_action} // 1;
78
79     # We do not want to oblige callers to pass this value
80     # Dev conveniences vs performance?
81     unless ( $self->biblioitemnumber ) {
82         $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
83     }
84
85     # See related changes from C4::Items::AddItem
86     unless ( $self->itype ) {
87         $self->itype($self->biblio->biblioitem->itemtype);
88     }
89
90     my $today  = dt_from_string;
91     my $action = 'create';
92
93     unless ( $self->in_storage ) { #AddItem
94         unless ( $self->permanent_location ) {
95             $self->permanent_location($self->location);
96         }
97         unless ( $self->replacementpricedate ) {
98             $self->replacementpricedate($today);
99         }
100         unless ( $self->datelastseen ) {
101             $self->datelastseen($today);
102         }
103
104         unless ( $self->dateaccessioned ) {
105             $self->dateaccessioned($today);
106         }
107
108         if (   $self->itemcallnumber
109             or $self->cn_source )
110         {
111             my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
112             $self->cn_sort($cn_sort);
113         }
114
115     } else { # ModItem
116
117         $action = 'modify';
118
119         { # Update *_on  fields if needed
120           # Why not for AddItem as well?
121             my @fields = qw( itemlost withdrawn damaged );
122
123             # Only retrieve the item if we need to set an "on" date field
124             if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
125                 my $pre_mod_item = $self->get_from_storage;
126                 for my $field (@fields) {
127                     if (    $self->$field
128                         and not $pre_mod_item->$field )
129                     {
130                         my $field_on = "${field}_on";
131                         $self->$field_on(
132                           DateTime::Format::MySQL->format_datetime( dt_from_string() )
133                         );
134                     }
135                 }
136             }
137
138             # If the field is defined but empty, we are removing and,
139             # and thus need to clear out the 'on' field as well
140             for my $field (@fields) {
141                 if ( defined( $self->$field ) && !$self->$field ) {
142                     my $field_on = "${field}_on";
143                     $self->$field_on(undef);
144                 }
145             }
146         }
147
148         my %updated_columns = $self->_result->get_dirty_columns;
149         return $self->SUPER::store unless %updated_columns;
150
151         if (   exists $updated_columns{itemcallnumber}
152             or exists $updated_columns{cn_source} )
153         {
154             my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
155             $self->cn_sort($cn_sort);
156         }
157
158
159         if (    exists $updated_columns{location}
160             and $self->location ne 'CART'
161             and $self->location ne 'PROC'
162             and not exists $updated_columns{permanent_location} )
163         {
164             $self->permanent_location( $self->location );
165         }
166
167         $self->_after_item_action_hooks({ action => 'modify' });
168
169     }
170
171     unless ( $self->dateaccessioned ) {
172         $self->dateaccessioned($today);
173     }
174
175     my $result = $self->SUPER::store;
176     if ( $log_action && C4::Context->preference("CataloguingLog") ) {
177         $action eq 'create'
178           ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
179           : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper( $self->unblessed ) );
180     }
181     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
182     $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
183         unless $params->{skip_record_index};
184
185     return $result;
186 }
187
188 =head3 delete
189
190 =cut
191
192 sub delete {
193     my $self = shift;
194     my $params = @_ ? shift : {};
195
196     # FIXME check the item has no current issues
197     # i.e. raise the appropriate exception
198
199     my $result = $self->SUPER::delete;
200
201     my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
202     $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
203         unless $params->{skip_record_index};
204
205     $self->_after_item_action_hooks({ action => 'delete' });
206
207     logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
208       if C4::Context->preference("CataloguingLog");
209
210     return $result;
211 }
212
213 =head3 safe_delete
214
215 =cut
216
217 sub safe_delete {
218     my $self = shift;
219     my $params = @_ ? shift : {};
220
221     my $safe_to_delete = $self->safe_to_delete;
222     return $safe_to_delete unless $safe_to_delete eq '1';
223
224     $self->move_to_deleted;
225
226     return $self->delete($params);
227 }
228
229 =head3 safe_to_delete
230
231 returns 1 if the item is safe to delete,
232
233 "book_on_loan" if the item is checked out,
234
235 "not_same_branch" if the item is blocked by independent branches,
236
237 "book_reserved" if the there are holds aganst the item, or
238
239 "linked_analytics" if the item has linked analytic records.
240
241 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
242
243 =cut
244
245 sub safe_to_delete {
246     my ($self) = @_;
247
248     return "book_on_loan" if $self->checkout;
249
250     return "not_same_branch"
251       if defined C4::Context->userenv
252       and !C4::Context->IsSuperLibrarian()
253       and C4::Context->preference("IndependentBranches")
254       and ( C4::Context->userenv->{branch} ne $self->homebranch );
255
256     # check it doesn't have a waiting reserve
257     return "book_reserved"
258       if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
259
260     return "linked_analytics"
261       if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
262
263     return "last_item_for_hold"
264       if $self->biblio->items->count == 1
265       && $self->biblio->holds->search(
266           {
267               itemnumber => undef,
268           }
269         )->count;
270
271     return 1;
272 }
273
274 =head3 move_to_deleted
275
276 my $is_moved = $item->move_to_deleted;
277
278 Move an item to the deleteditems table.
279 This can be done before deleting an item, to make sure the data are not completely deleted.
280
281 =cut
282
283 sub move_to_deleted {
284     my ($self) = @_;
285     my $item_infos = $self->unblessed;
286     delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
287     return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
288 }
289
290
291 =head3 effective_itemtype
292
293 Returns the itemtype for the item based on whether item level itemtypes are set or not.
294
295 =cut
296
297 sub effective_itemtype {
298     my ( $self ) = @_;
299
300     return $self->_result()->effective_itemtype();
301 }
302
303 =head3 home_branch
304
305 =cut
306
307 sub home_branch {
308     my ($self) = @_;
309
310     $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
311
312     return $self->{_home_branch};
313 }
314
315 =head3 holding_branch
316
317 =cut
318
319 sub holding_branch {
320     my ($self) = @_;
321
322     $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
323
324     return $self->{_holding_branch};
325 }
326
327 =head3 biblio
328
329 my $biblio = $item->biblio;
330
331 Return the bibliographic record of this item
332
333 =cut
334
335 sub biblio {
336     my ( $self ) = @_;
337     my $biblio_rs = $self->_result->biblio;
338     return Koha::Biblio->_new_from_dbic( $biblio_rs );
339 }
340
341 =head3 biblioitem
342
343 my $biblioitem = $item->biblioitem;
344
345 Return the biblioitem record of this item
346
347 =cut
348
349 sub biblioitem {
350     my ( $self ) = @_;
351     my $biblioitem_rs = $self->_result->biblioitem;
352     return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
353 }
354
355 =head3 checkout
356
357 my $checkout = $item->checkout;
358
359 Return the checkout for this item
360
361 =cut
362
363 sub checkout {
364     my ( $self ) = @_;
365     my $checkout_rs = $self->_result->issue;
366     return unless $checkout_rs;
367     return Koha::Checkout->_new_from_dbic( $checkout_rs );
368 }
369
370 =head3 holds
371
372 my $holds = $item->holds();
373 my $holds = $item->holds($params);
374 my $holds = $item->holds({ found => 'W'});
375
376 Return holds attached to an item, optionally accept a hashref of params to pass to search
377
378 =cut
379
380 sub holds {
381     my ( $self,$params ) = @_;
382     my $holds_rs = $self->_result->reserves->search($params);
383     return Koha::Holds->_new_from_dbic( $holds_rs );
384 }
385
386 =head3 get_transfer
387
388 my $transfer = $item->get_transfer;
389
390 Return the transfer if the item is in transit or undef
391
392 =cut
393
394 sub get_transfer {
395     my ( $self ) = @_;
396     my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
397     return unless $transfer_rs;
398     return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
399 }
400
401 =head3 last_returned_by
402
403 Gets and sets the last borrower to return an item.
404
405 Accepts and returns Koha::Patron objects
406
407 $item->last_returned_by( $borrowernumber );
408
409 $last_returned_by = $item->last_returned_by();
410
411 =cut
412
413 sub last_returned_by {
414     my ( $self, $borrower ) = @_;
415
416     my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
417
418     if ($borrower) {
419         return $items_last_returned_by_rs->update_or_create(
420             { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
421     }
422     else {
423         unless ( $self->{_last_returned_by} ) {
424             my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
425             if ($result) {
426                 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
427             }
428         }
429
430         return $self->{_last_returned_by};
431     }
432 }
433
434 =head3 can_article_request
435
436 my $bool = $item->can_article_request( $borrower )
437
438 Returns true if item can be specifically requested
439
440 $borrower must be a Koha::Patron object
441
442 =cut
443
444 sub can_article_request {
445     my ( $self, $borrower ) = @_;
446
447     my $rule = $self->article_request_type($borrower);
448
449     return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
450     return q{};
451 }
452
453 =head3 hidden_in_opac
454
455 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
456
457 Returns true if item fields match the hidding criteria defined in $rules.
458 Returns false otherwise.
459
460 Takes HASHref that can have the following parameters:
461     OPTIONAL PARAMETERS:
462     $rules : { <field> => [ value_1, ... ], ... }
463
464 Note: $rules inherits its structure from the parsed YAML from reading
465 the I<OpacHiddenItems> system preference.
466
467 =cut
468
469 sub hidden_in_opac {
470     my ( $self, $params ) = @_;
471
472     my $rules = $params->{rules} // {};
473
474     return 1
475         if C4::Context->preference('hidelostitems') and
476            $self->itemlost > 0;
477
478     my $hidden_in_opac = 0;
479
480     foreach my $field ( keys %{$rules} ) {
481
482         if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
483             $hidden_in_opac = 1;
484             last;
485         }
486     }
487
488     return $hidden_in_opac;
489 }
490
491 =head3 can_be_transferred
492
493 $item->can_be_transferred({ to => $to_library, from => $from_library })
494 Checks if an item can be transferred to given library.
495
496 This feature is controlled by two system preferences:
497 UseBranchTransferLimits to enable / disable the feature
498 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
499                          for setting the limitations
500
501 Takes HASHref that can have the following parameters:
502     MANDATORY PARAMETERS:
503     $to   : Koha::Library
504     OPTIONAL PARAMETERS:
505     $from : Koha::Library  # if not given, item holdingbranch
506                            # will be used instead
507
508 Returns 1 if item can be transferred to $to_library, otherwise 0.
509
510 To find out whether at least one item of a Koha::Biblio can be transferred, please
511 see Koha::Biblio->can_be_transferred() instead of using this method for
512 multiple items of the same biblio.
513
514 =cut
515
516 sub can_be_transferred {
517     my ($self, $params) = @_;
518
519     my $to   = $params->{to};
520     my $from = $params->{from};
521
522     $to   = $to->branchcode;
523     $from = defined $from ? $from->branchcode : $self->holdingbranch;
524
525     return 1 if $from eq $to; # Transfer to current branch is allowed
526     return 1 unless C4::Context->preference('UseBranchTransferLimits');
527
528     my $limittype = C4::Context->preference('BranchTransferLimitsType');
529     return Koha::Item::Transfer::Limits->search({
530         toBranch => $to,
531         fromBranch => $from,
532         $limittype => $limittype eq 'itemtype'
533                         ? $self->effective_itemtype : $self->ccode
534     })->count ? 0 : 1;
535
536 }
537
538 =head3 pickup_locations
539
540 $pickup_locations = $item->pickup_locations( {patron => $patron } )
541
542 Returns possible pickup locations for this item, according to patron's home library (if patron is defined and holds are allowed only from hold groups)
543 and if item can be transferred to each pickup location.
544
545 =cut
546
547 sub pickup_locations {
548     my ($self, $params) = @_;
549
550     my $patron = $params->{patron};
551
552     my $circ_control_branch =
553       C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
554     my $branchitemrule =
555       C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
556
557     if(defined $patron) {
558         return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
559         return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
560     }
561
562     my $pickup_libraries = Koha::Libraries->search();
563     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
564         $pickup_libraries = $self->home_branch->get_hold_libraries;
565     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
566         my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
567         $pickup_libraries = $plib->get_hold_libraries;
568     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
569         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
570     } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
571         $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
572     };
573
574     return $pickup_libraries->search(
575         {
576             pickup_location => 1
577         },
578         {
579             order_by => ['branchname']
580         }
581     ) unless C4::Context->preference('UseBranchTransferLimits');
582
583     my $limittype = C4::Context->preference('BranchTransferLimitsType');
584     my ($ccode, $itype) = (undef, undef);
585     if( $limittype eq 'ccode' ){
586         $ccode = $self->ccode;
587     } else {
588         $itype = $self->itype;
589     }
590     my $limits = Koha::Item::Transfer::Limits->search(
591         {
592             fromBranch => $self->holdingbranch,
593             ccode      => $ccode,
594             itemtype   => $itype,
595         },
596         { columns => ['toBranch'] }
597     );
598
599     return $pickup_libraries->search(
600         {
601             pickup_location => 1,
602             branchcode      => {
603                 '-not_in' => $limits->_resultset->as_query
604             }
605         },
606         {
607             order_by => ['branchname']
608         }
609     );
610 }
611
612 =head3 article_request_type
613
614 my $type = $item->article_request_type( $borrower )
615
616 returns 'yes', 'no', 'bib_only', or 'item_only'
617
618 $borrower must be a Koha::Patron object
619
620 =cut
621
622 sub article_request_type {
623     my ( $self, $borrower ) = @_;
624
625     my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
626     my $branchcode =
627         $branch_control eq 'homebranch'    ? $self->homebranch
628       : $branch_control eq 'holdingbranch' ? $self->holdingbranch
629       :                                      undef;
630     my $borrowertype = $borrower->categorycode;
631     my $itemtype = $self->effective_itemtype();
632     my $rule = Koha::CirculationRules->get_effective_rule(
633         {
634             rule_name    => 'article_requests',
635             categorycode => $borrowertype,
636             itemtype     => $itemtype,
637             branchcode   => $branchcode
638         }
639     );
640
641     return q{} unless $rule;
642     return $rule->rule_value || q{}
643 }
644
645 =head3 current_holds
646
647 =cut
648
649 sub current_holds {
650     my ( $self ) = @_;
651     my $attributes = { order_by => 'priority' };
652     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
653     my $params = {
654         itemnumber => $self->itemnumber,
655         suspend => 0,
656         -or => [
657             reservedate => { '<=' => $dtf->format_date(dt_from_string) },
658             waitingdate => { '!=' => undef },
659         ],
660     };
661     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
662     return Koha::Holds->_new_from_dbic($hold_rs);
663 }
664
665 =head3 stockrotationitem
666
667   my $sritem = Koha::Item->stockrotationitem;
668
669 Returns the stock rotation item associated with the current item.
670
671 =cut
672
673 sub stockrotationitem {
674     my ( $self ) = @_;
675     my $rs = $self->_result->stockrotationitem;
676     return 0 if !$rs;
677     return Koha::StockRotationItem->_new_from_dbic( $rs );
678 }
679
680 =head3 add_to_rota
681
682   my $item = $item->add_to_rota($rota_id);
683
684 Add this item to the rota identified by $ROTA_ID, which means associating it
685 with the first stage of that rota.  Should this item already be associated
686 with a rota, then we will move it to the new rota.
687
688 =cut
689
690 sub add_to_rota {
691     my ( $self, $rota_id ) = @_;
692     Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
693     return $self;
694 }
695
696 =head3 has_pending_hold
697
698   my $is_pending_hold = $item->has_pending_hold();
699
700 This method checks the tmp_holdsqueue to see if this item has been selected for a hold, but not filled yet and returns true or false
701
702 =cut
703
704 sub has_pending_hold {
705     my ( $self ) = @_;
706     my $pending_hold = $self->_result->tmp_holdsqueues;
707     return $pending_hold->count ? 1: 0;
708 }
709
710 =head3 as_marc_field
711
712     my $mss   = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
713     my $field = $item->as_marc_field({ [ mss => $mss ] });
714
715 This method returns a MARC::Field object representing the Koha::Item object
716 with the current mappings configuration.
717
718 =cut
719
720 sub as_marc_field {
721     my ( $self, $params ) = @_;
722
723     my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
724     my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
725
726     my @subfields;
727
728     my @columns = $self->_result->result_source->columns;
729
730     foreach my $item_field ( @columns ) {
731         my $mapping = $mss->{ "items.$item_field"}[0];
732         my $tagfield    = $mapping->{tagfield};
733         my $tagsubfield = $mapping->{tagsubfield};
734         next if !$tagfield; # TODO: Should we raise an exception instead?
735                             # Feels like safe fallback is better
736
737         push @subfields, $tagsubfield => $self->$item_field
738             if defined $self->$item_field and $item_field ne '';
739     }
740
741     my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
742     push( @subfields, @{$unlinked_item_subfields} )
743         if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
744
745     my $field;
746
747     $field = MARC::Field->new(
748         "$item_tag", ' ', ' ', @subfields
749     ) if @subfields;
750
751     return $field;
752 }
753
754 =head3 renewal_branchcode
755
756 Returns the branchcode to be recorded in statistics renewal of the item
757
758 =cut
759
760 sub renewal_branchcode {
761
762     my ($self, $params ) = @_;
763
764     my $interface = C4::Context->interface;
765     my $branchcode;
766     if ( $interface eq 'opac' ){
767         my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
768         if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
769             $branchcode = 'OPACRenew';
770         }
771         elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
772             $branchcode = $self->homebranch;
773         }
774         elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
775             $branchcode = $self->checkout->patron->branchcode;
776         }
777         elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
778             $branchcode = $self->checkout->branchcode;
779         }
780         else {
781             $branchcode = "";
782         }
783     } else {
784         $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
785             ? C4::Context->userenv->{branch} : $params->{branch};
786     }
787     return $branchcode;
788 }
789
790 =head3 to_api_mapping
791
792 This method returns the mapping for representing a Koha::Item object
793 on the API.
794
795 =cut
796
797 sub to_api_mapping {
798     return {
799         itemnumber               => 'item_id',
800         biblionumber             => 'biblio_id',
801         biblioitemnumber         => undef,
802         barcode                  => 'external_id',
803         dateaccessioned          => 'acquisition_date',
804         booksellerid             => 'acquisition_source',
805         homebranch               => 'home_library_id',
806         price                    => 'purchase_price',
807         replacementprice         => 'replacement_price',
808         replacementpricedate     => 'replacement_price_date',
809         datelastborrowed         => 'last_checkout_date',
810         datelastseen             => 'last_seen_date',
811         stack                    => undef,
812         notforloan               => 'not_for_loan_status',
813         damaged                  => 'damaged_status',
814         damaged_on               => 'damaged_date',
815         itemlost                 => 'lost_status',
816         itemlost_on              => 'lost_date',
817         withdrawn                => 'withdrawn',
818         withdrawn_on             => 'withdrawn_date',
819         itemcallnumber           => 'callnumber',
820         coded_location_qualifier => 'coded_location_qualifier',
821         issues                   => 'checkouts_count',
822         renewals                 => 'renewals_count',
823         reserves                 => 'holds_count',
824         restricted               => 'restricted_status',
825         itemnotes                => 'public_notes',
826         itemnotes_nonpublic      => 'internal_notes',
827         holdingbranch            => 'holding_library_id',
828         paidfor                  => undef,
829         timestamp                => 'timestamp',
830         location                 => 'location',
831         permanent_location       => 'permanent_location',
832         onloan                   => 'checked_out_date',
833         cn_source                => 'call_number_source',
834         cn_sort                  => 'call_number_sort',
835         ccode                    => 'collection_code',
836         materials                => 'materials_notes',
837         uri                      => 'uri',
838         itype                    => 'item_type',
839         more_subfields_xml       => 'extended_subfields',
840         enumchron                => 'serial_issue_number',
841         copynumber               => 'copy_number',
842         stocknumber              => 'inventory_number',
843         new_status               => 'new_status'
844     };
845 }
846
847 =head2 Internal methods
848
849 =head3 _after_item_action_hooks
850
851 Helper method that takes care of calling all plugin hooks
852
853 =cut
854
855 sub _after_item_action_hooks {
856     my ( $self, $params ) = @_;
857
858     my $action = $params->{action};
859
860     if ( C4::Context->config("enable_plugins") ) {
861
862         my @plugins = Koha::Plugins->new->GetPlugins({
863             method => 'after_item_action',
864         });
865
866         if (@plugins) {
867
868             foreach my $plugin ( @plugins ) {
869                 try {
870                     $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
871                 }
872                 catch {
873                     warn "$_";
874                 };
875             }
876         }
877     }
878 }
879
880 =head3 _type
881
882 =cut
883
884 sub _type {
885     return 'Item';
886 }
887
888 =head1 AUTHOR
889
890 Kyle M Hall <kyle@bywatersolutions.com>
891
892 =cut
893
894 1;