Compare commits
95 Commits
main
...
v26.05.01-
| Author | SHA1 | Date |
|---|---|---|
|
|
dacce7d3fc | |
|
|
8f5136e243 | |
|
|
bacea9025b | |
|
|
753bc3c0da | |
|
|
e00ba8d1aa | |
|
|
e54a0d3d41 | |
|
|
b2f42dda9e | |
|
|
76a32366c5 | |
|
|
4b9ea0c3a9 | |
|
|
ddfb6349d7 | |
|
|
4df08e5129 | |
|
|
a9f2bec7a4 | |
|
|
cd8608ab4e | |
|
|
d2e7b1e8e7 | |
|
|
67ff112866 | |
|
|
10a3768991 | |
|
|
a202ff3d33 | |
|
|
946ce25c52 | |
|
|
21dd3412f6 | |
|
|
40b9e8abd5 | |
|
|
78850b4a15 | |
|
|
0a44bf4e8e | |
|
|
ea10c84e80 | |
|
|
91f2e47f49 | |
|
|
efff7c7046 | |
|
|
b9bff3256a | |
|
|
c416062ef0 | |
|
|
a8e7087ce8 | |
|
|
e07b92e48a | |
|
|
183d1cf211 | |
|
|
fda196565d | |
|
|
07325de908 | |
|
|
10fc3f3c64 | |
|
|
b9d6d16f8e | |
|
|
c28b9b86ca | |
|
|
bba0146b3d | |
|
|
7b02715f4f | |
|
|
a26800691f | |
|
|
838fa2f78b | |
|
|
b8fab422f4 | |
|
|
2f03758634 | |
|
|
2cd11ac987 | |
|
|
606450fd56 | |
|
|
c5d078eccf | |
|
|
919b5e9e93 | |
|
|
9f3cdb665c | |
|
|
1c0de7e971 | |
|
|
952e60e221 | |
|
|
e7dc04a178 | |
|
|
f6deb07bdf | |
|
|
bb7b3aa9a0 | |
|
|
f4c0819644 | |
|
|
d2df80f20d | |
|
|
6fa6228f73 | |
|
|
19bc16654a | |
|
|
889ab52a33 | |
|
|
64e4501129 | |
|
|
5baf1ce6e0 | |
|
|
4e87782636 | |
|
|
87a552d8a7 | |
|
|
bcbe93fbea | |
|
|
9122ad9dc6 | |
|
|
eca5b83960 | |
|
|
885577073c | |
|
|
33a8dc5d6f | |
|
|
2f08c5b8ae | |
|
|
35e670c886 | |
|
|
dbd51e342c | |
|
|
c64f707691 | |
|
|
f545db2179 | |
|
|
39a090affe | |
|
|
4112dde941 | |
|
|
e6d6a608c3 | |
|
|
5dcec8b803 | |
|
|
adf7b69858 | |
|
|
dd99a66a4b | |
|
|
a0611d2809 | |
|
|
71559d0909 | |
|
|
d27639707f | |
|
|
1022ad4c13 | |
|
|
78d4c42a61 | |
|
|
125416a92d | |
|
|
74cf164d32 | |
|
|
f0bb03d6e9 | |
|
|
5d12a83751 | |
|
|
7c0880a9ec | |
|
|
caa63ce8e6 | |
|
|
df42d81dbe | |
|
|
9ee36cea5a | |
|
|
4c9604d1a7 | |
|
|
855e7669d4 | |
|
|
6b45fda621 | |
|
|
a5a1eeaa5b | |
|
|
e2f5afe100 | |
|
|
b860c04197 |
2
.mailmap
2
.mailmap
|
|
@ -230,8 +230,6 @@ Lari Strand <lmstrand@gmail.com> <lmstrand@gmail.
|
|||
Larry Baerveldt <larry@bywatersolutions.com>
|
||||
Laura Escamilla <laura.escamilla@bywatersolutions.com>
|
||||
Laurence Rault <laurence.rault@biblibre.com> <laurence.rault@biblibre.com>
|
||||
Lawrence O'Regan-Lloyd <loreganlloyd@clamsnet.org> <loreganlloyd@clamsnet.org>
|
||||
Lawrence O'Regan-Lloyd <loreganlloyd@clamsnet.org> <hello@lawrenceol.dev>
|
||||
Leire Diez <leire@scanbit.net>
|
||||
Liz Rea <liz@catalyst.net.nz>
|
||||
Liz Rea <lrea@nekls.org> <liz@nekls.org>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ BEGIN {
|
|||
}
|
||||
|
||||
use C4::Context;
|
||||
use C4::Stats;
|
||||
use C4::Members;
|
||||
use Koha::Account;
|
||||
use Koha::Account::Lines;
|
||||
|
|
|
|||
|
|
@ -2418,7 +2418,6 @@ sub GetInvoices {
|
|||
my $query = qq{
|
||||
SELECT aqinvoices.invoiceid, aqinvoices.invoicenumber, aqinvoices.booksellerid, aqinvoices.shipmentdate, aqinvoices.billingdate, aqinvoices.closedate, aqinvoices.shipmentcost, aqinvoices.shipmentcost_budgetid, aqinvoices.message_id,
|
||||
aqbooksellers.name AS suppliername,
|
||||
aqbooksellers.invoiceincgst,
|
||||
COUNT(
|
||||
DISTINCT IF(
|
||||
aqorders.datereceived IS NOT NULL,
|
||||
|
|
@ -2433,20 +2432,7 @@ sub GetInvoices {
|
|||
NULL
|
||||
)
|
||||
) AS is_linked_to_subscriptions,
|
||||
SUM(aqorders.quantityreceived) AS receiveditems,
|
||||
SUM(aqorders.quantity) AS itemsexpected,
|
||||
COALESCE(
|
||||
(SELECT SUM(o.unitprice_tax_excluded * o.quantity) FROM aqorders o WHERE o.invoiceid = aqinvoices.invoiceid),
|
||||
0
|
||||
) AS total_tax_excluded,
|
||||
COALESCE(
|
||||
(SELECT SUM(o.unitprice_tax_included * o.quantity) FROM aqorders o WHERE o.invoiceid = aqinvoices.invoiceid),
|
||||
0
|
||||
) AS total_tax_included,
|
||||
COALESCE(
|
||||
(SELECT SUM(adj.adjustment) FROM aqinvoice_adjustments adj WHERE adj.invoiceid = aqinvoices.invoiceid),
|
||||
0
|
||||
) AS total_adj
|
||||
SUM(aqorders.quantityreceived) AS receiveditems
|
||||
FROM aqinvoices
|
||||
LEFT JOIN aqbooksellers ON aqbooksellers.id = aqinvoices.booksellerid
|
||||
LEFT JOIN aqorders ON aqorders.invoiceid = aqinvoices.invoiceid
|
||||
|
|
@ -2539,12 +2525,12 @@ sub GetInvoices {
|
|||
}
|
||||
|
||||
$query .=
|
||||
" GROUP BY aqinvoices.invoiceid, aqinvoices.invoicenumber, aqinvoices.booksellerid, aqinvoices.shipmentdate, aqinvoices.billingdate, aqinvoices.closedate, aqinvoices.shipmentcost, aqinvoices.shipmentcost_budgetid, aqinvoices.message_id, aqbooksellers.name, aqbooksellers.invoiceincgst";
|
||||
" GROUP BY aqinvoices.invoiceid, aqinvoices.invoicenumber, aqinvoices.booksellerid, aqinvoices.shipmentdate, aqinvoices.billingdate, aqinvoices.closedate, aqinvoices.shipmentcost, aqinvoices.shipmentcost_budgetid, aqinvoices.message_id, aqbooksellers.name";
|
||||
|
||||
if ( $args{order_by} ) {
|
||||
my ( $column, $direction ) = split / /, $args{order_by};
|
||||
if ( grep { $_ eq $column } @columns ) {
|
||||
$direction = ( uc( $direction // '' ) eq 'DESC' ) ? 'DESC' : 'ASC';
|
||||
$direction ||= 'ASC';
|
||||
$query .= " ORDER BY $column $direction";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -275,18 +275,15 @@ sub get_template_and_user {
|
|||
}
|
||||
|
||||
if ($kick_out) {
|
||||
$session = get_session("");
|
||||
$template = C4::Templates::gettemplate(
|
||||
'opac-auth.tt', 'opac',
|
||||
$in->{query}
|
||||
);
|
||||
$template->param( sessionID => $session->id );
|
||||
|
||||
$cookie = $cookie_mgr->replace_in_list(
|
||||
$cookie,
|
||||
$in->{query}->cookie(
|
||||
-name => 'CGISESSID',
|
||||
-value => $session->id,
|
||||
-value => '',
|
||||
-HttpOnly => 1,
|
||||
-secure => ( C4::Context->https_enabled() ? 1 : 0 ),
|
||||
-sameSite => 'Lax',
|
||||
|
|
|
|||
|
|
@ -620,7 +620,16 @@ sub AddAuthority {
|
|||
}
|
||||
my $date = POSIX::strftime( "%y%m%d", localtime );
|
||||
if ( !$record->field('008') ) {
|
||||
$record->insert_fields_ordered( MARC::Field->new( '008', $date . Koha::Authority->default_marc21_008 ) );
|
||||
|
||||
# Get a valid default value for field 008
|
||||
my $default_008 = C4::Context->preference('MARCAuthorityControlField008');
|
||||
if ( !$default_008 or length($default_008) < 34 ) {
|
||||
$default_008 = '|| aca||aabn | a|a d';
|
||||
} else {
|
||||
$default_008 = substr( $default_008, 0, 34 );
|
||||
}
|
||||
|
||||
$record->insert_fields_ordered( MARC::Field->new( '008', $date . $default_008 ) );
|
||||
}
|
||||
if ( !$record->field('040') ) {
|
||||
$record->insert_fields_ordered(
|
||||
|
|
@ -1111,8 +1120,7 @@ sub BuildSummary {
|
|||
};
|
||||
}
|
||||
}
|
||||
my @fields = _marc21_sort_hierarchy_alpha( $record->field('4..') );
|
||||
foreach my $field (@fields) { #See From
|
||||
foreach my $field ( $record->field('4..') ) { #See From
|
||||
my $type = 'seefrom';
|
||||
$type = ( $marc21controlrefs{ substr $field->subfield('w'), 0, 1 } || '' ) if ( $field->subfield('w') );
|
||||
if ( $type eq 'notapplicable' ) {
|
||||
|
|
@ -1135,8 +1143,7 @@ sub BuildSummary {
|
|||
};
|
||||
}
|
||||
}
|
||||
@fields = _marc21_sort_hierarchy_alpha( $record->field('5..') );
|
||||
foreach my $field (@fields) { #See Also
|
||||
foreach my $field ( $record->field('5..') ) { #See Also
|
||||
my $type = 'seealso';
|
||||
$type = ( $marc21controlrefs{ substr $field->subfield('w'), 0, 1 } || '' ) if ( $field->subfield('w') );
|
||||
if ( $type eq 'notapplicable' ) {
|
||||
|
|
@ -1268,32 +1275,6 @@ sub BuildSummary {
|
|||
return \%summary;
|
||||
}
|
||||
|
||||
=head2 _marc21_sort_hierarchy_alpha
|
||||
|
||||
my @sorted = _marc21_sort_hierarchy_alpha(@fields);
|
||||
|
||||
sorts 4xx/5xx fields by hierarchy using subfield $w (g=broader, h=narrower,
|
||||
anything else between), then alphabetically by subfield $a.
|
||||
|
||||
=cut
|
||||
|
||||
sub _marc21_sort_hierarchy_alpha {
|
||||
my @fields = @_;
|
||||
return (
|
||||
sort {
|
||||
# Sort broader (g) - no hierarchy (tric:gh) - narrower (h)
|
||||
# MARC21 $w has up to 4 positions; only position 0 encodes hierarchy.
|
||||
my $a_hier = substr( $a->subfield('w') // q{}, 0, 1 );
|
||||
$a_hier = 'gh' if $a_hier !~ /^[gh]$/;
|
||||
my $b_hier = substr( $b->subfield('w') // q{}, 0, 1 );
|
||||
$b_hier = 'gh' if $b_hier !~ /^[gh]$/;
|
||||
|
||||
# When hierarchy does not resolve, sort on $a
|
||||
$a_hier cmp $b_hier || ( ( $a->subfield('a') // q{} ) cmp( $b->subfield('a') // q{} ) );
|
||||
} @fields
|
||||
);
|
||||
}
|
||||
|
||||
=head2 GetAuthorizedHeading
|
||||
|
||||
$heading = &GetAuthorizedHeading({ record => $record, authid => $authid })
|
||||
|
|
|
|||
78
C4/Biblio.pm
78
C4/Biblio.pm
|
|
@ -99,7 +99,6 @@ use Koha::Logger;
|
|||
use Koha::Cache::Memory::Lite;
|
||||
use Koha::Caches;
|
||||
use Koha::ClassSources;
|
||||
use Koha::Authority;
|
||||
use Koha::Authority::Types;
|
||||
use Koha::Acquisition::Currencies;
|
||||
use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
|
||||
|
|
@ -302,7 +301,6 @@ sub AddBiblio {
|
|||
{
|
||||
skip_record_index => 1,
|
||||
record_source_id => $options->{record_source_id},
|
||||
( ref $options->{warnings} eq 'ARRAY' ? ( warnings => $options->{warnings} ) : () ),
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -332,14 +330,8 @@ sub AddBiblio {
|
|||
$indexer->index_records( $biblionumber, "specialUpdate", "biblioserver" );
|
||||
}
|
||||
} catch {
|
||||
my $error = $_;
|
||||
if ( ref($error) eq 'Koha::Exceptions::Metadata::Invalid' ) {
|
||||
( $biblionumber, $biblioitemnumber ) = ( undef, undef );
|
||||
$error->rethrow;
|
||||
} else {
|
||||
warn $error;
|
||||
( $biblionumber, $biblioitemnumber ) = ( undef, undef );
|
||||
}
|
||||
warn $_;
|
||||
( $biblionumber, $biblioitemnumber ) = ( undef, undef );
|
||||
};
|
||||
return ( $biblionumber, $biblioitemnumber );
|
||||
}
|
||||
|
|
@ -407,12 +399,7 @@ sub ModBiblio {
|
|||
( exists $options->{record_source_id} )
|
||||
? ( record_source_id => $options->{record_source_id} )
|
||||
: ()
|
||||
),
|
||||
(
|
||||
( ref $options->{warnings} eq 'ARRAY' )
|
||||
? ( warnings => $options->{warnings} )
|
||||
: ()
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
if ( !$record ) {
|
||||
|
|
@ -812,47 +799,10 @@ sub LinkBibHeadingsToAuthorities {
|
|||
|
||||
if ( C4::Context->preference('marcflavour') eq 'MARC21' ) {
|
||||
my $userenv = C4::Context->userenv;
|
||||
|
||||
# userenv's library could not exist because of a trick in misc/commit_file.pl
|
||||
# (see FIXME and set_userenv statement there)
|
||||
my $library =
|
||||
$userenv && $userenv->{branch} ? Koha::Libraries->find( $userenv->{branch} ) : undef;
|
||||
my $marcorgcode =
|
||||
$library ? $library->get_effective_marcorgcode : C4::Context->preference('MARCOrgCode');
|
||||
|
||||
# Bug 31925: code the new authority with the source heading's actual
|
||||
# thesaurus instead of relying on AddAuthority()'s LCSH-only default.
|
||||
# AddAuthority() only defaults 008/040 when they aren't already present
|
||||
# on the record, so pre-populating them here is sufficient - no change
|
||||
# to AddAuthority() itself is needed. Skip this when the heading's
|
||||
# thesaurus couldn't actually be identified (bug 31925 comment 30) - in
|
||||
# that case the site's own MARCAuthorityControlField008 default should
|
||||
# apply, the same as it always has, rather than being overridden. Also
|
||||
# skip it entirely when LinkerConsiderThesaurus is off (bug 31925 comment
|
||||
# 45): with thesaurus-aware matching disabled, headings from any thesaurus
|
||||
# can link to the same auto-created authority by text alone, so coding
|
||||
# that authority for whichever heading happened to create it would let its
|
||||
# thesaurus silently propagate onto unrelated bibs the next time the
|
||||
# authority is saved - only sites that opted into thesaurus-aware linking
|
||||
# (and so already search by thesaurus) should get thesaurus-aware creation.
|
||||
if ( C4::Context->preference('LinkerConsiderThesaurus')
|
||||
&& Koha::Authority->has_known_thesaurus( $heading->{thesaurus} ) )
|
||||
{
|
||||
my $date = POSIX::strftime( '%y%m%d', localtime );
|
||||
$marcrecordauth->insert_fields_ordered(
|
||||
MARC::Field->new(
|
||||
'008', $date . Koha::Authority->default_marc21_008( $heading->{thesaurus} )
|
||||
)
|
||||
);
|
||||
|
||||
my @f040_subfields = ( 'a' => $marcorgcode, 'c' => $marcorgcode );
|
||||
push @f040_subfields, ( 'f' => $heading->{thesaurus} )
|
||||
unless exists
|
||||
$Koha::Authority::MARC21_THESAURUS_TO_CONTROL_FIELD_008_11->{ $heading->{thesaurus} };
|
||||
$marcrecordauth->insert_fields_ordered(
|
||||
MARC::Field->new( '040', '', '', @f040_subfields ) );
|
||||
my $library;
|
||||
if ( $userenv && $userenv->{'branch'} ) {
|
||||
$library = Koha::Libraries->find( $userenv->{'branch'} );
|
||||
}
|
||||
|
||||
$marcrecordauth->insert_fields_ordered(
|
||||
MARC::Field->new(
|
||||
'667', '', '',
|
||||
|
|
@ -863,8 +813,10 @@ sub LinkBibHeadingsToAuthorities {
|
|||
$cite =~ s/^[\s\,]*//;
|
||||
$cite =~ s/[\s\,]*$//;
|
||||
$cite =
|
||||
C4::Context->preference('GenerateAuthorityField670') . ": ("
|
||||
. "$marcorgcode)"
|
||||
C4::Context->preference('GenerateAuthorityField670') . ": ("
|
||||
. (
|
||||
$library ? $library->get_effective_marcorgcode : C4::Context->preference('MARCOrgCode') )
|
||||
. ")"
|
||||
. $bib->subfield( '999', 'c' ) . ": "
|
||||
. $cite;
|
||||
$marcrecordauth->insert_fields_ordered( MARC::Field->new( '670', '', '', 'a' => $cite ) );
|
||||
|
|
@ -2400,7 +2352,7 @@ sub TransformHtmlToMarc {
|
|||
|
||||
# between 001 and 009 (included)
|
||||
} elsif ( $fval ne '' ) {
|
||||
$fval = StripNonXmlChars($fval);
|
||||
$fval = StripNonXmlChars($fval); #Strip out any non-XML characters like control characters
|
||||
$newfield = MARC::Field->new( $tag, $fval, );
|
||||
}
|
||||
|
||||
|
|
@ -2425,7 +2377,7 @@ sub TransformHtmlToMarc {
|
|||
if ( $fval ne '' && $newfield ) {
|
||||
$newfield->add_subfields( $fkey => $fval );
|
||||
} elsif ( $fval ne '' ) {
|
||||
$fval = StripNonXmlChars($fval);
|
||||
$fval = StripNonXmlChars($fval); #Strip out any non-XML characters like control characters
|
||||
$newfield = MARC::Field->new( $tag, $ind1, $ind2, $fkey => $fval );
|
||||
}
|
||||
$j += 2;
|
||||
|
|
@ -3078,12 +3030,6 @@ sub ModBiblioMarc {
|
|||
}
|
||||
)->store;
|
||||
|
||||
if ( ref $options->{warnings} eq 'ARRAY' ) {
|
||||
for my $msg ( grep { $_->type eq 'warning' } @{ $m_rs->object_messages } ) {
|
||||
push @{ $options->{warnings} }, { type => $msg->message, message => $msg->payload };
|
||||
}
|
||||
}
|
||||
|
||||
unless ($skip_record_index) {
|
||||
my $indexer = Koha::SearchEngine::Indexer->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
|
||||
$indexer->index_records( $biblionumber, "specialUpdate", "biblioserver" );
|
||||
|
|
|
|||
|
|
@ -155,9 +155,7 @@ sub Z3950Search {
|
|||
}
|
||||
my $xslh = Koha::XSLT::Base->new;
|
||||
|
||||
my $nremaining = $s;
|
||||
my $hits_per_page = 20; #TODO make configurable
|
||||
my $total_hits = 0;
|
||||
my $nremaining = $s;
|
||||
while ( $nremaining-- ) {
|
||||
my $k;
|
||||
my $event;
|
||||
|
|
@ -171,22 +169,18 @@ sub Z3950Search {
|
|||
my ($error) = $oConnection[$k]->error_x(); #ignores errmsg, addinfo, diagset
|
||||
if ($error) {
|
||||
if ( $error =~ m/^(10000|10007)$/ ) {
|
||||
push( @errconn, { server => $servers[$k]->{host}, errcode => $error } );
|
||||
push( @errconn, { server => $servers[$k]->{host}, error => $error } );
|
||||
}
|
||||
} else {
|
||||
my $numresults = $oResult[$k]->size();
|
||||
$total_hits += $numresults;
|
||||
my $i;
|
||||
my $res;
|
||||
if ( $numresults > 0 and $numresults >= ( ( $page - 1 ) * $hits_per_page ) ) {
|
||||
$show_next = 1 if $numresults >= ( $page * $hits_per_page );
|
||||
$total_pages = int( $numresults / $hits_per_page ) + 1
|
||||
if $total_pages < ( $numresults / $hits_per_page );
|
||||
if ( $numresults > 0 and $numresults >= ( ( $page - 1 ) * 20 ) ) {
|
||||
$show_next = 1 if $numresults >= ( $page * 20 );
|
||||
$total_pages = int( $numresults / 20 ) + 1 if $total_pages < ( $numresults / 20 );
|
||||
for (
|
||||
$i = ( $page - 1 ) * $hits_per_page ;
|
||||
$i < (
|
||||
( $numresults < ( $page * $hits_per_page ) ) ? $numresults : ( $page * $hits_per_page )
|
||||
) ;
|
||||
$i = ( $page - 1 ) * 20 ;
|
||||
$i < ( ( $numresults < ( $page * 20 ) ) ? $numresults : ( $page * 20 ) ) ;
|
||||
$i++
|
||||
)
|
||||
{
|
||||
|
|
@ -197,17 +191,14 @@ sub Z3950Search {
|
|||
$xslh
|
||||
); #ignores error in sequence numbering
|
||||
push @breeding_loop, $res if $res;
|
||||
push @errconn, { server => $servers[$k]->{servername}, errcode => $error, seq => $i + 1 }
|
||||
push @errconn, { server => $servers[$k]->{servername}, error => $error, seq => $i + 1 }
|
||||
if $error;
|
||||
} else {
|
||||
my @diags = $oConnection[$k]->error_x(); # errcode, errmsg, addinfo, diagset
|
||||
next unless $diags[0]; # Just silently ignore empty results
|
||||
push @errconn,
|
||||
{
|
||||
server => $servers[$k]->{servername},
|
||||
errcode => $diags[0],
|
||||
errmsg => $diags[1],
|
||||
seq => $i + 1,
|
||||
'server' => $servers[$k]->{servername},
|
||||
error => ( ( $oConnection[$k]->error_x() )[0] ),
|
||||
error_msg => ( ( $oConnection[$k]->error_x() )[1] ), seq => $i + 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -221,7 +212,6 @@ sub Z3950Search {
|
|||
total_pages => $total_pages,
|
||||
show_nextbutton => $show_next ? 1 : 0,
|
||||
show_prevbutton => $page != 1,
|
||||
total_hits => $total_hits,
|
||||
);
|
||||
} # while nremaining
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ BEGIN {
|
|||
GetBudgetChildren
|
||||
SetOwnerToFundHierarchy
|
||||
CheckBudgetParent
|
||||
CheckBudgetParentPerm
|
||||
|
||||
HideCols
|
||||
GetCols
|
||||
|
|
@ -135,11 +136,30 @@ sub GetCols {
|
|||
|
||||
}
|
||||
|
||||
=head2 CheckBudgetParentPerm
|
||||
|
||||
Missing POD for CheckBudgetParentPerm.
|
||||
|
||||
=cut
|
||||
|
||||
sub CheckBudgetParentPerm {
|
||||
my ( $budget, $borrower_id ) = @_;
|
||||
my $depth = $budget->{depth};
|
||||
my $parent_id = $budget->{budget_parent_id};
|
||||
while ($depth) {
|
||||
my $parent = GetBudget($parent_id);
|
||||
$parent_id = $parent->{budget_parent_id};
|
||||
if ( $parent->{budget_owner_id} == $borrower_id ) {
|
||||
return 1;
|
||||
}
|
||||
$depth--;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
=head2 AddBudgetPeriod
|
||||
|
||||
Create a BudgetPeriod period with given parameters
|
||||
|
||||
my $rs = AddBudgetPeriod({ budget_period_startdate => $start_date, budget_period_enddate => $budget_period_enddate });
|
||||
Missing POD for AddBudgetPeriod.
|
||||
|
||||
=cut
|
||||
|
||||
|
|
|
|||
381
C4/Calendar.pm
381
C4/Calendar.pm
|
|
@ -21,7 +21,6 @@ use vars qw(@EXPORT);
|
|||
|
||||
use Carp qw( croak );
|
||||
use Date::Calc qw( Today );
|
||||
use Koha::Library::Calendar;
|
||||
|
||||
use C4::Context;
|
||||
use Koha::Caches;
|
||||
|
|
@ -65,32 +64,30 @@ sub new {
|
|||
return $self;
|
||||
}
|
||||
|
||||
=head2 _init
|
||||
|
||||
Loads calendar data from the database for the given branch.
|
||||
|
||||
=cut
|
||||
|
||||
sub _init {
|
||||
my $self = shift @_;
|
||||
my $branch = shift;
|
||||
defined($branch) or die "No branchcode sent to _init"; # must test for defined here and above to allow ""
|
||||
my $dbh = C4::Context->dbh();
|
||||
|
||||
my $weekly_sth = $dbh->prepare('SELECT * FROM library_weekly_closures WHERE library_id = ?');
|
||||
$weekly_sth->execute($branch);
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $repeatable = $dbh->prepare(
|
||||
'SELECT *
|
||||
FROM repeatable_holidays
|
||||
WHERE ( branchcode = ? )
|
||||
AND (ISNULL(weekday) = ?)'
|
||||
);
|
||||
$repeatable->execute( $branch, 0 );
|
||||
my %week_days_holidays;
|
||||
while ( my $row = $weekly_sth->fetchrow_hashref ) {
|
||||
|
||||
while ( my $row = $repeatable->fetchrow_hashref ) {
|
||||
my $key = $row->{weekday};
|
||||
$week_days_holidays{$key}{title} = $row->{title};
|
||||
$week_days_holidays{$key}{description} = $row->{description};
|
||||
}
|
||||
$self->{'week_days_holidays'} = \%week_days_holidays;
|
||||
|
||||
my $repeating_sth = $dbh->prepare('SELECT * FROM library_repeating_closures WHERE library_id = ?');
|
||||
$repeating_sth->execute($branch);
|
||||
$repeatable->execute( $branch, 1 );
|
||||
my %day_month_holidays;
|
||||
while ( my $row = $repeating_sth->fetchrow_hashref ) {
|
||||
while ( my $row = $repeatable->fetchrow_hashref ) {
|
||||
my $key = $row->{month} . "/" . $row->{day};
|
||||
$day_month_holidays{$key}{title} = $row->{title};
|
||||
$day_month_holidays{$key}{description} = $row->{description};
|
||||
|
|
@ -99,26 +96,29 @@ sub _init {
|
|||
}
|
||||
$self->{'day_month_holidays'} = \%day_month_holidays;
|
||||
|
||||
my $exceptions_sth =
|
||||
$dbh->prepare('SELECT date, title, description FROM library_closure_exceptions WHERE library_id = ?');
|
||||
$exceptions_sth->execute($branch);
|
||||
my $special = $dbh->prepare(
|
||||
'SELECT day, month, year, title, description
|
||||
FROM special_holidays
|
||||
WHERE ( branchcode = ? )
|
||||
AND (isexception = ?)'
|
||||
);
|
||||
$special->execute( $branch, 1 );
|
||||
my %exception_holidays;
|
||||
while ( my $row = $exceptions_sth->fetchrow_hashref ) {
|
||||
my ( $year, $month, $day ) = map { $_ + 0 } split( /-/, $row->{date} );
|
||||
$exception_holidays{"$year/$month/$day"}{title} = $row->{title};
|
||||
$exception_holidays{"$year/$month/$day"}{description} = $row->{description};
|
||||
$exception_holidays{"$year/$month/$day"}{date} = $row->{date};
|
||||
while ( my ( $day, $month, $year, $title, $description ) = $special->fetchrow ) {
|
||||
$exception_holidays{"$year/$month/$day"}{title} = $title;
|
||||
$exception_holidays{"$year/$month/$day"}{description} = $description;
|
||||
$exception_holidays{"$year/$month/$day"}{date} =
|
||||
sprintf( ISO_DATE_FORMAT, $year, $month, $day );
|
||||
}
|
||||
$self->{'exception_holidays'} = \%exception_holidays;
|
||||
|
||||
my $single_sth = $dbh->prepare('SELECT date, title, description FROM library_single_closures WHERE library_id = ?');
|
||||
$single_sth->execute($branch);
|
||||
$special->execute( $branch, 0 );
|
||||
my %single_holidays;
|
||||
while ( my $row = $single_sth->fetchrow_hashref ) {
|
||||
my ( $year, $month, $day ) = map { $_ + 0 } split( /-/, $row->{date} );
|
||||
$single_holidays{"$year/$month/$day"}{title} = $row->{title};
|
||||
$single_holidays{"$year/$month/$day"}{description} = $row->{description};
|
||||
$single_holidays{"$year/$month/$day"}{date} = $row->{date};
|
||||
while ( my ( $day, $month, $year, $title, $description ) = $special->fetchrow ) {
|
||||
$single_holidays{"$year/$month/$day"}{title} = $title;
|
||||
$single_holidays{"$year/$month/$day"}{description} = $description;
|
||||
$single_holidays{"$year/$month/$day"}{date} =
|
||||
sprintf( ISO_DATE_FORMAT, $year, $month, $day );
|
||||
}
|
||||
$self->{'single_holidays'} = \%single_holidays;
|
||||
return $self;
|
||||
|
|
@ -202,13 +202,17 @@ C<$description> Is the description to store for the holiday formed by $year/$mon
|
|||
sub insert_week_day_holiday {
|
||||
my $self = shift @_;
|
||||
my %options = @_;
|
||||
Koha::Library::Calendar->new( branchcode => $self->{branchcode} )->add_weekly_closure(
|
||||
{
|
||||
weekday => $options{weekday}, title => $options{title}, description => $options{description},
|
||||
}
|
||||
|
||||
my $weekday = $options{weekday};
|
||||
croak "Invalid weekday $weekday" unless $weekday =~ m/^[0-6]$/;
|
||||
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $insertHoliday = $dbh->prepare(
|
||||
"insert into repeatable_holidays (branchcode,weekday,day,month,title,description) values ( ?,?,NULL,NULL,?,? )"
|
||||
);
|
||||
$self->{'week_days_holidays'}->{ $options{weekday} }{title} = $options{title};
|
||||
$self->{'week_days_holidays'}->{ $options{weekday} }{description} = $options{description};
|
||||
$insertHoliday->execute( $self->{branchcode}, $weekday, $options{title}, $options{description} );
|
||||
$self->{'week_days_holidays'}->{$weekday}{title} = $options{title};
|
||||
$self->{'week_days_holidays'}->{$weekday}{description} = $options{description};
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
|
@ -234,11 +238,14 @@ C<$description> Is the description to store for the holiday formed by $year/$mon
|
|||
sub insert_day_month_holiday {
|
||||
my $self = shift @_;
|
||||
my %options = @_;
|
||||
Koha::Library::Calendar->new( branchcode => $self->{branchcode} )->add_repeating_closure(
|
||||
{
|
||||
day => $options{day}, month => $options{month}, title => $options{title},
|
||||
description => $options{description},
|
||||
}
|
||||
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $insertHoliday = $dbh->prepare(
|
||||
"insert into repeatable_holidays (branchcode,weekday,day,month,title,description) values (?, NULL, ?, ?, ?,? )"
|
||||
);
|
||||
$insertHoliday->execute(
|
||||
$self->{branchcode}, $options{day}, $options{month}, $options{title},
|
||||
$options{description}
|
||||
);
|
||||
$self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{title} = $options{title};
|
||||
$self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{description} = $options{description};
|
||||
|
|
@ -270,23 +277,28 @@ C<$description> Is the description to store for the holiday formed by $year/$mon
|
|||
sub insert_single_holiday {
|
||||
my $self = shift @_;
|
||||
my %options = @_;
|
||||
my $date = $options{date};
|
||||
unless ($date) {
|
||||
$date = sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} );
|
||||
}
|
||||
@options{qw(year month day)} = map { $_ + 0 } ( $date =~ m/(\d+)-(\d+)-(\d+)/ )
|
||||
unless $options{day};
|
||||
@options{qw(year month day)} = ( $options{date} =~ m/(\d+)-(\d+)-(\d+)/o )
|
||||
if $options{date} && !$options{day};
|
||||
|
||||
Koha::Library::Calendar->new( branchcode => $self->{branchcode} )->add_single_closure(
|
||||
{
|
||||
date => $date, title => $options{title}, description => $options{description},
|
||||
}
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $isexception = 0;
|
||||
my $insertHoliday = $dbh->prepare(
|
||||
"insert into special_holidays (branchcode,day,month,year,isexception,title,description) values (?,?,?,?,?,?,?)"
|
||||
);
|
||||
$insertHoliday->execute(
|
||||
$self->{branchcode}, $options{day}, $options{month}, $options{year}, $isexception,
|
||||
$options{title}, $options{description}
|
||||
);
|
||||
|
||||
$self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"}{title} = $options{title};
|
||||
$self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"}{description} = $options{description};
|
||||
|
||||
# changed the 'single_holidays' table, lets force/reset its cache
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $key = $self->{branchcode} . "_holidays";
|
||||
$cache->clear_from_cache($key);
|
||||
|
||||
return $self;
|
||||
|
||||
}
|
||||
|
||||
=head2 insert_exception_holiday
|
||||
|
|
@ -314,23 +326,28 @@ C<$description> Is the description to store for the holiday formed by $year/$mon
|
|||
sub insert_exception_holiday {
|
||||
my $self = shift @_;
|
||||
my %options = @_;
|
||||
my $date = $options{date};
|
||||
unless ($date) {
|
||||
$date = sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} );
|
||||
}
|
||||
@options{qw(year month day)} = map { $_ + 0 } ( $date =~ m/(\d+)-(\d+)-(\d+)/ )
|
||||
unless $options{day};
|
||||
|
||||
Koha::Library::Calendar->new( branchcode => $self->{branchcode} )->add_exception(
|
||||
{
|
||||
date => $date, title => $options{title}, description => $options{description},
|
||||
}
|
||||
@options{qw(year month day)} = ( $options{date} =~ m/(\d+)-(\d+)-(\d+)/o )
|
||||
if $options{date} && !$options{day};
|
||||
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $isexception = 1;
|
||||
my $insertException = $dbh->prepare(
|
||||
"insert into special_holidays (branchcode,day,month,year,isexception,title,description) values (?,?,?,?,?,?,?)"
|
||||
);
|
||||
$insertException->execute(
|
||||
$self->{branchcode}, $options{day}, $options{month}, $options{year}, $isexception,
|
||||
$options{title}, $options{description}
|
||||
);
|
||||
|
||||
$self->{'exception_holidays'}->{"$options{year}/$options{month}/$options{day}"}{title} = $options{title};
|
||||
$self->{'exception_holidays'}->{"$options{year}/$options{month}/$options{day}"}{description} =
|
||||
$options{description};
|
||||
|
||||
# changed the 'single_holidays' table, lets force/reset its cache
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $key = $self->{branchcode} . "_holidays";
|
||||
$cache->clear_from_cache($key);
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
|
@ -351,10 +368,13 @@ C<$description> Is the description to update for the holiday.
|
|||
sub ModWeekdayholiday {
|
||||
my $self = shift @_;
|
||||
my %options = @_;
|
||||
my $closure =
|
||||
Koha::Library::Calendar::WeeklyClosures->search(
|
||||
{ library_id => $self->{branchcode}, weekday => $options{weekday} } )->next;
|
||||
$closure->title( $options{title} )->description( $options{description} )->store if $closure;
|
||||
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $updateHoliday =
|
||||
$dbh->prepare("UPDATE repeatable_holidays SET title = ?, description = ? WHERE branchcode = ? AND weekday = ?");
|
||||
$updateHoliday->execute( $options{title}, $options{description}, $self->{branchcode}, $options{weekday} );
|
||||
$self->{'week_days_holidays'}->{ $options{weekday} }{title} = $options{title};
|
||||
$self->{'week_days_holidays'}->{ $options{weekday} }{description} = $options{description};
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
|
@ -380,9 +400,16 @@ C<$description> The description to be update for the holiday.
|
|||
sub ModDaymonthholiday {
|
||||
my $self = shift @_;
|
||||
my %options = @_;
|
||||
my $closure = Koha::Library::Calendar::RepeatingClosures->search(
|
||||
{ library_id => $self->{branchcode}, day => $options{day}, month => $options{month} } )->next;
|
||||
$closure->title( $options{title} )->description( $options{description} )->store if $closure;
|
||||
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $updateHoliday = $dbh->prepare(
|
||||
"UPDATE repeatable_holidays SET title = ?, description = ? WHERE month = ? AND day = ? AND branchcode = ?");
|
||||
$updateHoliday->execute(
|
||||
$options{title}, $options{description}, $options{month}, $options{day},
|
||||
$self->{branchcode}
|
||||
);
|
||||
$self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{title} = $options{title};
|
||||
$self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{description} = $options{description};
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
|
@ -411,12 +438,25 @@ C<$description> Is the description to update for the holiday formed by $year/$mo
|
|||
sub ModSingleholiday {
|
||||
my $self = shift @_;
|
||||
my %options = @_;
|
||||
my $date = $options{date} || sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} );
|
||||
my $closure =
|
||||
Koha::Library::Calendar::SingleClosures->search( { library_id => $self->{branchcode}, date => $date } )->next;
|
||||
if ($closure) {
|
||||
$closure->title( $options{title} )->description( $options{description} )->store;
|
||||
}
|
||||
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $isexception = 0;
|
||||
|
||||
my $updateHoliday = $dbh->prepare( "
|
||||
UPDATE special_holidays SET title = ?, description = ?
|
||||
WHERE day = ? AND month = ? AND year = ? AND branchcode = ? AND isexception = ?" );
|
||||
$updateHoliday->execute(
|
||||
$options{title}, $options{description}, $options{day}, $options{month}, $options{year},
|
||||
$self->{branchcode}, $isexception
|
||||
);
|
||||
$self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"}{title} = $options{title};
|
||||
$self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"}{description} = $options{description};
|
||||
|
||||
# changed the 'single_holidays' table, lets force/reset its cache
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $key = $self->{branchcode} . "_holidays";
|
||||
$cache->clear_from_cache($key);
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
|
@ -445,12 +485,25 @@ C<$description> Is the description to be modified for the holiday formed by $yea
|
|||
sub ModExceptionholiday {
|
||||
my $self = shift @_;
|
||||
my %options = @_;
|
||||
my $date = $options{date} || sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} );
|
||||
my $exception =
|
||||
Koha::Library::Calendar::Exceptions->search( { library_id => $self->{branchcode}, date => $date } )->next;
|
||||
if ($exception) {
|
||||
$exception->title( $options{title} )->description( $options{description} )->store;
|
||||
}
|
||||
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $isexception = 1;
|
||||
my $updateHoliday = $dbh->prepare( "
|
||||
UPDATE special_holidays SET title = ?, description = ?
|
||||
WHERE day = ? AND month = ? AND year = ? AND branchcode = ? AND isexception = ?" );
|
||||
$updateHoliday->execute(
|
||||
$options{title}, $options{description}, $options{day}, $options{month}, $options{year},
|
||||
$self->{branchcode}, $isexception
|
||||
);
|
||||
$self->{'exception_holidays'}->{"$options{year}/$options{month}/$options{day}"}{title} = $options{title};
|
||||
$self->{'exception_holidays'}->{"$options{year}/$options{month}/$options{day}"}{description} =
|
||||
$options{description};
|
||||
|
||||
# changed the 'single_holidays' table, lets force/reset its cache
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $key = $self->{branchcode} . "_holidays";
|
||||
$cache->clear_from_cache($key);
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
|
@ -476,40 +529,67 @@ C<$year> Is year to make the date to delete.
|
|||
sub delete_holiday {
|
||||
my $self = shift @_;
|
||||
my %options = @_;
|
||||
my $cal = Koha::Library::Calendar->new( branchcode => $self->{branchcode} );
|
||||
|
||||
if ( $options{day} && $options{month} && $options{year} ) {
|
||||
my $date = sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} );
|
||||
# Verify what kind of holiday that day is. For example, if it is
|
||||
# a repeatable holiday, this should check if there are some exception
|
||||
# for that holiday rule. Otherwise, if it is a regular holiday, it´s
|
||||
# ok just deleting it.
|
||||
|
||||
# Try single closure first
|
||||
if ( Koha::Library::Calendar::SingleClosures->search( { library_id => $self->{branchcode}, date => $date } )
|
||||
->count )
|
||||
{
|
||||
$cal->delete_single_closure( { date => $date } );
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $isSingleHoliday = $dbh->prepare(
|
||||
"SELECT id FROM special_holidays WHERE (branchcode = ?) AND (day = ?) AND (month = ?) AND (year = ?)");
|
||||
$isSingleHoliday->execute( $self->{branchcode}, $options{day}, $options{month}, $options{year} );
|
||||
if ( $isSingleHoliday->rows ) {
|
||||
my $id = $isSingleHoliday->fetchrow;
|
||||
$isSingleHoliday->finish; # Close the last query
|
||||
|
||||
my $deleteHoliday = $dbh->prepare("DELETE FROM special_holidays WHERE id = ?");
|
||||
$deleteHoliday->execute($id);
|
||||
delete( $self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"} );
|
||||
} else {
|
||||
$isSingleHoliday->finish; # Close the last query
|
||||
|
||||
my $isWeekdayHoliday = $dbh->prepare("SELECT id FROM repeatable_holidays WHERE branchcode = ? AND weekday = ?");
|
||||
$isWeekdayHoliday->execute( $self->{branchcode}, $options{weekday} );
|
||||
if ( $isWeekdayHoliday->rows ) {
|
||||
my $id = $isWeekdayHoliday->fetchrow;
|
||||
$isWeekdayHoliday->finish; # Close the last query
|
||||
|
||||
my $updateExceptions = $dbh->prepare(
|
||||
"UPDATE special_holidays SET isexception = 0 WHERE (WEEKDAY(CONCAT(special_holidays.year,'-',special_holidays.month,'-',special_holidays.day)) = ?) AND (branchcode = ?)"
|
||||
);
|
||||
$updateExceptions->execute( $options{weekday}, $self->{branchcode} );
|
||||
$updateExceptions->finish; # Close the last query
|
||||
|
||||
my $deleteHoliday = $dbh->prepare("DELETE FROM repeatable_holidays WHERE id = ?");
|
||||
$deleteHoliday->execute($id);
|
||||
delete( $self->{'week_days_holidays'}->{ $options{weekday} } );
|
||||
} else {
|
||||
$isWeekdayHoliday->finish; # Close the last query
|
||||
|
||||
my $isDayMonthHoliday = $dbh->prepare(
|
||||
"SELECT id FROM repeatable_holidays WHERE (branchcode = ?) AND (day = ?) AND (month = ?)");
|
||||
$isDayMonthHoliday->execute( $self->{branchcode}, $options{day}, $options{month} );
|
||||
if ( $isDayMonthHoliday->rows ) {
|
||||
my $id = $isDayMonthHoliday->fetchrow;
|
||||
$isDayMonthHoliday->finish;
|
||||
my $updateExceptions = $dbh->prepare(
|
||||
"UPDATE special_holidays SET isexception = 0 WHERE (special_holidays.branchcode = ?) AND (special_holidays.day = ?) and (special_holidays.month = ?)"
|
||||
);
|
||||
$updateExceptions->execute( $self->{branchcode}, $options{day}, $options{month} );
|
||||
$updateExceptions->finish; # Close the last query
|
||||
|
||||
my $deleteHoliday = $dbh->prepare("DELETE FROM repeatable_holidays WHERE (id = ?)");
|
||||
$deleteHoliday->execute($id);
|
||||
delete( $self->{'day_month_holidays'}->{"$options{month}/$options{day}"} );
|
||||
}
|
||||
}
|
||||
|
||||
# Also remove any exception for this date
|
||||
Koha::Library::Calendar::Exceptions->search( { library_id => $self->{branchcode}, date => $date } )->delete;
|
||||
}
|
||||
|
||||
if (
|
||||
defined $options{weekday}
|
||||
&& Koha::Library::Calendar::WeeklyClosures->search(
|
||||
{ library_id => $self->{branchcode}, weekday => $options{weekday} }
|
||||
)->count
|
||||
)
|
||||
{
|
||||
$cal->delete_weekly_closure( { weekday => $options{weekday} } );
|
||||
} elsif (
|
||||
$options{day}
|
||||
&& $options{month}
|
||||
&& Koha::Library::Calendar::RepeatingClosures->search(
|
||||
{ library_id => $self->{branchcode}, day => $options{day}, month => $options{month} }
|
||||
)->count
|
||||
)
|
||||
{
|
||||
$cal->delete_repeating_closure( { day => $options{day}, month => $options{month} } );
|
||||
}
|
||||
# changed the 'single_holidays' table, lets force/reset its cache
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $key = $self->{branchcode} . "_holidays";
|
||||
$cache->clear_from_cache($key);
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
|
@ -533,8 +613,17 @@ C<$year> Is year to make the date to delete.
|
|||
sub delete_holiday_range {
|
||||
my $self = shift;
|
||||
my %options = @_;
|
||||
my $date = $options{date} || sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} );
|
||||
Koha::Library::Calendar->new( branchcode => $self->{branchcode} )->delete_single_closure( { date => $date } );
|
||||
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $sth = $dbh->prepare(
|
||||
"DELETE FROM special_holidays WHERE (branchcode = ?) AND (day = ?) AND (month = ?) AND (year = ?)");
|
||||
$sth->execute( $self->{branchcode}, $options{day}, $options{month}, $options{year} );
|
||||
|
||||
# changed the 'single_holidays' table, lets force/reset its cache
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $key = $self->{branchcode} . "_holidays";
|
||||
$cache->clear_from_cache($key);
|
||||
|
||||
}
|
||||
|
||||
=head2 delete_holiday_range_repeatable
|
||||
|
|
@ -553,8 +642,15 @@ C<$month> Is month to make the date to delete.
|
|||
sub delete_holiday_range_repeatable {
|
||||
my $self = shift;
|
||||
my %options = @_;
|
||||
Koha::Library::Calendar->new( branchcode => $self->{branchcode} )
|
||||
->delete_repeating_closure( { day => $options{day}, month => $options{month} } );
|
||||
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $sth = $dbh->prepare("DELETE FROM repeatable_holidays WHERE (branchcode = ?) AND (day = ?) AND (month = ?)");
|
||||
$sth->execute( $self->{branchcode}, $options{day}, $options{month} );
|
||||
|
||||
# changed the 'single_holidays' table, lets force/reset its cache
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $key = $self->{branchcode} . "_holidays";
|
||||
$cache->clear_from_cache($key);
|
||||
}
|
||||
|
||||
=head2 delete_exception_holiday_range
|
||||
|
|
@ -577,8 +673,17 @@ C<$year> Is year to make the date to delete.
|
|||
sub delete_exception_holiday_range {
|
||||
my $self = shift;
|
||||
my %options = @_;
|
||||
my $date = $options{date} || sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} );
|
||||
Koha::Library::Calendar->new( branchcode => $self->{branchcode} )->delete_exception( { date => $date } );
|
||||
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $sth = $dbh->prepare(
|
||||
"DELETE FROM special_holidays WHERE (branchcode = ?) AND (isexception = 1) AND (day = ?) AND (month = ?) AND (year = ?)"
|
||||
);
|
||||
$sth->execute( $self->{branchcode}, $options{day}, $options{month}, $options{year} );
|
||||
|
||||
# changed the 'single_holidays' table, lets force/reset its cache
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $key = $self->{branchcode} . "_holidays";
|
||||
$cache->clear_from_cache($key);
|
||||
}
|
||||
|
||||
=head2 isHoliday
|
||||
|
|
@ -629,7 +734,47 @@ sub isHoliday {
|
|||
|
||||
sub copy_to_branch {
|
||||
my ( $self, $target_branch ) = @_;
|
||||
return Koha::Library::Calendar->new( branchcode => $self->{branchcode} )->copy_to($target_branch);
|
||||
|
||||
croak "No target_branch" unless $target_branch;
|
||||
|
||||
my $target_calendar = C4::Calendar->new( branchcode => $target_branch );
|
||||
|
||||
my ( $y, $m, $d ) = Today();
|
||||
my $today = sprintf ISO_DATE_FORMAT, $y, $m, $d;
|
||||
|
||||
my $wdh = $self->get_week_days_holidays;
|
||||
my $target_wdh = $target_calendar->get_week_days_holidays;
|
||||
foreach my $key ( keys %$wdh ) {
|
||||
unless ( grep { $_ eq $key } keys %$target_wdh ) {
|
||||
$target_calendar->insert_week_day_holiday( weekday => $key, %{ $wdh->{$key} } );
|
||||
}
|
||||
}
|
||||
|
||||
my $dmh = $self->get_day_month_holidays;
|
||||
my $target_dmh = $target_calendar->get_day_month_holidays;
|
||||
foreach my $values ( values %$dmh ) {
|
||||
unless ( grep { $_->{day} eq $values->{day} && $_->{month} eq $values->{month} } values %$target_dmh ) {
|
||||
$target_calendar->insert_day_month_holiday( %{$values} );
|
||||
}
|
||||
}
|
||||
|
||||
my $exception_holidays = $self->get_exception_holidays;
|
||||
my $target_exceptions = $target_calendar->get_exception_holidays;
|
||||
foreach my $values ( grep { $_->{date} gt $today } values %{$exception_holidays} ) {
|
||||
unless ( grep { $_->{date} eq $values->{date} } values %$target_exceptions ) {
|
||||
$target_calendar->insert_exception_holiday( %{$values} );
|
||||
}
|
||||
}
|
||||
|
||||
my $single_holidays = $self->get_single_holidays;
|
||||
my $target_singles = $target_calendar->get_single_holidays;
|
||||
foreach my $values ( grep { $_->{date} gt $today } values %{$single_holidays} ) {
|
||||
unless ( grep { $_->{date} eq $values->{date} } values %$target_singles ) {
|
||||
$target_calendar->insert_single_holiday( %{$values} );
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ BEGIN {
|
|||
GetUpcomingDueIssues
|
||||
CheckIfIssuedToPatron
|
||||
IsItemIssued
|
||||
GetAgeRestriction
|
||||
GetTopIssues
|
||||
|
||||
AddReturn
|
||||
|
|
@ -72,6 +73,7 @@ BEGIN {
|
|||
ProcessOfflinePayment
|
||||
ProcessOfflineIssue
|
||||
);
|
||||
push @EXPORT_OK, '_GetCircControlBranch'; # This is wrong!
|
||||
}
|
||||
|
||||
use DateTime;
|
||||
|
|
@ -81,7 +83,7 @@ use Try::Tiny;
|
|||
use JSON;
|
||||
|
||||
use C4::Context;
|
||||
use Koha::Policy::Circulation;
|
||||
use C4::Stats qw( UpdateStats );
|
||||
use C4::Reserves
|
||||
qw( CheckReserves CanItemBeReserved MoveReserve ModReserve ModReserveMinusPriority IsAvailableForItemLevelRequest );
|
||||
use C4::Biblio qw( UpdateTotalIssues );
|
||||
|
|
@ -100,14 +102,12 @@ use Koha::AuthorisedValues;
|
|||
use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
|
||||
use Koha::Biblioitems;
|
||||
use Koha::DateUtils qw( dt_from_string );
|
||||
use Koha::Library::Calendar;
|
||||
use Koha::Calendar;
|
||||
use Koha::Checkouts;
|
||||
use Koha::ILL::Requests;
|
||||
use Koha::ILL::ISO18626::Requests;
|
||||
use Koha::Bookings;
|
||||
use Koha::Items;
|
||||
use Koha::Patrons;
|
||||
use Koha::Policy::Biblio::AgeRestriction;
|
||||
use Koha::Patron::Debarments qw( DelUniqueDebarment AddUniqueDebarment );
|
||||
use Koha::Database;
|
||||
use Koha::Libraries;
|
||||
|
|
@ -123,7 +123,6 @@ use Koha::SearchEngine::Indexer;
|
|||
use Koha::Exceptions::Checkout;
|
||||
use Koha::Plugins;
|
||||
use Koha::Recalls;
|
||||
use Koha::Statistics;
|
||||
use Koha::Library::Hours;
|
||||
use Koha::Library::FloatLimits;
|
||||
use Carp qw( carp );
|
||||
|
|
@ -394,7 +393,7 @@ sub transferbook {
|
|||
}
|
||||
|
||||
# find recall
|
||||
if ( C4::Context->preference('UseRecalls') ne "off" ) {
|
||||
if ( C4::Context->preference('UseRecalls') ) {
|
||||
my $recall = Koha::Recalls->find( { item_id => $itemnumber, status => 'in_transit' } );
|
||||
if ( defined $recall ) {
|
||||
|
||||
|
|
@ -436,7 +435,7 @@ sub TooMany {
|
|||
my $cat_borrower = $patron->categorycode;
|
||||
|
||||
# Get which branchcode we need
|
||||
my $branch = Koha::Policy::Circulation->circ_control_library( $item, $patron );
|
||||
my $branch = _GetCircControlBranch( $item, $patron );
|
||||
my $type = $item->effective_itemtype;
|
||||
|
||||
my ( $type_object, $parent_type, $parent_maxissueqty_rule );
|
||||
|
|
@ -860,12 +859,10 @@ sub CanBookBeIssued {
|
|||
my $dbh = C4::Context->dbh;
|
||||
my $patron_unblessed = $patron->unblessed;
|
||||
|
||||
my $circ_library =
|
||||
Koha::Libraries->find( Koha::Policy::Circulation->circ_control_library( $item_object, $patron ) );
|
||||
my $circ_library = Koha::Libraries->find( _GetCircControlBranch( $item_object, $patron ) );
|
||||
|
||||
my $now = dt_from_string();
|
||||
|
||||
my $requested_duedate = $duedate ? 1 : 0;
|
||||
$duedate ||= CalcDateDue( $now, $effective_itemtype, $circ_library->branchcode, $patron );
|
||||
|
||||
if ( DateTime->compare( $duedate, $now ) == -1 ) { # duedate cannot be before now
|
||||
|
|
@ -888,7 +885,7 @@ sub CanBookBeIssued {
|
|||
if ( $patron->category->category_type eq 'X' && ( $item_object->barcode ) ) {
|
||||
|
||||
# stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
|
||||
Koha::Statistic->new(
|
||||
C4::Stats::UpdateStats(
|
||||
{
|
||||
branch => C4::Context->userenv->{'branch'},
|
||||
type => 'localuse',
|
||||
|
|
@ -900,7 +897,7 @@ sub CanBookBeIssued {
|
|||
location => $item_object->location,
|
||||
interface => C4::Context->interface,
|
||||
}
|
||||
)->store();
|
||||
);
|
||||
my $block_lost_return = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
|
||||
ModDateLastSeen( $item_object->itemnumber, $block_lost_return ); # FIXME Move to Koha::Item
|
||||
return ( { STATS => 1, }, {} );
|
||||
|
|
@ -976,29 +973,6 @@ sub CanBookBeIssued {
|
|||
}
|
||||
}
|
||||
|
||||
my $no_issues_charge_linked = $patron_borrowing_status->{NoIssuesChargeLinkedAccounts}->{limit};
|
||||
if ( defined $no_issues_charge_linked ) {
|
||||
if ( $patron_borrowing_status->{NoIssuesChargeLinkedAccounts}->{overlimit}
|
||||
&& !$inprocess
|
||||
&& !$allowfineoverride )
|
||||
{
|
||||
$issuingimpossible{DEBT_LINKED_ACCOUNTS} =
|
||||
$patron_borrowing_status->{NoIssuesChargeLinkedAccounts}->{charge};
|
||||
} elsif ( $patron_borrowing_status->{NoIssuesChargeLinkedAccounts}->{overlimit}
|
||||
&& !$inprocess
|
||||
&& $allowfineoverride )
|
||||
{
|
||||
$needsconfirmation{DEBT_LINKED_ACCOUNTS} =
|
||||
$patron_borrowing_status->{NoIssuesChargeLinkedAccounts}->{charge};
|
||||
} elsif ( $allfinesneedoverride
|
||||
&& $patron_borrowing_status->{NoIssuesChargeLinkedAccounts}->{charge} > 0
|
||||
&& !$inprocess )
|
||||
{
|
||||
$needsconfirmation{DEBT_LINKED_ACCOUNTS} =
|
||||
$patron_borrowing_status->{NoIssuesChargeLinkedAccounts}->{charge};
|
||||
}
|
||||
}
|
||||
|
||||
if ( C4::Context->preference("IssuingInProcess") ) {
|
||||
if ( $patron_borrowing_status->{noissuescharge}->{overlimit} && !$inprocess && !$allowfineoverride ) {
|
||||
$issuingimpossible{DEBT} = $patron_borrowing_status->{noissuescharge}->{charge};
|
||||
|
|
@ -1237,7 +1211,7 @@ sub CanBookBeIssued {
|
|||
# CHECK IF ITEM HAS BEEN RECALLED BY ANOTHER PATRON
|
||||
# Only bother doing this if UseRecalls is enabled and the item is recallable
|
||||
# Don't look at recalls that are in transit
|
||||
if ( C4::Context->preference('UseRecalls') ne "off" and $item_object->can_be_waiting_recall ) {
|
||||
if ( C4::Context->preference('UseRecalls') and $item_object->can_be_waiting_recall ) {
|
||||
my @recalls = $biblio->recalls( {}, { order_by => { -asc => 'created_date' } } )->filter_by_current->as_list;
|
||||
|
||||
foreach my $r (@recalls) {
|
||||
|
|
@ -1285,34 +1259,16 @@ sub CanBookBeIssued {
|
|||
if ($restype) {
|
||||
my $resbor = $res->{'borrowernumber'};
|
||||
if ( $resbor ne $patron->borrowernumber ) {
|
||||
my $hold_patron = Koha::Patrons->find($resbor);
|
||||
|
||||
# Check if hold is for a linked account
|
||||
my $is_linked_account = 0;
|
||||
if ( C4::Context->preference('EnablePatronAccountLinking')
|
||||
&& C4::Context->preference('AllowLinkedAccountHoldPickup') )
|
||||
{
|
||||
my $linked_ids = $patron->all_linked_borrowernumbers;
|
||||
$is_linked_account = grep { $_ == $resbor } @$linked_ids;
|
||||
}
|
||||
|
||||
if ($is_linked_account) {
|
||||
|
||||
# Hold is for a linked account - allow checkout with redirect
|
||||
$alerts{LINKED_ACCOUNT_HOLD_PICKUP} = {
|
||||
reserve_id => $res->{reserve_id},
|
||||
hold_patron_id => $resbor,
|
||||
hold_patron => $hold_patron,
|
||||
};
|
||||
} elsif ( $restype eq "Waiting" ) {
|
||||
my $patron = Koha::Patrons->find($resbor);
|
||||
if ( $restype eq "Waiting" ) {
|
||||
|
||||
# The item is on reserve and waiting, but has been
|
||||
# reserved by some other patron.
|
||||
$needsconfirmation{RESERVE_WAITING} = 1;
|
||||
$needsconfirmation{'resfirstname'} = $hold_patron->firstname;
|
||||
$needsconfirmation{'ressurname'} = $hold_patron->surname;
|
||||
$needsconfirmation{'rescardnumber'} = $hold_patron->cardnumber;
|
||||
$needsconfirmation{'resborrowernumber'} = $hold_patron->borrowernumber;
|
||||
$needsconfirmation{'resfirstname'} = $patron->firstname;
|
||||
$needsconfirmation{'ressurname'} = $patron->surname;
|
||||
$needsconfirmation{'rescardnumber'} = $patron->cardnumber;
|
||||
$needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
|
||||
$needsconfirmation{'resbranchcode'} = $res->{branchcode};
|
||||
$needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
|
||||
$needsconfirmation{'reserve_id'} = $res->{reserve_id};
|
||||
|
|
@ -1320,10 +1276,10 @@ sub CanBookBeIssued {
|
|||
|
||||
# The item is on reserve for someone else.
|
||||
$needsconfirmation{RESERVED} = 1;
|
||||
$needsconfirmation{'resfirstname'} = $hold_patron->firstname;
|
||||
$needsconfirmation{'ressurname'} = $hold_patron->surname;
|
||||
$needsconfirmation{'rescardnumber'} = $hold_patron->cardnumber;
|
||||
$needsconfirmation{'resborrowernumber'} = $hold_patron->borrowernumber;
|
||||
$needsconfirmation{'resfirstname'} = $patron->firstname;
|
||||
$needsconfirmation{'ressurname'} = $patron->surname;
|
||||
$needsconfirmation{'rescardnumber'} = $patron->cardnumber;
|
||||
$needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
|
||||
$needsconfirmation{'resbranchcode'} = $res->{branchcode};
|
||||
$needsconfirmation{'resreservedate'} = $res->{reservedate};
|
||||
$needsconfirmation{'reserve_id'} = $res->{reserve_id};
|
||||
|
|
@ -1331,10 +1287,10 @@ sub CanBookBeIssued {
|
|||
|
||||
# The item is determined hold being transferred for someone else.
|
||||
$needsconfirmation{TRANSFERRED} = 1;
|
||||
$needsconfirmation{'resfirstname'} = $hold_patron->firstname;
|
||||
$needsconfirmation{'ressurname'} = $hold_patron->surname;
|
||||
$needsconfirmation{'rescardnumber'} = $hold_patron->cardnumber;
|
||||
$needsconfirmation{'resborrowernumber'} = $hold_patron->borrowernumber;
|
||||
$needsconfirmation{'resfirstname'} = $patron->firstname;
|
||||
$needsconfirmation{'ressurname'} = $patron->surname;
|
||||
$needsconfirmation{'rescardnumber'} = $patron->cardnumber;
|
||||
$needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
|
||||
$needsconfirmation{'resbranchcode'} = $res->{branchcode};
|
||||
$needsconfirmation{'resreservedate'} = $res->{reservedate};
|
||||
$needsconfirmation{'reserve_id'} = $res->{reserve_id};
|
||||
|
|
@ -1342,10 +1298,10 @@ sub CanBookBeIssued {
|
|||
|
||||
# The item is determined hold being processed for someone else.
|
||||
$needsconfirmation{PROCESSING} = 1;
|
||||
$needsconfirmation{'resfirstname'} = $hold_patron->firstname;
|
||||
$needsconfirmation{'ressurname'} = $hold_patron->surname;
|
||||
$needsconfirmation{'rescardnumber'} = $hold_patron->cardnumber;
|
||||
$needsconfirmation{'resborrowernumber'} = $hold_patron->borrowernumber;
|
||||
$needsconfirmation{'resfirstname'} = $patron->firstname;
|
||||
$needsconfirmation{'ressurname'} = $patron->surname;
|
||||
$needsconfirmation{'rescardnumber'} = $patron->cardnumber;
|
||||
$needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
|
||||
$needsconfirmation{'resbranchcode'} = $res->{branchcode};
|
||||
$needsconfirmation{'resreservedate'} = $res->{reservedate};
|
||||
$needsconfirmation{'reserve_id'} = $res->{reserve_id};
|
||||
|
|
@ -1370,21 +1326,6 @@ sub CanBookBeIssued {
|
|||
} else {
|
||||
$alerts{'BOOKED'} = $booking;
|
||||
}
|
||||
|
||||
# An explicitly requested due date that runs into the next booking
|
||||
# for this item, regardless of whose booking that is, needs staff
|
||||
# confirmation; the booking end date will not be extended past that
|
||||
# booking (see AddIssue)
|
||||
if ($requested_duedate) {
|
||||
my $next_booking = $item_object->find_booking(
|
||||
{
|
||||
checkout_date => $now,
|
||||
due_date => $duedate,
|
||||
exclude_booking_id => $booking->booking_id,
|
||||
}
|
||||
);
|
||||
$needsconfirmation{'BOOKED_DUE_DATE_CLASH'} = $next_booking if $next_booking;
|
||||
}
|
||||
}
|
||||
|
||||
# Booked to another patron
|
||||
|
|
@ -1402,9 +1343,9 @@ sub CanBookBeIssued {
|
|||
}
|
||||
|
||||
## CHECK AGE RESTRICTION
|
||||
my $age_check = Koha::Policy::Biblio::AgeRestriction->check( $biblio, $patron );
|
||||
if ( !$age_check ) {
|
||||
my $agerestriction = $biblioitem->agerestriction;
|
||||
my $agerestriction = $biblioitem->agerestriction;
|
||||
my $restriction_age = GetAgeRestriction($agerestriction);
|
||||
if ( $restriction_age && $patron->dateofbirth && $restriction_age > $patron->get_age() ) {
|
||||
if ( C4::Context->preference('AgeRestrictionOverride') ) {
|
||||
$needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
|
||||
} else {
|
||||
|
|
@ -1520,7 +1461,37 @@ Returns:
|
|||
|
||||
sub CanBookBeReturned {
|
||||
my ( $item, $returnbranch, $transferbranch ) = @_;
|
||||
return $item->can_be_returned_at( { library => $returnbranch, to_library => $transferbranch } );
|
||||
my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
|
||||
|
||||
my $allowed = 1;
|
||||
my $message;
|
||||
|
||||
# Refuse check-in if it does not respect AllowReturnToBranch rules
|
||||
if ( $allowreturntobranch eq 'homebranch' && $returnbranch ne $item->homebranch ) {
|
||||
$allowed = 0;
|
||||
$message = $item->homebranch;
|
||||
} elsif ( $allowreturntobranch eq 'holdingbranch' && $returnbranch ne $item->holdingbranch ) {
|
||||
$allowed = 0;
|
||||
$message = $item->holdingbranch;
|
||||
} elsif ( $allowreturntobranch eq 'homeorholdingbranch'
|
||||
&& $returnbranch ne $item->homebranch
|
||||
&& $returnbranch ne $item->holdingbranch )
|
||||
{
|
||||
$allowed = 0;
|
||||
$message = $item->homebranch; # FIXME: choice of homebranch is arbitrary
|
||||
}
|
||||
|
||||
# Refuse check-in if book cannot be transferred to $transferbranch due to transfer limits
|
||||
if ( defined($transferbranch) && $allowed ) {
|
||||
my $from_library = Koha::Libraries->find($returnbranch);
|
||||
my $to_library = Koha::Libraries->find($transferbranch);
|
||||
if ( !$item->can_be_transferred( { from => $from_library, to => $to_library } ) ) {
|
||||
$allowed = 0;
|
||||
$message = $transferbranch;
|
||||
}
|
||||
}
|
||||
|
||||
return ( $allowed, $message );
|
||||
}
|
||||
|
||||
=head2 CheckHighHolds
|
||||
|
|
@ -1539,7 +1510,7 @@ Missing POD for checkHighHolds.
|
|||
|
||||
sub checkHighHolds {
|
||||
my ( $item, $patron ) = @_;
|
||||
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
|
||||
my $branchcode = _GetCircControlBranch( $item, $patron );
|
||||
|
||||
my $return_data = {
|
||||
exceeded => 0,
|
||||
|
|
@ -1613,7 +1584,7 @@ sub checkHighHolds {
|
|||
branchcode => $branchcode,
|
||||
}
|
||||
);
|
||||
my $calendar = Koha::Library::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
|
||||
my $calendar = Koha::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
|
||||
|
||||
my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $patron );
|
||||
|
||||
|
|
@ -1729,7 +1700,7 @@ sub AddIssue {
|
|||
or return; # if we don't get an Item, abort.
|
||||
my $item_unblessed = $item_object->unblessed;
|
||||
|
||||
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item_object, $patron );
|
||||
my $branchcode = _GetCircControlBranch( $item_object, $patron );
|
||||
|
||||
# get actual issuing if there is one
|
||||
my $actualissue = $item_object->checkout;
|
||||
|
|
@ -1803,7 +1774,7 @@ sub AddIssue {
|
|||
$item_object->discard_changes;
|
||||
}
|
||||
|
||||
if ( C4::Context->preference('UseRecalls') ne "off" ) {
|
||||
if ( C4::Context->preference('UseRecalls') ) {
|
||||
Koha::Recalls->move_recall(
|
||||
{
|
||||
action => $cancel_recall,
|
||||
|
|
@ -1883,36 +1854,8 @@ sub AddIssue {
|
|||
{
|
||||
if ( $booking->patron_id == $patron->borrowernumber ) {
|
||||
|
||||
# Patron's own booking - mark as issued and link checkout to booking
|
||||
$booking->status('issued');
|
||||
|
||||
# Sync any due date override back to the booking end_date so the
|
||||
# booking and checkout agree from the moment of issue
|
||||
my $booking_end = dt_from_string( $booking->end_date );
|
||||
if ( $datedue->compare($booking_end) != 0 ) {
|
||||
$booking->end_date($datedue);
|
||||
|
||||
# An early collection can be due back before the booked period
|
||||
# even starts; pull the start_date back to the checkout date
|
||||
# rather than storing an inverted booking period
|
||||
my $booking_start = dt_from_string( $booking->start_date );
|
||||
$booking->start_date($issuedate) if $datedue->compare($booking_start) < 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$booking->store;
|
||||
} catch {
|
||||
my $error = $_;
|
||||
if ( blessed $error && $error->isa('Koha::Exceptions::Booking::Clash') ) {
|
||||
|
||||
# The overridden due date would clash with a subsequent
|
||||
# booking; proceed with the booking's original end_date
|
||||
$booking->discard_changes;
|
||||
$booking->status('issued')->store;
|
||||
} else {
|
||||
die $error;
|
||||
}
|
||||
};
|
||||
# Patron's own booking - mark as completed and link checkout to booking
|
||||
$booking->status('completed')->store;
|
||||
$issue_attributes->{'booking_id'} = $booking->booking_id;
|
||||
} else {
|
||||
|
||||
|
|
@ -2016,7 +1959,7 @@ sub AddIssue {
|
|||
|
||||
# If it costs to borrow this book, charge it to the patron's account.
|
||||
my ( $charge, $itemtype ) = GetIssuingCharges( $item_object->itemnumber, $patron->borrowernumber );
|
||||
if ( $charge > 0 ) {
|
||||
if ( $charge && $charge > 0 ) {
|
||||
AddIssuingCharge( $issue, $charge, 'RENT' );
|
||||
}
|
||||
|
||||
|
|
@ -2042,7 +1985,7 @@ sub AddIssue {
|
|||
}
|
||||
|
||||
# Record the fact that this book was issued.
|
||||
Koha::Statistic->new(
|
||||
C4::Stats::UpdateStats(
|
||||
{
|
||||
branch => C4::Context->userenv->{'branch'},
|
||||
type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
|
||||
|
|
@ -2055,7 +1998,7 @@ sub AddIssue {
|
|||
ccode => $item_object->ccode,
|
||||
categorycode => $patron->categorycode
|
||||
}
|
||||
)->store();
|
||||
);
|
||||
|
||||
# Send a checkout slip.
|
||||
my $circulation_alert = 'C4::ItemCirculationAlertPreference';
|
||||
|
|
@ -2078,16 +2021,19 @@ sub AddIssue {
|
|||
|
||||
# Log the checkout
|
||||
if ( C4::Context->preference('IssueLog') ) {
|
||||
my $info = to_json(
|
||||
{
|
||||
issue => $issue->issue_id,
|
||||
branchcode => $issue->branchcode,
|
||||
itemnumber => $item_object->itemnumber,
|
||||
confirmations => $confirmations || [],
|
||||
forced => $forced || []
|
||||
},
|
||||
{ pretty => 1, canonical => 1 }
|
||||
);
|
||||
my $info = $item_object->itemnumber;
|
||||
if ( defined($confirmations) || defined($forced) ) {
|
||||
$info = to_json(
|
||||
{
|
||||
issue => $issue->issue_id,
|
||||
branchcode => $issue->branchcode,
|
||||
itemnumber => $item_object->itemnumber,
|
||||
confirmations => $confirmations,
|
||||
forced => $forced
|
||||
},
|
||||
{ pretty => 1, canonical => 1 }
|
||||
);
|
||||
}
|
||||
logaction(
|
||||
"CIRCULATION", "ISSUE",
|
||||
$patron->borrowernumber,
|
||||
|
|
@ -2407,13 +2353,51 @@ sub AddReturn {
|
|||
my $validTransfer = 1;
|
||||
my $stat_type = 'return';
|
||||
|
||||
# Get item
|
||||
# get information on item
|
||||
my $item = Koha::Items->find( { barcode => $barcode } );
|
||||
unless ($item) {
|
||||
return ( 0, { BadBarcode => $barcode } ); # no barcode means no item or borrower. bail out.
|
||||
}
|
||||
|
||||
# Check availability
|
||||
my $itemnumber = $item->itemnumber;
|
||||
my $itemtype = $item->effective_itemtype;
|
||||
my $localuse_count = $item->localuse || 0;
|
||||
|
||||
my $issue = $item->checkout;
|
||||
if ($issue) {
|
||||
$patron = $issue->patron
|
||||
or die
|
||||
"Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '"
|
||||
. $issue->borrowernumber . "'\n"
|
||||
. Dumper( $issue->unblessed ) . "\n";
|
||||
$iso18626_request = $issue->iso18626_request;
|
||||
} else {
|
||||
$messages->{'NotIssued'} = $barcode;
|
||||
$item->onloan(undef)->store( { skip_record_index => 1, skip_holds_queue => 1 } ) if defined $item->onloan;
|
||||
|
||||
# even though item is not on loan, it may still be transferred; therefore, get current branch info
|
||||
$doreturn = 0;
|
||||
|
||||
# No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
|
||||
# Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
|
||||
if ( C4::Context->preference("RecordLocalUseOnReturn") ) {
|
||||
$localuse_count++;
|
||||
$item->localuse($localuse_count)->store;
|
||||
$messages->{'LocalUse'} = 1;
|
||||
$stat_type = 'localuse';
|
||||
}
|
||||
}
|
||||
|
||||
if ( $item->withdrawn ) { # book has been cancelled
|
||||
$messages->{'withdrawn'} = 1;
|
||||
|
||||
# In the case where we block return of withdrawn, we should completely block the return
|
||||
# without updating item statuses, so we exit early
|
||||
return ( 0, $messages, $issue, ( $patron ? $patron->unblessed : {} ) )
|
||||
if C4::Context->preference("BlockReturnOfWithdrawnItems");
|
||||
}
|
||||
|
||||
# full item data, but no borrowernumber or checkout info (no issue)
|
||||
my $hbr = Koha::CirculationRules->get_return_branch_policy($item);
|
||||
|
||||
# check if returnbranch and homebranch belong to the same float group
|
||||
|
|
@ -2450,66 +2434,6 @@ sub AddReturn {
|
|||
}
|
||||
}
|
||||
|
||||
my $availability = $item->checkin_availability( { library => $branch, to_library => $returnbranch } );
|
||||
|
||||
# Extract context objects
|
||||
my $issue = $availability->context->{checkout};
|
||||
$patron = $availability->context->{patron};
|
||||
$iso18626_request = $issue->iso18626_request if $issue;
|
||||
|
||||
# Data consistency check for issued items - must run before any blocker
|
||||
# handling so that DB corruption is never masked by an early return
|
||||
if ( $issue && !$patron ) {
|
||||
die "Data inconsistency: barcode $barcode (itemnumber:"
|
||||
. $item->itemnumber
|
||||
. ") claims to be issued to non-existent borrowernumber '"
|
||||
. $issue->borrowernumber . "'\n"
|
||||
. Dumper( $issue->unblessed ) . "\n";
|
||||
}
|
||||
|
||||
# Set NotIssued message if item not checked out
|
||||
if ( !$issue ) {
|
||||
$messages->{'NotIssued'} = $barcode;
|
||||
$item->onloan(undef)->store( { skip_record_index => 1, skip_holds_queue => 1 } ) if defined $item->onloan;
|
||||
$doreturn = 0;
|
||||
}
|
||||
|
||||
# Handle BlockedWithdrawn blocker first - this was the earliest early
|
||||
# return in the original code, before any status updates ran
|
||||
if ( !$availability->available && $availability->blockers->{BlockedWithdrawn} ) {
|
||||
$messages->{'withdrawn'} = 1;
|
||||
|
||||
# Record local use even when blocked, if preference is on
|
||||
if ( !$issue && C4::Context->preference("RecordLocalUseOnReturn") ) {
|
||||
my $localuse_count = $item->localuse || 0;
|
||||
$localuse_count++;
|
||||
$item->localuse($localuse_count)->store;
|
||||
$messages->{'LocalUse'} = 1;
|
||||
}
|
||||
|
||||
return ( 0, $messages, $issue, ( $patron ? $patron->unblessed : {} ) );
|
||||
}
|
||||
|
||||
my $itemnumber = $item->itemnumber;
|
||||
my $itemtype = $item->effective_itemtype;
|
||||
my $localuse_count = $item->localuse || 0;
|
||||
|
||||
# Handle not issued items (continued)
|
||||
if ( !$issue ) {
|
||||
|
||||
# Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
|
||||
if ( C4::Context->preference("RecordLocalUseOnReturn") ) {
|
||||
$localuse_count++;
|
||||
$item->localuse($localuse_count)->store;
|
||||
$messages->{'LocalUse'} = 1;
|
||||
$stat_type = 'localuse';
|
||||
}
|
||||
}
|
||||
|
||||
if ( $availability->warnings->{withdrawn} ) { # book has been cancelled
|
||||
$messages->{'withdrawn'} = 1;
|
||||
}
|
||||
|
||||
my $borrowernumber = $patron ? $patron->borrowernumber : undef; # we don't know if we had a borrower or not
|
||||
my $patron_unblessed = $patron ? $patron->unblessed : {};
|
||||
|
||||
|
|
@ -2546,22 +2470,22 @@ sub AddReturn {
|
|||
}
|
||||
}
|
||||
|
||||
# Handle remaining blockers after status updates - in the original code
|
||||
# CanBookBeReturned and BlockReturnOfLostItems ran after
|
||||
# location_update_trigger and UpdateNotForLoanStatusOnCheckin
|
||||
if ( !$availability->available ) {
|
||||
my $blockers = $availability->blockers;
|
||||
# check if the return is allowed at this branch
|
||||
my ( $returnallowed, $message ) = CanBookBeReturned( $item, $branch, $returnbranch );
|
||||
|
||||
if ( $blockers->{Wrongbranch} ) {
|
||||
$messages->{'Wrongbranch'} = $blockers->{Wrongbranch};
|
||||
$doreturn = 0;
|
||||
my $indexer = Koha::SearchEngine::Indexer->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
|
||||
$indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
|
||||
return ( $doreturn, $messages, $issue, $patron_unblessed );
|
||||
}
|
||||
if ( $blockers->{BlockedLost} ) {
|
||||
$doreturn = 0;
|
||||
}
|
||||
unless ($returnallowed) {
|
||||
$messages->{'Wrongbranch'} = {
|
||||
Wrongbranch => $branch,
|
||||
Rightbranch => $message
|
||||
} if $message;
|
||||
$doreturn = 0;
|
||||
my $indexer = Koha::SearchEngine::Indexer->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
|
||||
$indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
|
||||
return ( $doreturn, $messages, $issue, $patron_unblessed );
|
||||
}
|
||||
|
||||
if ( $item->itemlost and C4::Context->preference("BlockReturnOfLostItems") ) {
|
||||
$doreturn = 0;
|
||||
}
|
||||
|
||||
# case of a return of document (deal with issues and holdingbranch)
|
||||
|
|
@ -2570,7 +2494,6 @@ sub AddReturn {
|
|||
$patron or warn "AddReturn without current borrower";
|
||||
|
||||
if ($patron) {
|
||||
my $booking_id = $issue->booking_id;
|
||||
eval {
|
||||
MarkIssueReturned(
|
||||
$borrowernumber, $item->itemnumber, $return_date, $patron->privacy,
|
||||
|
|
@ -2593,13 +2516,6 @@ sub AddReturn {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($booking_id) {
|
||||
my $booking = Koha::Bookings->find($booking_id);
|
||||
if ( $booking && $booking->status eq 'issued' ) {
|
||||
$booking->status('completed')->store;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
my $error = "$@";
|
||||
carp
|
||||
|
|
@ -2769,7 +2685,7 @@ sub AddReturn {
|
|||
}
|
||||
|
||||
# find recalls...
|
||||
if ( C4::Context->preference('UseRecalls') ne "off" ) {
|
||||
if ( C4::Context->preference('UseRecalls') ) {
|
||||
|
||||
# check if this item is recallable first, which includes checking if UseRecalls syspref is enabled
|
||||
my $recall = undef;
|
||||
|
|
@ -2804,7 +2720,7 @@ sub AddReturn {
|
|||
|
||||
# Record the fact that this book was returned.
|
||||
my $categorycode = $patron_unblessed ? $patron_unblessed->{categorycode} : undef;
|
||||
Koha::Statistic->new(
|
||||
C4::Stats::UpdateStats(
|
||||
{
|
||||
branch => $branch,
|
||||
type => $stat_type,
|
||||
|
|
@ -2816,8 +2732,7 @@ sub AddReturn {
|
|||
categorycode => $categorycode,
|
||||
interface => C4::Context->interface,
|
||||
}
|
||||
)->store()
|
||||
unless ( $skip_localuse && !$issue );
|
||||
) unless ( $skip_localuse && !$issue );
|
||||
|
||||
# Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
|
||||
if ($patron) {
|
||||
|
|
@ -2860,7 +2775,7 @@ sub AddReturn {
|
|||
$request->status('RET') if $request;
|
||||
}
|
||||
|
||||
if ( C4::Context->preference('UseRecalls') ne "off" ) {
|
||||
if ( C4::Context->preference('UseRecalls') ) {
|
||||
|
||||
# all recalls that have triggered a transfer will have an allocated itemnumber
|
||||
my $transfer_recall = Koha::Recalls->find( { item_id => $item->itemnumber, status => 'in_transit' } );
|
||||
|
|
@ -3003,7 +2918,7 @@ sub MarkIssueReturned {
|
|||
my $schema = Koha::Database->schema;
|
||||
|
||||
# FIXME Improve the return value and handle it from callers
|
||||
my $rv = $schema->txn_do(
|
||||
$schema->txn_do(
|
||||
sub {
|
||||
|
||||
my $patron = Koha::Patrons->find($borrowernumber);
|
||||
|
|
@ -3015,9 +2930,7 @@ sub MarkIssueReturned {
|
|||
$issue->returndate( \'NOW()' )->store->discard_changes; # update and refetch
|
||||
}
|
||||
|
||||
my $checkin_library = defined C4::Context->userenv
|
||||
&& exists C4::Context->userenv->{'branch'} ? C4::Context->userenv->{'branch'} : $issue->branchcode;
|
||||
$issue->checkin_library($checkin_library);
|
||||
$issue->checkin_library( C4::Context->userenv->{'branch'} );
|
||||
|
||||
# Create the old_issues entry
|
||||
my $old_checkout = Koha::Old::Checkout->new( $issue->unblessed )->store;
|
||||
|
|
@ -3050,7 +2963,7 @@ sub MarkIssueReturned {
|
|||
if ( C4::Context->preference('AutoRemoveOverduesRestrictions') ne 'no' && $patron->is_debarred ) {
|
||||
my $remove_restrictions =
|
||||
C4::Context->preference('AutoRemoveOverduesRestrictions') eq 'when_no_overdue_causing_debarment'
|
||||
? !$patron->has_restricting_overdues()
|
||||
? !$patron->has_restricting_overdues( { issue_branchcode => $issue_branchcode } )
|
||||
: !$patron->has_overdues;
|
||||
if ( $remove_restrictions && $overdue_restrictions->count ) {
|
||||
DelUniqueDebarment( { borrowernumber => $borrowernumber, type => 'OVERDUES' } );
|
||||
|
|
@ -3119,7 +3032,7 @@ within the grace period).
|
|||
sub _calculate_new_debar_dt {
|
||||
my ( $patron, $item, $dt_due, $return_date ) = @_;
|
||||
|
||||
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
|
||||
my $branchcode = _GetCircControlBranch( $item, $patron );
|
||||
my $circcontrol = C4::Context->preference('CircControl');
|
||||
my $issuing_rule = Koha::CirculationRules->get_effective_rules(
|
||||
{
|
||||
|
|
@ -3182,7 +3095,7 @@ sub _calculate_new_debar_dt {
|
|||
|
||||
# Use the calendar or not to calculate the debarment date
|
||||
if ( C4::Context->preference('SuspensionsCalendar') eq 'noSuspensionsWhenClosed' ) {
|
||||
my $calendar = Koha::Library::Calendar->new(
|
||||
my $calendar = Koha::Calendar->new(
|
||||
branchcode => $branchcode,
|
||||
days_mode => 'Calendar'
|
||||
);
|
||||
|
|
@ -3306,6 +3219,44 @@ sub _FixOverduesOnReturn {
|
|||
return $result;
|
||||
}
|
||||
|
||||
=head2 _GetCircControlBranch
|
||||
|
||||
my $circ_control_branch = _GetCircControlBranch($item, $patron);
|
||||
|
||||
Internal function :
|
||||
|
||||
Return the library code to be used to determine which circulation
|
||||
policy applies to a transaction. Looks up the CircControl and
|
||||
HomeOrHoldingBranch system preferences.
|
||||
|
||||
C<$item> is an item object.
|
||||
|
||||
C<$patron> is a patron object.
|
||||
|
||||
=cut
|
||||
|
||||
sub _GetCircControlBranch {
|
||||
my ( $item, $patron ) = @_;
|
||||
my $circcontrol = C4::Context->preference('CircControl');
|
||||
my $branch;
|
||||
|
||||
if ( $circcontrol eq 'PickupLibrary' and ( C4::Context->userenv and C4::Context->userenv->{'branch'} ) ) {
|
||||
$branch = C4::Context->userenv->{'branch'};
|
||||
} elsif ( $circcontrol eq 'PatronLibrary' ) {
|
||||
$branch = $patron->branchcode;
|
||||
} else {
|
||||
my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
|
||||
$branch = $item->get_column($branchfield);
|
||||
|
||||
# default to item home branch if holdingbranch is used
|
||||
# and is not defined
|
||||
if ( !defined($branch) && $branchfield eq 'holdingbranch' ) {
|
||||
$branch = $item->homebranch;
|
||||
}
|
||||
}
|
||||
return $branch;
|
||||
}
|
||||
|
||||
=head2 GetUpcomingDueIssues
|
||||
|
||||
my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
|
||||
|
|
@ -3337,7 +3288,7 @@ sub GetUpcomingDueIssues {
|
|||
|
||||
=head2 CanBookBeRenewed
|
||||
|
||||
($ok,$error,$info) = &CanBookBeRenewed($patron, $issue, $override_limit, $cron, $date_due);
|
||||
($ok,$error,$info) = &CanBookBeRenewed($patron, $issue, $override_limit, $cron);
|
||||
|
||||
Find out whether a borrowed item may be renewed.
|
||||
|
||||
|
|
@ -3354,11 +3305,6 @@ the limit on the number of times that the loan can be renewed
|
|||
to renew sooner than "No renewal before" and to manually renew loans
|
||||
that are automatically renewed.
|
||||
|
||||
C<$date_due>, optional, is the requested due date for the renewal when
|
||||
the caller intends to renew to a specific date rather than let the
|
||||
circulation rules calculate one. It is used in place of the calculated
|
||||
due date when checking the renewal against bookings for the item.
|
||||
|
||||
C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
|
||||
item must currently be on loan to the specified borrower; renewals
|
||||
must be allowed for the item's type; and the borrower must not have
|
||||
|
|
@ -3370,7 +3316,7 @@ already renewed the loan.
|
|||
=cut
|
||||
|
||||
sub CanBookBeRenewed {
|
||||
my ( $patron, $issue, $override_limit, $cron, $date_due ) = @_;
|
||||
my ( $patron, $issue, $override_limit, $cron ) = @_;
|
||||
|
||||
my $auto_renew = "no";
|
||||
my $soonest;
|
||||
|
|
@ -3386,7 +3332,7 @@ sub CanBookBeRenewed {
|
|||
my $final_unseen_renewal = 0;
|
||||
|
||||
# override_limit will override anything else except on_reserve
|
||||
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
|
||||
my $branchcode = _GetCircControlBranch( $item, $patron );
|
||||
unless ($override_limit) {
|
||||
|
||||
( $auto_renew, $soonest ) = _CanBookBeAutoRenewed(
|
||||
|
|
@ -3456,7 +3402,7 @@ sub CanBookBeRenewed {
|
|||
|
||||
}
|
||||
|
||||
if ( C4::Context->preference('UseRecalls') ne "off" ) {
|
||||
if ( C4::Context->preference('UseRecalls') ) {
|
||||
my $recall = undef;
|
||||
$recall = $item->check_recalls if $item->can_be_waiting_recall;
|
||||
if ( defined $recall ) {
|
||||
|
|
@ -3535,10 +3481,7 @@ sub CanBookBeRenewed {
|
|||
( C4::Context->preference('ManualRenewalPeriodBase') eq 'date_due' )
|
||||
? dt_from_string( $issue->date_due, 'sql' )
|
||||
: dt_from_string();
|
||||
my $datedue =
|
||||
$date_due
|
||||
? dt_from_string($date_due)
|
||||
: CalcDateDue( $startdate, $item->effective_itemtype, $branchcode, $patron, 'is a renewal' );
|
||||
my $datedue = CalcDateDue( $startdate, $item->effective_itemtype, $branchcode, $patron, 'is a renewal' );
|
||||
if (
|
||||
my $booking = $item->find_booking(
|
||||
{ checkout_date => $startdate, due_date => $datedue, patron_id => $patron->borrowernumber }
|
||||
|
|
@ -3546,19 +3489,6 @@ sub CanBookBeRenewed {
|
|||
)
|
||||
{
|
||||
return ( 0, 'booked' ) unless ( $booking->patron_id == $patron->borrowernumber );
|
||||
|
||||
# The renewal must not run into the next booking for this item,
|
||||
# regardless of whose booking that is; the patron's own booking
|
||||
# hides any subsequent ones from the lookup above, so repeat it
|
||||
# with their booking excluded
|
||||
return ( 0, 'booked' )
|
||||
if $item->find_booking(
|
||||
{
|
||||
checkout_date => $startdate,
|
||||
due_date => $datedue,
|
||||
exclude_booking_id => $booking->booking_id,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -3567,13 +3497,17 @@ sub CanBookBeRenewed {
|
|||
|
||||
# If its cron, tell it it's too soon for a an auto renewal
|
||||
return ( 0, $auto_renew, { soonest_renew_date => $soonest } ) if $cron;
|
||||
|
||||
# Check if it's too soon for a manual renewal
|
||||
my $soonestManual = GetSoonestRenewDate( $patron, $issue );
|
||||
if ( $soonestManual > dt_from_string() ) {
|
||||
return ( 0, "too_soon", { soonest_renew_date => $soonestManual } ) unless $override_limit;
|
||||
}
|
||||
}
|
||||
|
||||
unless ($cron) {
|
||||
$soonest = GetSoonestRenewDate( $patron, $issue );
|
||||
if ( $soonest > dt_from_string() ) {
|
||||
return ( 0, "too_soon", { soonest_renew_date => $soonest } ) unless $override_limit;
|
||||
}
|
||||
$soonest = GetSoonestRenewDate( $patron, $issue );
|
||||
if ( $soonest > dt_from_string() ) {
|
||||
return ( 0, "too_soon", { soonest_renew_date => $soonest } ) unless $override_limit;
|
||||
}
|
||||
|
||||
my $auto_renew_code =
|
||||
|
|
@ -3671,8 +3605,7 @@ sub AddRenewal {
|
|||
my $patron = Koha::Patrons->find($borrowernumber) or return; # FIXME Should do more than just return
|
||||
my $patron_unblessed = $patron->unblessed;
|
||||
|
||||
my $circ_library =
|
||||
Koha::Libraries->find( Koha::Policy::Circulation->circ_control_library( $item_object, $patron ) );
|
||||
my $circ_library = Koha::Libraries->find( _GetCircControlBranch( $item_object, $patron ) );
|
||||
|
||||
my $schema = Koha::Database->schema;
|
||||
$schema->txn_do(
|
||||
|
|
@ -3750,36 +3683,6 @@ sub AddRenewal {
|
|||
error => 'Update of issue# ' . $issue->issue_id . ' failed with error: ' . $sth->errstr );
|
||||
}
|
||||
|
||||
# Keep any linked booking's end_date in sync with the renewed due date
|
||||
if ( $issue->booking_id ) {
|
||||
my $booking = Koha::Bookings->find( $issue->booking_id );
|
||||
if ( $booking && $booking->status eq 'issued' ) {
|
||||
my $booking_end = dt_from_string( $booking->end_date );
|
||||
if ( $datedue->compare($booking_end) != 0 ) {
|
||||
try {
|
||||
$booking->end_date($datedue);
|
||||
|
||||
# As at issue time, guard against inverting the booking period
|
||||
# when the new due date precedes the booking start
|
||||
my $booking_start = dt_from_string( $booking->start_date );
|
||||
$booking->start_date( dt_from_string( $issue->issuedate ) )
|
||||
if $datedue->compare($booking_start) < 0;
|
||||
$booking->store;
|
||||
} catch {
|
||||
my $error = $_;
|
||||
if ( blessed $error && $error->isa('Koha::Exceptions::Booking::Clash') ) {
|
||||
|
||||
# The renewal was explicitly allowed past a clash;
|
||||
# keep the booking's original end_date
|
||||
$booking->discard_changes;
|
||||
} else {
|
||||
die $error;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Update the renewal count on the item, and tell zebra to reindex
|
||||
$renews = ( $item_object->renewals || 0 ) + 1;
|
||||
$item_object->renewals($renews);
|
||||
|
|
@ -3847,7 +3750,7 @@ sub AddRenewal {
|
|||
)->store();
|
||||
|
||||
# Add the renewal to stats
|
||||
Koha::Statistic->new(
|
||||
C4::Stats::UpdateStats(
|
||||
{
|
||||
branch => $item_object->renewal_branchcode( { branch => $branch } ),
|
||||
type => 'renew',
|
||||
|
|
@ -3860,7 +3763,7 @@ sub AddRenewal {
|
|||
categorycode => $patron->categorycode,
|
||||
interface => C4::Context->interface,
|
||||
}
|
||||
)->store();
|
||||
);
|
||||
|
||||
#Update borrowers.lastseen
|
||||
try {
|
||||
|
|
@ -3947,7 +3850,7 @@ sub GetRenewCount {
|
|||
$unseencount = $data->{'unseen_renewals'} if $data->{'unseen_renewals'};
|
||||
|
||||
# $item and $borrower should be calculated
|
||||
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
|
||||
my $branchcode = _GetCircControlBranch( $item, $patron );
|
||||
|
||||
my $rules = Koha::CirculationRules->get_effective_rules(
|
||||
{
|
||||
|
|
@ -4008,7 +3911,7 @@ sub GetSoonestRenewDate {
|
|||
|
||||
my $dbh = C4::Context->dbh;
|
||||
|
||||
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
|
||||
my $branchcode = _GetCircControlBranch( $item, $patron );
|
||||
my $issuing_rule = Koha::CirculationRules->get_effective_rules(
|
||||
{
|
||||
categorycode => $patron->categorycode,
|
||||
|
|
@ -4076,7 +3979,7 @@ sub GetLatestAutoRenewDate {
|
|||
return unless $item;
|
||||
|
||||
my $dbh = C4::Context->dbh;
|
||||
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
|
||||
my $branchcode = _GetCircControlBranch( $item, $patron );
|
||||
|
||||
my $circulation_rules = Koha::CirculationRules->get_effective_rules(
|
||||
{
|
||||
|
|
@ -4151,7 +4054,7 @@ sub GetIssuingCharges {
|
|||
my $patron;
|
||||
if ( my $item_data = $sth->fetchrow_hashref ) {
|
||||
$item_type = $item_data->{itemtype};
|
||||
$charge = $item_data->{rentalcharge} || 0;
|
||||
$charge = $item_data->{rentalcharge};
|
||||
if ($charge) {
|
||||
|
||||
# FIXME This should follow CircControl
|
||||
|
|
@ -4484,7 +4387,7 @@ sub CalcDateDue {
|
|||
} else { # days
|
||||
$dur = DateTime::Duration->new( days => $loanlength->{$length_key} );
|
||||
}
|
||||
my $calendar = Koha::Library::Calendar->new( branchcode => $branch, days_mode => $daysmode );
|
||||
my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
|
||||
$datedue = $calendar->addDuration( $datedue, $dur, $loanlength->{lengthunit} ) if $dur;
|
||||
if ( $loanlength->{lengthunit} eq 'days' ) {
|
||||
$datedue->set_hour(23);
|
||||
|
|
@ -4522,7 +4425,7 @@ sub CalcDateDue {
|
|||
}
|
||||
}
|
||||
if ( $daysmode ne 'Days' ) {
|
||||
my $calendar = Koha::Library::Calendar->new( branchcode => $branch, days_mode => $daysmode );
|
||||
my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
|
||||
if ( $calendar->is_holiday($datedue) ) {
|
||||
|
||||
# Don't return on a closed day
|
||||
|
|
@ -5002,6 +4905,54 @@ sub IsItemIssued {
|
|||
return $sth->fetchrow;
|
||||
}
|
||||
|
||||
=head2 GetAgeRestriction
|
||||
|
||||
my $ageRestriction = GetAgeRestriction($record_restrictions);
|
||||
|
||||
@PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
|
||||
@RETURNS The age restriction age in years.
|
||||
|
||||
=cut
|
||||
|
||||
sub GetAgeRestriction {
|
||||
my ($record_restrictions) = @_;
|
||||
my $markers = C4::Context->preference('AgeRestrictionMarker');
|
||||
|
||||
return unless $record_restrictions;
|
||||
|
||||
# Split $record_restrictions to something like FSK 16 or PEGI 6
|
||||
my @values = split ' ', uc($record_restrictions);
|
||||
return unless @values;
|
||||
|
||||
# Search first occurrence of one of the markers
|
||||
my @markers = split /\|/, uc($markers);
|
||||
return unless @markers;
|
||||
|
||||
my $index = 0;
|
||||
my $restriction_year = 0;
|
||||
for my $value (@values) {
|
||||
$index++;
|
||||
for my $marker (@markers) {
|
||||
$marker =~ s/^\s+//; #remove leading spaces
|
||||
$marker =~ s/\s+$//; #remove trailing spaces
|
||||
if ( $marker eq $value ) {
|
||||
if ( $index <= $#values ) {
|
||||
$restriction_year += $values[$index];
|
||||
}
|
||||
last;
|
||||
} elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
|
||||
|
||||
# Perhaps it is something like "K16" (as in Finland)
|
||||
$restriction_year += $1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
last if ( $restriction_year > 0 );
|
||||
}
|
||||
|
||||
return $restriction_year;
|
||||
}
|
||||
|
||||
=head2 GetPendingOnSiteCheckouts
|
||||
|
||||
=cut
|
||||
|
|
|
|||
|
|
@ -19,9 +19,6 @@ package C4::ClassSplitRoutine::RegEx;
|
|||
|
||||
use Modern::Perl;
|
||||
|
||||
use Koha::Logger;
|
||||
use Koha::Regex::Replacement;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
C4::ClassSplitRoutine::RegEx - regex call number sorting key routine
|
||||
|
|
@ -44,62 +41,13 @@ sub split_callnumber {
|
|||
my ( $cn_item, $regexs ) = @_;
|
||||
|
||||
for my $regex (@$regexs) {
|
||||
_apply_substitution( \$cn_item, $regex );
|
||||
eval "\$cn_item =~ $regex"; ## no critic (StringyEval)
|
||||
}
|
||||
my @lines = split "\n", $cn_item;
|
||||
|
||||
return @lines;
|
||||
}
|
||||
|
||||
=head2 _apply_substitution
|
||||
|
||||
_apply_substitution( \$string, $regex );
|
||||
|
||||
Applies a single admin-configured s/// substitution to $string in place,
|
||||
without ever evaluating the input as code. The rule is parsed as
|
||||
C<s/PATTERN/REPLACEMENT/FLAGS> (any non-space delimiter is accepted); the pattern is compiled with
|
||||
qr// (so embedded-code assertions are treated as a runtime pattern and blocked
|
||||
by Perl), and the replacement is expanded as data via
|
||||
Koha::Regex::Replacement::expand_template. The
|
||||
/e flag is rejected and anything that is not a plain substitution is ignored.
|
||||
This replaces the previous C<eval "\$cn_item =~ $regex"> stringy eval, which
|
||||
executed arbitrary Perl.
|
||||
|
||||
=cut
|
||||
|
||||
sub _apply_substitution {
|
||||
my ( $ref, $regex ) = @_;
|
||||
|
||||
return unless defined $regex;
|
||||
|
||||
# Parse s<DELIM>PATTERN<DELIM>REPLACEMENT<DELIM>FLAGS, allowing any
|
||||
# non-space delimiter and backslash-escaped delimiters inside the pattern
|
||||
# and replacement. Anything that is not a well-formed plain substitution is
|
||||
# ignored rather than executed.
|
||||
return unless $regex =~ m{^s(\S)((?:\\.|(?!\1).)*)\1((?:\\.|(?!\1).)*)\1([a-z]*)\z}s;
|
||||
my ( $pattern, $replacement, $flags ) = ( $2, $3, $4 );
|
||||
|
||||
return if $flags =~ /e/; # never evaluate the replacement as code
|
||||
my $global = $flags =~ /g/ ? 1 : 0;
|
||||
my $ci = $flags =~ /i/ ? '(?i)' : '';
|
||||
|
||||
my $compiled = eval { qr/$ci$pattern/ };
|
||||
unless ( defined $compiled ) {
|
||||
Koha::Logger->get->warn("C4::ClassSplitRoutine::RegEx: failed to compile pattern [$pattern]: $@");
|
||||
return;
|
||||
}
|
||||
|
||||
my $expand = sub { Koha::Regex::Replacement::expand_template( $replacement, [ @{^CAPTURE} ], {%+} ) };
|
||||
|
||||
if ($global) {
|
||||
${$ref} =~ s/$compiled/$expand->()/ge;
|
||||
} else {
|
||||
${$ref} =~ s/$compiled/$expand->()/e;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
=head1 AUTHOR
|
||||
|
|
|
|||
|
|
@ -983,26 +983,6 @@ sub is_internal_PSGI_request {
|
|||
return $is_internal;
|
||||
}
|
||||
|
||||
=head3 is_opac_suppressed
|
||||
|
||||
return if C4::Context->is_opac_suppressed;
|
||||
|
||||
is_opac_suppressed returns true if and only if OPAC suppression is enabled for
|
||||
the current request, based on the sysprefs OpacSuppression and
|
||||
OpacSuppressionByIPRange.
|
||||
|
||||
=cut
|
||||
|
||||
sub is_opac_suppressed {
|
||||
return 0 unless C4::Context->preference('OpacSuppression');
|
||||
|
||||
my $IPAddress = $ENV{'REMOTE_ADDR'};
|
||||
my $IPRange = C4::Context->preference('OpacSuppressionByIPRange');
|
||||
return 1 unless $IPRange;
|
||||
|
||||
return $IPAddress !~ /^$IPRange/;
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
=head1 ENVIRONMENT
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
package C4::External::Amazon;
|
||||
|
||||
# Copyright (C) 2006 LibLime
|
||||
# <jmf at liblime dot com>
|
||||
#
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use vars qw(@ISA @EXPORT);
|
||||
|
||||
BEGIN {
|
||||
require Exporter;
|
||||
@ISA = qw(Exporter);
|
||||
@EXPORT = qw(
|
||||
get_amazon_tld
|
||||
);
|
||||
}
|
||||
|
||||
sub get_amazon_tld {
|
||||
my %tld = (
|
||||
CA => '.ca',
|
||||
DE => '.de',
|
||||
FR => '.fr',
|
||||
IN => '.in',
|
||||
JP => '.jp',
|
||||
UK => '.co.uk',
|
||||
US => '.com',
|
||||
);
|
||||
|
||||
my $locale = C4::Context->preference('AmazonLocale');
|
||||
my $tld = $tld{$locale} || '.com'; # default top level domain is .com
|
||||
return $tld;
|
||||
}
|
||||
|
||||
=head1 NAME
|
||||
|
||||
C4::External::Amazon - Functions for retrieving Amazon.com content in Koha
|
||||
|
||||
=head2 FUNCTIONS
|
||||
|
||||
This module provides facilities for retrieving Amazon.com content in Koha
|
||||
|
||||
=over
|
||||
|
||||
=item get_amazon_tld()
|
||||
|
||||
Get Amazon Top Level Domain depending on Amazon local preference: AmazonLocal.
|
||||
For example, if AmazonLocal is 'UK', returns '.co.uk'.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
1;
|
||||
__END__
|
||||
|
||||
=head1 NOTES
|
||||
|
||||
=cut
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Joshua Ferraro <jmf@liblime.com>
|
||||
|
||||
=cut
|
||||
119
C4/HoldsQueue.pm
119
C4/HoldsQueue.pm
|
|
@ -476,6 +476,7 @@ sub _allocateWithTransportCostMatrix {
|
|||
|
||||
my @m = map { [ (undef) x $num_tasks ] } ( 1 .. $num_agents );
|
||||
|
||||
my $inf = -1; # Initially represent infinity with a negative value.
|
||||
my $max = 0;
|
||||
|
||||
# If some candidate holds requests cannot be filled and there are
|
||||
|
|
@ -492,7 +493,6 @@ sub _allocateWithTransportCostMatrix {
|
|||
|
||||
RETRY:
|
||||
while (1) {
|
||||
my $inf = -1; # Initially represent infinity with a negative value.
|
||||
return [] if $num_agents == 0 || $num_tasks + scalar(@remaining) == 0;
|
||||
|
||||
if ( $num_tasks < $num_agents && @remaining ) {
|
||||
|
|
@ -668,32 +668,20 @@ RETRY:
|
|||
|
||||
if ( $retries-- > 0 && @unallocated && @remaining ) {
|
||||
|
||||
# @unallocated is built by iterating agents (rows), so its
|
||||
# entries are not necessarily in column order: Munkres'
|
||||
# row-to-column assignment is a permutation, not a monotonic
|
||||
# mapping. The matrix compaction below scans columns in
|
||||
# ascending order and expects to consume @unallocated in that
|
||||
# same order, so it must be sorted first.
|
||||
my @unallocated_sorted = sort { $a <=> $b } @unallocated;
|
||||
|
||||
# Remove the transport cost of unfilled holds and compact the matrix.
|
||||
# Also remove the hold request from the array.
|
||||
for ( my $i = 0 ; $i < $num_agents ; $i++ ) {
|
||||
my $u = 0;
|
||||
for ( my $j = 0 ; $j < $num_tasks ; $j++ ) {
|
||||
if ( $u < scalar(@unallocated_sorted) && $unallocated_sorted[$u] == $j ) {
|
||||
if ( $u < scalar(@unallocated) && $unallocated[$u] == $j ) {
|
||||
$u++;
|
||||
} elsif ( $u > 0 ) {
|
||||
$m[$i][ $j - $u ] = $m[$i][$j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Indices must be spliced out in reverse order, otherwise removing
|
||||
# a lower index shifts the position of any higher index still to
|
||||
# be removed.
|
||||
for my $u ( reverse @unallocated_sorted ) {
|
||||
splice @requests, $u, 1;
|
||||
for ( my $u = 0 ; $u < scalar(@unallocated) ; $u++ ) {
|
||||
splice @requests, $unallocated[$u], 1;
|
||||
}
|
||||
$num_tasks = scalar(@requests);
|
||||
|
||||
|
|
@ -725,8 +713,7 @@ RETRY:
|
|||
about the hold it has been mapped to.
|
||||
|
||||
This routine attempts to match the holds in the following priority
|
||||
1 - If local holds priority is enabled, and LocalHoldsPriorityScope includes the holds queue,
|
||||
we check all requests to see if local matches can be found
|
||||
1 - If local holds priority is enabled we check all requests to see if local matches can be found
|
||||
2 - We check for item level matches and fill those
|
||||
3 - We now loop the remaining requests in priority order attempting to fill with
|
||||
a - Items where HoldsQueuePrioritizeBranch matches either from items held at the pickup branch, or at the least cost branch (if Transport Cost Matrix is being used)
|
||||
|
|
@ -761,9 +748,8 @@ sub MapItemsToHoldRequests {
|
|||
|
||||
# Look for Local Holds Priority matches first
|
||||
|
||||
my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
|
||||
my $LocalHoldsPriorityScope = C4::Context->preference('LocalHoldsPriorityScope');
|
||||
if ( $LocalHoldsPriority ne 'None' && $LocalHoldsPriorityScope eq 'capture_and_queue' ) {
|
||||
my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
|
||||
if ( $LocalHoldsPriority ne 'None' ) {
|
||||
my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
|
||||
my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
|
||||
foreach my $request (@$hold_requests) {
|
||||
|
|
@ -911,7 +897,6 @@ sub MapItemsToHoldRequests {
|
|||
$item_map{ $allocation->[0] } = $allocation->[1];
|
||||
$num_items_remaining--;
|
||||
}
|
||||
_flag_local_holdgroup_matches( \%item_map, \%items_by_itemnumber );
|
||||
return \%item_map;
|
||||
}
|
||||
|
||||
|
|
@ -923,10 +908,7 @@ sub MapItemsToHoldRequests {
|
|||
push @{ $items_by_branch{ $item->{holdingbranch} } }, $item
|
||||
unless exists $allocated_items{ $item->{itemnumber} };
|
||||
}
|
||||
unless ( keys %items_by_branch ) {
|
||||
_flag_local_holdgroup_matches( \%item_map, \%items_by_itemnumber );
|
||||
return \%item_map;
|
||||
}
|
||||
return \%item_map unless keys %items_by_branch;
|
||||
|
||||
# now handle the title-level requests
|
||||
$num_items_remaining = scalar(@$available_items) - scalar( keys %allocated_items );
|
||||
|
|
@ -1038,63 +1020,9 @@ sub MapItemsToHoldRequests {
|
|||
$num_items_remaining--;
|
||||
}
|
||||
}
|
||||
_flag_local_holdgroup_matches( \%item_map, \%items_by_itemnumber );
|
||||
return \%item_map;
|
||||
}
|
||||
|
||||
=head2 _flag_local_holdgroup_matches
|
||||
|
||||
_flag_local_holdgroup_matches( $item_map, $items_by_itemnumber );
|
||||
|
||||
For each mapped item that does not already have local_holdgroup_match set,
|
||||
check whether the item and the patron belong to the same library hold group
|
||||
using the LocalHoldsExclusivity* system preferences. This runs independently
|
||||
of LocalHoldsPriority so that the exclusivity feature can operate on its own.
|
||||
|
||||
=cut
|
||||
|
||||
sub _flag_local_holdgroup_matches {
|
||||
my ( $item_map, $items_by_itemnumber ) = @_;
|
||||
|
||||
my $excl_period = C4::Context->preference('LocalHoldsExclusivityPeriod');
|
||||
return unless $excl_period;
|
||||
|
||||
my $patron_control = C4::Context->preference('LocalHoldsExclusivityPatronControl') || 'PickupLibrary';
|
||||
my $item_control = C4::Context->preference('LocalHoldsExclusivityItemControl') || 'holdingbranch';
|
||||
|
||||
foreach my $itemnumber ( keys %$item_map ) {
|
||||
my $mapped = $item_map->{$itemnumber};
|
||||
next if $mapped->{local_holdgroup_match};
|
||||
|
||||
my $item_branchcode =
|
||||
$items_by_itemnumber->{$itemnumber}
|
||||
? $items_by_itemnumber->{$itemnumber}->{$item_control}
|
||||
: undef;
|
||||
|
||||
unless ($item_branchcode) {
|
||||
my $item_obj = Koha::Items->find($itemnumber);
|
||||
$item_branchcode = $item_obj->$item_control if $item_obj;
|
||||
}
|
||||
next unless $item_branchcode;
|
||||
|
||||
my $patron_branchcode =
|
||||
( $patron_control eq 'PickupLibrary' ) ? $mapped->{pickup_branch}
|
||||
: ( $patron_control eq 'HomeLibrary' ) ? Koha::Patrons->find( $mapped->{borrowernumber} )->branchcode
|
||||
: undef;
|
||||
next unless $patron_branchcode;
|
||||
|
||||
if ( $item_branchcode eq $patron_branchcode ) {
|
||||
$mapped->{local_holdgroup_match} = 1;
|
||||
next;
|
||||
}
|
||||
|
||||
my $item_library = Koha::Libraries->find( { branchcode => $item_branchcode } );
|
||||
if ( $item_library && $item_library->validate_hold_sibling( { branchcode => $patron_branchcode } ) ) {
|
||||
$mapped->{local_holdgroup_match} = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
=head2 _can_item_fill_request
|
||||
|
||||
my $bool = _can_item_fill_request( $item, $request, $libraries );
|
||||
|
|
@ -1146,20 +1074,19 @@ sub CreatePicklistFromItemMap {
|
|||
my $sth_load = $dbh->prepare( "
|
||||
INSERT INTO tmp_holdsqueue (biblionumber,itemnumber,barcode,surname,firstname,phone,borrowernumber,
|
||||
cardnumber,reservedate,title, itemcallnumber,
|
||||
holdingbranch,pickbranch,notes, hold_group_id, item_level_request, local_holdgroup_match)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
holdingbranch,pickbranch,notes, hold_group_id, item_level_request)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
" );
|
||||
|
||||
foreach my $itemnumber ( sort keys %$item_map ) {
|
||||
my $mapped_item = $item_map->{$itemnumber};
|
||||
my $biblionumber = $mapped_item->{biblionumber};
|
||||
my $borrowernumber = $mapped_item->{borrowernumber};
|
||||
my $pickbranch = $mapped_item->{pickup_branch};
|
||||
my $holdingbranch = $mapped_item->{holdingbranch};
|
||||
my $reservedate = $mapped_item->{reservedate};
|
||||
my $reservenotes = $mapped_item->{reservenotes};
|
||||
my $item_level = $mapped_item->{item_level};
|
||||
my $local_holdgroup_match = $mapped_item->{local_holdgroup_match} ? 1 : 0;
|
||||
my $mapped_item = $item_map->{$itemnumber};
|
||||
my $biblionumber = $mapped_item->{biblionumber};
|
||||
my $borrowernumber = $mapped_item->{borrowernumber};
|
||||
my $pickbranch = $mapped_item->{pickup_branch};
|
||||
my $holdingbranch = $mapped_item->{holdingbranch};
|
||||
my $reservedate = $mapped_item->{reservedate};
|
||||
my $reservenotes = $mapped_item->{reservenotes};
|
||||
my $item_level = $mapped_item->{item_level};
|
||||
|
||||
my $item = Koha::Items->find($itemnumber);
|
||||
my $barcode = $item->barcode;
|
||||
|
|
@ -1184,7 +1111,7 @@ sub CreatePicklistFromItemMap {
|
|||
$sth_load->execute(
|
||||
$biblionumber, $itemnumber, $barcode, $surname, $firstname, $phone, $borrowernumber,
|
||||
$cardnumber, $reservedate, $title, $itemcallnumber,
|
||||
$holdingbranch, $pickbranch, $reservenotes, $hold_group_id, $item_level, $local_holdgroup_match
|
||||
$holdingbranch, $pickbranch, $reservenotes, $hold_group_id, $item_level
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1200,7 +1127,7 @@ sub AddToHoldTargetMap {
|
|||
my $schema = Koha::Database->new->schema;
|
||||
|
||||
my $insert_sql = q(
|
||||
INSERT INTO hold_fill_targets (borrowernumber, biblionumber, itemnumber, source_branchcode, item_level_request, reserve_id, local_holdgroup_match) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO hold_fill_targets (borrowernumber, biblionumber, itemnumber, source_branchcode, item_level_request, reserve_id) VALUES (?, ?, ?, ?, ?, ?)
|
||||
);
|
||||
my $sth_insert = $dbh->prepare($insert_sql);
|
||||
|
||||
|
|
@ -1211,8 +1138,7 @@ sub AddToHoldTargetMap {
|
|||
$dbh->do( 'DELETE FROM hold_fill_targets WHERE itemnumber = ?', {}, $itemnumber );
|
||||
$sth_insert->execute(
|
||||
$mapped_item->{borrowernumber}, $mapped_item->{biblionumber}, $itemnumber,
|
||||
$mapped_item->{holdingbranch}, $mapped_item->{item_level}, $mapped_item->{reserve_id},
|
||||
$mapped_item->{local_holdgroup_match} ? 1 : 0,
|
||||
$mapped_item->{holdingbranch}, $mapped_item->{item_level}, $mapped_item->{reserve_id}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -1259,8 +1185,7 @@ sub load_branches_to_pull_from {
|
|||
|
||||
my $today = dt_from_string();
|
||||
if ( C4::Context->preference('HoldsQueueSkipClosed') ) {
|
||||
@branches_to_use =
|
||||
grep { !Koha::Library::Calendar->new( branchcode => $_ )->is_holiday($today) } @branches_to_use;
|
||||
@branches_to_use = grep { !Koha::Calendar->new( branchcode => $_ )->is_holiday($today) } @branches_to_use;
|
||||
}
|
||||
|
||||
return \@branches_to_use;
|
||||
|
|
|
|||
48
C4/Items.pm
48
C4/Items.pm
|
|
@ -38,7 +38,6 @@ BEGIN {
|
|||
SearchItems
|
||||
PrepareItemrecordDisplay
|
||||
ToggleNewStatus
|
||||
GetAutomaticItemModificationAgeFields
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +58,6 @@ use Try::Tiny qw( catch try );
|
|||
use Koha::AuthorisedValues;
|
||||
use Koha::DateUtils qw( dt_from_string );
|
||||
use Koha::Database;
|
||||
use Koha::Logger;
|
||||
|
||||
use Koha::Biblios;
|
||||
use Koha::Biblioitems;
|
||||
|
|
@ -1657,25 +1655,6 @@ sub PrepareItemrecordDisplay {
|
|||
};
|
||||
}
|
||||
|
||||
=head2 GetAutomaticItemModificationAgeFields
|
||||
|
||||
my @fields = GetAutomaticItemModificationAgeFields();
|
||||
|
||||
Returns the fixed, C<items.>-prefixed list of date columns that are valid
|
||||
"age field" values for automatic item modification rules (see
|
||||
ToggleNewStatus below). This is the single source of truth for that
|
||||
allow-list; it is shared with F<tools/automatic_item_modification_by_age.pl>
|
||||
so the two cannot drift apart.
|
||||
|
||||
=cut
|
||||
|
||||
sub GetAutomaticItemModificationAgeFields {
|
||||
return (
|
||||
'items.dateaccessioned', 'items.replacementpricedate', 'items.datelastborrowed', 'items.datelastseen',
|
||||
'items.damaged_on', 'items.itemlost_on', 'items.withdrawn_on'
|
||||
);
|
||||
}
|
||||
|
||||
=head2 ToggleNewStatus
|
||||
|
||||
Missing POD for ToggleNewStatus.
|
||||
|
|
@ -1692,32 +1671,17 @@ sub ToggleNewStatus {
|
|||
my @item_columns = map { "items.$_" } Koha::Items->columns;
|
||||
my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
|
||||
my @biblio_columns = map { "biblio.$_" } Koha::Biblios->columns;
|
||||
my @age_fields = GetAutomaticItemModificationAgeFields();
|
||||
my $report;
|
||||
for my $rule (@rules) {
|
||||
my $age = $rule->{age};
|
||||
|
||||
# Default to using items.dateaccessioned if there's an old item modification rule
|
||||
# missing an agefield value, or if the stored value is not (or is no longer) a
|
||||
# recognised age field - values are only trusted from a fixed allow-list since
|
||||
# this is spliced directly into the SQL below and cannot be bound as a placeholder.
|
||||
my $agefield = $rule->{agefield} // '';
|
||||
unless ( grep { $_ eq $agefield } @age_fields ) {
|
||||
Koha::Logger->get->warn(qq{Rejecting invalid agefield "$agefield" in automatic item modification rule})
|
||||
if length $agefield;
|
||||
$agefield = 'items.dateaccessioned';
|
||||
}
|
||||
# missing an agefield value
|
||||
my $agefield = $rule->{agefield} ? $rule->{agefield} : 'items.dateaccessioned';
|
||||
my $conditions = $rule->{conditions};
|
||||
my $substitutions = $rule->{substitutions};
|
||||
foreach my $substitution (@$substitutions) {
|
||||
my $field = $substitution->{field} // '';
|
||||
if ( grep { $_ eq $field } @item_columns ) {
|
||||
( $substitution->{item_field} ) = ( $field =~ /^items\.(.*)/ );
|
||||
} else {
|
||||
Koha::Logger->get->warn(
|
||||
qq{Rejecting invalid substitution field "$field" in automatic item modification rule})
|
||||
if length $field;
|
||||
}
|
||||
foreach (@$substitutions) {
|
||||
( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
|
||||
}
|
||||
my @params;
|
||||
|
||||
|
|
@ -1742,9 +1706,6 @@ sub ToggleNewStatus {
|
|||
$query .= qq| AND $condition->{field} = ?|;
|
||||
push @params, $condition->{value};
|
||||
}
|
||||
} else {
|
||||
Koha::Logger->get->warn(
|
||||
qq{Rejecting invalid condition field "$condition->{field}" in automatic item modification rule});
|
||||
}
|
||||
}
|
||||
if ( defined $age ) {
|
||||
|
|
@ -1761,7 +1722,6 @@ sub ToggleNewStatus {
|
|||
my $field = $substitution->{item_field};
|
||||
my $value = $substitution->{value};
|
||||
next unless $substitution->{field};
|
||||
next unless defined $field;
|
||||
next
|
||||
if ( defined $values->{ $substitution->{item_field} }
|
||||
and $values->{ $substitution->{item_field} } eq $substitution->{value} );
|
||||
|
|
|
|||
149
C4/Koha.pm
149
C4/Koha.pm
|
|
@ -33,7 +33,10 @@ BEGIN {
|
|||
getitemtypeimagesrc
|
||||
getitemtypeimagelocation
|
||||
GetAuthorisedValues
|
||||
GetNormalizedUPC
|
||||
GetNormalizedISBN
|
||||
GetNormalizedEAN
|
||||
GetNormalizedOCLCNumber
|
||||
xml_escape
|
||||
|
||||
GetVariationsOfISBN
|
||||
|
|
@ -505,51 +508,37 @@ C<$opac> If set to a true value, displays OPAC descriptions rather than normal o
|
|||
sub GetAuthorisedValues {
|
||||
my $category = shift // ''; # optional parameter
|
||||
my $opac = shift ? 1 : 0; # normalise to be safe
|
||||
my $options = shift // '';
|
||||
my $no_limit;
|
||||
if ($options) { $no_limit = $options->{'no_limit'}; } #optional parameter to ignore library limitation
|
||||
$no_limit = $no_limit ? 1 : 0;
|
||||
|
||||
# Is this cached already?
|
||||
my $branch_limit = C4::Context::mybranch();
|
||||
my $cache_key = "AuthorisedValues-$category-$opac-$branch_limit-$no_limit";
|
||||
my $cache_key = "AuthorisedValues-$category-$opac-$branch_limit";
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $result = $cache->get_from_cache($cache_key);
|
||||
return $result if $result;
|
||||
|
||||
my @results;
|
||||
my $dbh = C4::Context->dbh;
|
||||
|
||||
my $select_clause = 'SELECT av.*';
|
||||
my @where_args;
|
||||
if ( $branch_limit && $no_limit ) {
|
||||
$select_clause .= ', MIN(IF (branchcode = ? OR branchcode IS NULL, 0, 1)) as restricted';
|
||||
push @where_args, $branch_limit;
|
||||
}
|
||||
|
||||
my $dbh = C4::Context->dbh;
|
||||
my $query = qq{
|
||||
$select_clause
|
||||
SELECT DISTINCT av.*
|
||||
FROM authorised_values av
|
||||
};
|
||||
$query .= qq{
|
||||
LEFT JOIN authorised_values_branches ON ( id = av_id )
|
||||
} if $branch_limit;
|
||||
my @where_strings;
|
||||
my @where_args;
|
||||
|
||||
if ($category) {
|
||||
push @where_strings, "category = ?";
|
||||
push @where_args, $category;
|
||||
}
|
||||
|
||||
if ( $branch_limit && !$no_limit ) {
|
||||
if ($branch_limit) {
|
||||
push @where_strings, "( branchcode = ? OR branchcode IS NULL )";
|
||||
push @where_args, $branch_limit;
|
||||
}
|
||||
|
||||
if ( @where_strings > 0 ) {
|
||||
$query .= " WHERE " . join( " AND ", @where_strings );
|
||||
}
|
||||
$query .= ' GROUP BY av.id, av.category, av.authorised_value, av.lib, av.lib_opac, av.imageurl'
|
||||
if $branch_limit && $no_limit;
|
||||
$query .= ' ORDER BY category, '
|
||||
. (
|
||||
$opac
|
||||
|
|
@ -612,6 +601,39 @@ sub display_marc_indicators {
|
|||
return $indicators;
|
||||
}
|
||||
|
||||
=head2 GetNormalizedUPC
|
||||
|
||||
Missing POD for GetNormalizedUPC.
|
||||
|
||||
=cut
|
||||
|
||||
sub GetNormalizedUPC {
|
||||
my ( $marcrecord, $marcflavour ) = @_;
|
||||
|
||||
$marcflavour ||= C4::Context->preference('marcflavour');
|
||||
|
||||
return unless $marcrecord;
|
||||
if ( $marcflavour eq 'UNIMARC' ) {
|
||||
my @fields = $marcrecord->field('072');
|
||||
foreach my $field (@fields) {
|
||||
my $upc = _normalize_match_point( $field->subfield('a') );
|
||||
if ($upc) {
|
||||
return $upc;
|
||||
}
|
||||
}
|
||||
|
||||
} else { # assume marc21 if not unimarc
|
||||
my @fields = $marcrecord->field('024');
|
||||
foreach my $field (@fields) {
|
||||
my $indicator = $field->indicator(1);
|
||||
my $upc = _normalize_match_point( $field->subfield('a') );
|
||||
if ( $upc && $indicator eq '1' ) {
|
||||
return $upc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Normalizes and returns the first valid ISBN found in the record
|
||||
# ISBN13 are converted into ISBN10. This is required to get some book cover images.
|
||||
|
||||
|
|
@ -652,6 +674,64 @@ sub GetNormalizedISBN {
|
|||
}
|
||||
}
|
||||
|
||||
=head2 GetNormalizedEAN
|
||||
|
||||
Missing POD for GetNormalizedEAN.
|
||||
|
||||
=cut
|
||||
|
||||
sub GetNormalizedEAN {
|
||||
my ( $marcrecord, $marcflavour ) = @_;
|
||||
|
||||
return unless $marcrecord;
|
||||
|
||||
if ( $marcflavour eq 'UNIMARC' ) {
|
||||
my @fields = $marcrecord->field('073');
|
||||
foreach my $field (@fields) {
|
||||
my $ean = _normalize_match_point( $field->subfield('a') );
|
||||
if ($ean) {
|
||||
return $ean;
|
||||
}
|
||||
}
|
||||
} else { # assume marc21 if not unimarc
|
||||
my @fields = $marcrecord->field('024');
|
||||
foreach my $field (@fields) {
|
||||
my $indicator = $field->indicator(1);
|
||||
my $ean = _normalize_match_point( $field->subfield('a') );
|
||||
if ( $ean && $indicator eq '3' ) {
|
||||
return $ean;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
=head2 GetNormalizedOCLCNumber
|
||||
|
||||
Missing POD for GetNormalizedOCLCNumber.
|
||||
|
||||
=cut
|
||||
|
||||
sub GetNormalizedOCLCNumber {
|
||||
my ( $marcrecord, $marcflavour ) = @_;
|
||||
return unless $marcrecord;
|
||||
|
||||
$marcflavour ||= C4::Context->preference('marcflavour');
|
||||
|
||||
if ( $marcflavour ne 'UNIMARC' ) {
|
||||
my @fields = $marcrecord->field('035');
|
||||
foreach my $field (@fields) {
|
||||
my $oclc = $field->subfield('a');
|
||||
if ( $oclc && $oclc =~ /OCoLC/ ) {
|
||||
$oclc =~ s/\(OCoLC\)//;
|
||||
return $oclc;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
# TODO for UNIMARC
|
||||
}
|
||||
}
|
||||
|
||||
sub _normalize_match_point {
|
||||
my $match_point = shift;
|
||||
( my $normalized_match_point ) = $match_point =~ /([\d-]*[X]*)/;
|
||||
|
|
@ -662,28 +742,13 @@ sub _normalize_match_point {
|
|||
|
||||
sub _isbn_cleanup {
|
||||
my ($isbn) = @_;
|
||||
|
||||
return unless $isbn;
|
||||
|
||||
if ( $isbn =~ /^979/ ) {
|
||||
|
||||
# ISBNs starting with 979 cannot be converted to ISBN-10 format, but all other valid ISBNs can
|
||||
return NormalizeISBN(
|
||||
{
|
||||
isbn => $isbn,
|
||||
format => 'ISBN-13',
|
||||
strip_hyphens => 1,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return NormalizeISBN(
|
||||
{
|
||||
isbn => $isbn,
|
||||
format => 'ISBN-10',
|
||||
strip_hyphens => 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
return NormalizeISBN(
|
||||
{
|
||||
isbn => $isbn,
|
||||
format => 'ISBN-10',
|
||||
strip_hyphens => 1,
|
||||
}
|
||||
) if $isbn;
|
||||
}
|
||||
|
||||
=head2 NormalizeISBN
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ BEGIN {
|
|||
|
||||
use Carp qw( carp );
|
||||
use CGI;
|
||||
use List::MoreUtils qw( any uniq );
|
||||
use List::MoreUtils qw( any );
|
||||
use C4::Context;
|
||||
use Koha::Caches;
|
||||
use Koha::Cache::Memory::Lite;
|
||||
|
|
@ -123,10 +123,6 @@ sub getTranslatedLanguages {
|
|||
} elsif ( $interface eq 'opac' ) {
|
||||
@enabled_languages = split ",", C4::Context->preference('OPACLanguages');
|
||||
}
|
||||
} else {
|
||||
@enabled_languages = split ",",
|
||||
join( ',', C4::Context->preference( 'StaffInterfaceLanguages', C4::Context->preference('OPACLanguages') ) );
|
||||
@enabled_languages = uniq @enabled_languages;
|
||||
}
|
||||
|
||||
my $cache = Koha::Caches->get_instance;
|
||||
|
|
@ -156,7 +152,9 @@ sub getTranslatedLanguages {
|
|||
$htdocs = C4::Context->config('opachtdocs');
|
||||
push @languages, _get_opac_language_dirs($htdocs);
|
||||
|
||||
@languages = uniq @languages;
|
||||
my %seen;
|
||||
$seen{$_}++ for @languages;
|
||||
@languages = keys %seen;
|
||||
}
|
||||
return _build_languages_arrayref( \@languages, $current_language, \@enabled_languages );
|
||||
}
|
||||
|
|
@ -371,16 +369,14 @@ FIXME: this could be rewritten and simplified using map
|
|||
|
||||
sub _build_languages_arrayref {
|
||||
my ( $translated_languages, $current_language, $enabled_languages ) = @_;
|
||||
|
||||
$current_language //= '';
|
||||
my @translated_languages = @$translated_languages;
|
||||
my @languages_loop; # the final reference to an array of hashrefs
|
||||
my @enabled_languages = @$enabled_languages;
|
||||
|
||||
# how many languages are enabled, if one, take note, some contexts won't need to display it
|
||||
my $language_groups = {};
|
||||
my $track_language_groups = {};
|
||||
|
||||
my $language_groups;
|
||||
my $track_language_groups;
|
||||
my $current_language_regex = regex_lang_subtags($current_language);
|
||||
|
||||
# Loop through the translated languages
|
||||
|
|
@ -447,15 +443,9 @@ sub _build_languages_arrayref {
|
|||
}
|
||||
}
|
||||
}
|
||||
if ( exists $idx{$aa} and exists $idx{$bb} ) {
|
||||
return $idx{$aa} cmp $idx{$bb};
|
||||
} elsif ( exists $idx{$aa} ) {
|
||||
return -1;
|
||||
} elsif ( exists $idx{$bb} ) {
|
||||
return 1;
|
||||
} else {
|
||||
return $a cmp $b; # Finally order by lang code
|
||||
}
|
||||
( exists $idx{$aa} and exists $idx{$bb} and ( $idx{$aa} cmp $idx{$bb} ) )
|
||||
|| ( exists $idx{$aa} and exists $idx{$bb} )
|
||||
|| exists $idx{$bb}
|
||||
} keys %$language_groups;
|
||||
|
||||
for my $key (@ordered_keys) {
|
||||
|
|
|
|||
|
|
@ -1032,7 +1032,6 @@ sub EnqueueLetter {
|
|||
message_id => $id,
|
||||
borrowernumber => $who_letter_is_for,
|
||||
letter_code => $letter_code, # can be scalar or arrayref
|
||||
exclude_letter_code => $exclude_letter_code, # can be scalar or arrayref
|
||||
type => $type, # can be scalar or arrayref
|
||||
limit => 50,
|
||||
verbose => 1,
|
||||
|
|
@ -1045,10 +1044,6 @@ The (optional) message_id, borrowernumber, letter_code, type and where
|
|||
parameter are used to select which pending messages will be processed. The
|
||||
limit parameter determines the volume of results, i.e. sent messages.
|
||||
|
||||
The optional exclude_letter_code parameter processes all pending messages
|
||||
except those with the given letter code(s). It cannot be combined with
|
||||
letter_code; passing both raises a Koha::Exceptions::BadParameter exception.
|
||||
|
||||
The optional verbose parameter can be used to generate debugging output.
|
||||
|
||||
Returns number of messages sent.
|
||||
|
|
@ -1063,18 +1058,6 @@ sub SendQueuedMessages {
|
|||
Koha::Exceptions::BadParameter->throw("Parameter message_id cannot be empty if passed.")
|
||||
if ( exists( $params->{message_id} ) && !$params->{message_id} );
|
||||
|
||||
my $has_letter_code =
|
||||
ref( $params->{letter_code} ) ? scalar @{ $params->{letter_code} } : $params->{letter_code};
|
||||
my $has_exclude_letter_code =
|
||||
ref( $params->{exclude_letter_code} )
|
||||
? scalar @{ $params->{exclude_letter_code} }
|
||||
: $params->{exclude_letter_code};
|
||||
|
||||
# letter_code and exclude_letter_code build the same search key ('letter_code'); passing both
|
||||
# would let one silently clobber the other in the hash literal built below.
|
||||
Koha::Exceptions::BadParameter->throw("Parameters letter_code and exclude_letter_code are mutually exclusive.")
|
||||
if ( $has_letter_code && $has_exclude_letter_code );
|
||||
|
||||
if ( C4::Context->config("enable_plugins") ) {
|
||||
my @plugins = Koha::Plugins->new->GetPlugins(
|
||||
{
|
||||
|
|
@ -1101,17 +1084,10 @@ sub SendQueuedMessages {
|
|||
$params->{message_id} ? ( message_id => $params->{message_id} ) : (),
|
||||
$params->{borrowernumber} ? ( borrowernumber => $params->{borrowernumber} ) : (),
|
||||
|
||||
# letter_code and exclude_letter_code can each be scalar or arrayref; guaranteed
|
||||
# mutually exclusive above, so they never both contribute a letter_code key here.
|
||||
$has_letter_code ? ( letter_code => $params->{letter_code} ) : (),
|
||||
$has_exclude_letter_code
|
||||
? (
|
||||
letter_code => ref( $params->{exclude_letter_code} )
|
||||
? { '-not_in' => $params->{exclude_letter_code} }
|
||||
: { '!=' => $params->{exclude_letter_code} }
|
||||
)
|
||||
: (),
|
||||
|
||||
# Check for scalar or array in letter_code and type
|
||||
ref( $params->{letter_code} )
|
||||
&& @{ $params->{letter_code} } ? ( letter_code => $params->{letter_code} ) : (),
|
||||
!ref( $params->{letter_code} ) && $params->{letter_code} ? ( letter_code => $params->{letter_code} ) : (),
|
||||
ref( $params->{type} )
|
||||
&& @{ $params->{type} }
|
||||
? ( message_transport_type => $params->{type} )
|
||||
|
|
|
|||
30
C4/Log.pm
30
C4/Log.pm
|
|
@ -38,7 +38,7 @@ use vars qw(@ISA @EXPORT);
|
|||
BEGIN {
|
||||
require Exporter;
|
||||
@ISA = qw(Exporter);
|
||||
@EXPORT = qw(logaction cronlogaction logscriptaction);
|
||||
@EXPORT = qw(logaction cronlogaction);
|
||||
}
|
||||
|
||||
=head1 NAME
|
||||
|
|
@ -215,41 +215,17 @@ Logs the path and name of the calling script plus the information provided by pa
|
|||
|
||||
=cut
|
||||
|
||||
#'
|
||||
sub cronlogaction {
|
||||
my $params = shift;
|
||||
my $info = $params->{info};
|
||||
my $action = $params->{action};
|
||||
$action ||= "Run";
|
||||
|
||||
# $0 is a special Perl variable containing the program that is currently running
|
||||
# https://perldoc.perl.org/variables/$0
|
||||
my $loginfo = $0;
|
||||
|
||||
my $loginfo = ( caller(0) )[1];
|
||||
$loginfo .= ' ' . $info if $info;
|
||||
logaction( 'CRONJOBS', $action, $$, $loginfo ) if C4::Context->preference('CronjobLog');
|
||||
}
|
||||
|
||||
=item logscriptaction
|
||||
|
||||
&logscriptaction({ info => $info, action => $action });
|
||||
|
||||
Convenience routine to add a record into the action_logs table for a
|
||||
non-cron Koha script. Logs the path of the running script plus any
|
||||
additional information provided in C<info>. Only logs when the
|
||||
C<ScriptLog> system preference is enabled.
|
||||
|
||||
=cut
|
||||
|
||||
sub logscriptaction {
|
||||
my $params = shift;
|
||||
my $info = $params->{info};
|
||||
my $action = $params->{action};
|
||||
$action ||= "Run";
|
||||
my $loginfo = $0;
|
||||
$loginfo .= ' ' . $info if $info;
|
||||
logaction( 'SCRIPTS', $action, $$, $loginfo ) if C4::Context->preference('ScriptLog');
|
||||
}
|
||||
|
||||
1;
|
||||
__END__
|
||||
|
||||
|
|
|
|||
|
|
@ -640,7 +640,7 @@ sub ModifyRecordWithTemplate {
|
|||
{
|
||||
record => $record,
|
||||
field => $from_field,
|
||||
subfield => $from_field < 10 ? undef : $from_subfield,
|
||||
subfield => $from_subfield,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ The following will be set where applicable:
|
|||
$flags->{ ODUES }->{itemlist} ref-to-array: list of overdue books
|
||||
$flags->{ ODUES }->{itemlisttext} Text list of overdue items -- deprecated
|
||||
|
||||
$flags->{WAITING} Set if any of patron's reserves are available
|
||||
$flags->{WAITING}->{message} Message -- deprecated
|
||||
$flags->{WAITING}->{itemlist} ref-to-array: list of available items
|
||||
|
||||
=over
|
||||
|
||||
=item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
|
||||
|
|
@ -104,6 +108,10 @@ biblioitems, and items tables of the Koha database.
|
|||
=item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
|
||||
the overdue items, one per line. Deprecated.
|
||||
|
||||
=item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
|
||||
available items. Each element is a reference-to-hash whose keys are
|
||||
fields from the reserves table of the Koha database.
|
||||
|
||||
=back
|
||||
|
||||
All the "message" fields that include language generated in this function are deprecated,
|
||||
|
|
@ -195,6 +203,14 @@ sub patronflags {
|
|||
$flags{'ODUES'} = \%flaginfo;
|
||||
}
|
||||
|
||||
my $waiting_holds = $patron->holds->search( { found => 'W' } );
|
||||
my $nowaiting = $waiting_holds->count;
|
||||
if ( $nowaiting > 0 ) {
|
||||
my %flaginfo;
|
||||
$flaginfo{'message'} = "Reserved items available";
|
||||
$flaginfo{'itemlist'} = $waiting_holds->unblessed;
|
||||
$flags{'WAITING'} = \%flaginfo;
|
||||
}
|
||||
return ( \%flags );
|
||||
}
|
||||
|
||||
|
|
|
|||
20
C4/Output.pm
20
C4/Output.pm
|
|
@ -438,7 +438,8 @@ sub parametrized_url {
|
|||
|
||||
=item redirect_if_opac_suppressed
|
||||
|
||||
redirect_if_opac_suppressed( $query, $biblio );
|
||||
redirect_if_opac_suppressed( $query, $biblio )
|
||||
if C4::Context->preference('OpacSuppression');
|
||||
|
||||
For a given I<Koha::Biblio> object, it handles redirection if it is suppressed
|
||||
from the OPAC.
|
||||
|
|
@ -455,9 +456,20 @@ sub redirect_if_opac_suppressed {
|
|||
} else {
|
||||
$redirect_url = "/cgi-bin/koha/errors/404.pl";
|
||||
}
|
||||
if ( C4::Context->is_opac_suppressed && $biblio->opac_suppressed() ) {
|
||||
print $query->redirect($redirect_url);
|
||||
C4::Auth::safe_exit();
|
||||
if ( $biblio->opac_suppressed() ) {
|
||||
|
||||
# if OPAC suppression by IP address
|
||||
if ( C4::Context->preference('OpacSuppressionByIPRange') ) {
|
||||
my $IPAddress = $ENV{'REMOTE_ADDR'};
|
||||
my $IPRange = C4::Context->preference('OpacSuppressionByIPRange');
|
||||
if ( $IPAddress !~ /^$IPRange/ ) {
|
||||
print $query->redirect($redirect_url);
|
||||
C4::Auth::safe_exit();
|
||||
}
|
||||
} else {
|
||||
print $query->redirect($redirect_url);
|
||||
C4::Auth::safe_exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
191
C4/Overdues.pm
191
C4/Overdues.pm
|
|
@ -32,11 +32,14 @@ BEGIN {
|
|||
GetBranchcodesWithOverdueRules
|
||||
get_chargeable_units
|
||||
GetOverduesForBranch
|
||||
GetOverdueMessageTransportTypes
|
||||
parse_overdues_letter
|
||||
GetIssuesIteminfo
|
||||
);
|
||||
}
|
||||
|
||||
use Date::Calc qw( Today );
|
||||
use Date::Manip qw( UnixDate );
|
||||
use List::MoreUtils qw( uniq );
|
||||
use POSIX qw( ceil floor );
|
||||
use Locale::Currency::Format 1.28 qw( currency_format FMT_SYMBOL );
|
||||
|
|
@ -348,7 +351,7 @@ sub get_chargeable_units {
|
|||
|
||||
if ( $unit eq 'hours' ) {
|
||||
if ( C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed' ) {
|
||||
my $calendar = Koha::Library::Calendar->new( branchcode => $branchcode );
|
||||
my $calendar = Koha::Calendar->new( branchcode => $branchcode );
|
||||
$charge_duration = $calendar->hours_between( $date_due, $date_returned );
|
||||
} else {
|
||||
$charge_duration = $date_returned->delta_ms($date_due);
|
||||
|
|
@ -359,7 +362,7 @@ sub get_chargeable_units {
|
|||
return $charge_duration->in_units('hours');
|
||||
} else { # days
|
||||
if ( C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed' ) {
|
||||
my $calendar = Koha::Library::Calendar->new( branchcode => $branchcode );
|
||||
my $calendar = Koha::Calendar->new( branchcode => $branchcode );
|
||||
$charge_duration = $calendar->days_between( $date_due, $date_returned );
|
||||
} else {
|
||||
$charge_duration = $date_returned->delta_days($date_due);
|
||||
|
|
@ -368,6 +371,131 @@ sub get_chargeable_units {
|
|||
}
|
||||
}
|
||||
|
||||
=head2 GetSpecialHolidays
|
||||
|
||||
&GetSpecialHolidays($date_dues,$itemnumber);
|
||||
|
||||
return number of special days between date of the day and date due
|
||||
|
||||
C<$date_dues> is the envisaged date of book return.
|
||||
|
||||
C<$itemnumber> is the book's item number.
|
||||
|
||||
=cut
|
||||
|
||||
sub GetSpecialHolidays {
|
||||
my ( $date_dues, $itemnumber ) = @_;
|
||||
|
||||
# calcul the today date
|
||||
my $today = join "-", &Today();
|
||||
|
||||
# return the holdingbranch
|
||||
my $iteminfo = GetIssuesIteminfo($itemnumber);
|
||||
|
||||
# use sql request to find all date between date_due and today
|
||||
my $dbh = C4::Context->dbh;
|
||||
my $query = qq|SELECT DATE_FORMAT(concat(year,'-',month,'-',day),'%Y-%m-%d') as date
|
||||
FROM `special_holidays`
|
||||
WHERE DATE_FORMAT(concat(year,'-',month,'-',day),'%Y-%m-%d') >= ?
|
||||
AND DATE_FORMAT(concat(year,'-',month,'-',day),'%Y-%m-%d') <= ?
|
||||
AND branchcode=?
|
||||
|;
|
||||
my @result = GetWdayFromItemnumber($itemnumber);
|
||||
my @result_date;
|
||||
my $wday;
|
||||
my $dateinsec;
|
||||
my $sth = $dbh->prepare($query);
|
||||
$sth->execute( $date_dues, $today, $iteminfo->{'branchcode'} )
|
||||
; # FIXME: just use NOW() in SQL instead of passing in $today
|
||||
|
||||
while ( my $special_date = $sth->fetchrow_hashref ) {
|
||||
push( @result_date, $special_date );
|
||||
}
|
||||
|
||||
my $specialdaycount = scalar(@result_date);
|
||||
|
||||
for ( my $i = 0 ; $i < scalar(@result_date) ; $i++ ) {
|
||||
$dateinsec = UnixDate( $result_date[$i]->{'date'}, "%o" );
|
||||
( undef, undef, undef, undef, undef, undef, $wday, undef, undef ) = localtime($dateinsec);
|
||||
for ( my $j = 0 ; $j < scalar(@result) ; $j++ ) {
|
||||
if ( $wday == ( $result[$j]->{'weekday'} ) ) {
|
||||
$specialdaycount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $specialdaycount;
|
||||
}
|
||||
|
||||
=head2 GetRepeatableHolidays
|
||||
|
||||
&GetRepeatableHolidays($date_dues, $itemnumber, $difference,);
|
||||
|
||||
return number of day closed between date of the day and date due
|
||||
|
||||
C<$date_dues> is the envisaged date of book return.
|
||||
|
||||
C<$itemnumber> is item number.
|
||||
|
||||
C<$difference> numbers of between day date of the day and date due
|
||||
|
||||
=cut
|
||||
|
||||
sub GetRepeatableHolidays {
|
||||
my ( $date_dues, $itemnumber, $difference ) = @_;
|
||||
my $dateinsec = UnixDate( $date_dues, "%o" );
|
||||
my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime($dateinsec);
|
||||
my @result = GetWdayFromItemnumber($itemnumber);
|
||||
my @dayclosedcount;
|
||||
my $j;
|
||||
|
||||
for ( my $i = 0 ; $i < scalar(@result) ; $i++ ) {
|
||||
my $k = $wday;
|
||||
|
||||
for ( $j = 0 ; $j < $difference ; $j++ ) {
|
||||
if ( $result[$i]->{'weekday'} == $k ) {
|
||||
push( @dayclosedcount, $k );
|
||||
}
|
||||
$k++;
|
||||
( $k = 0 ) if ( $k eq 7 );
|
||||
}
|
||||
}
|
||||
return scalar(@dayclosedcount);
|
||||
}
|
||||
|
||||
=head2 GetWayFromItemnumber
|
||||
|
||||
&Getwdayfromitemnumber($itemnumber);
|
||||
|
||||
return the different week day from repeatable_holidays table
|
||||
|
||||
C<$itemnumber> is item number.
|
||||
|
||||
=cut
|
||||
|
||||
=head2 GetWdayFromItemnumber
|
||||
|
||||
Missing POD for GetWdayFromItemnumber.
|
||||
|
||||
=cut
|
||||
|
||||
sub GetWdayFromItemnumber {
|
||||
my ($itemnumber) = @_;
|
||||
my $iteminfo = GetIssuesIteminfo($itemnumber);
|
||||
my @result;
|
||||
my $query = qq|SELECT weekday
|
||||
FROM repeatable_holidays
|
||||
WHERE branchcode=?
|
||||
|;
|
||||
my $sth = C4::Context->dbh->prepare($query);
|
||||
|
||||
$sth->execute( $iteminfo->{'branchcode'} );
|
||||
while ( my $weekday = $sth->fetchrow_hashref ) {
|
||||
push( @result, $weekday );
|
||||
}
|
||||
return @result;
|
||||
}
|
||||
|
||||
=head2 GetIssuesIteminfo
|
||||
|
||||
&GetIssuesIteminfo($itemnumber);
|
||||
|
|
@ -592,19 +720,21 @@ returns a list of branch codes for branches with overdue rules defined.
|
|||
=cut
|
||||
|
||||
sub GetBranchcodesWithOverdueRules {
|
||||
my @branchcodes = Koha::CirculationRules->search(
|
||||
{ rule_name => 'overdue_1_delay' },
|
||||
{
|
||||
order_by => 'branchcode',
|
||||
columns => ['branchcode'],
|
||||
distinct => 1,
|
||||
}
|
||||
)->get_column('branchcode');
|
||||
if ( @branchcodes && !defined $branchcodes[0] ) {
|
||||
@branchcodes = Koha::Libraries->search( {}, { order_by => 'branchname' } )->get_column('branchcode');
|
||||
}
|
||||
my $dbh = C4::Context->dbh;
|
||||
my $branchcodes = $dbh->selectcol_arrayref(
|
||||
q|
|
||||
SELECT DISTINCT(branchcode)
|
||||
FROM overduerules
|
||||
WHERE delay1 IS NOT NULL
|
||||
ORDER BY branchcode
|
||||
|
|
||||
);
|
||||
if ( defined $branchcodes->[0] && $branchcodes->[0] eq '' ) {
|
||||
|
||||
return @branchcodes;
|
||||
# If a default rule exists, all branches should be returned
|
||||
return Koha::Libraries->search( {}, { order_by => 'branchname' } )->get_column('branchcode');
|
||||
}
|
||||
return @$branchcodes;
|
||||
}
|
||||
|
||||
=head2 GetOverduesForBranch
|
||||
|
|
@ -671,6 +801,39 @@ sub GetOverduesForBranch {
|
|||
}
|
||||
}
|
||||
|
||||
=head2 GetOverdueMessageTransportTypes
|
||||
|
||||
my $message_transport_types = GetOverdueMessageTransportTypes( $branchcode, $categorycode, $letternumber);
|
||||
|
||||
return a arrayref with all message_transport_type for given branchcode, categorycode and letternumber(1,2 or 3)
|
||||
|
||||
=cut
|
||||
|
||||
sub GetOverdueMessageTransportTypes {
|
||||
my ( $branchcode, $categorycode, $letternumber ) = @_;
|
||||
return unless $categorycode and $letternumber;
|
||||
my $dbh = C4::Context->dbh;
|
||||
my $sth = $dbh->prepare( "
|
||||
SELECT message_transport_type
|
||||
FROM overduerules odr LEFT JOIN overduerules_transport_types ott USING (overduerules_id)
|
||||
WHERE branchcode = ?
|
||||
AND categorycode = ?
|
||||
AND letternumber = ?
|
||||
" );
|
||||
$sth->execute( $branchcode, $categorycode, $letternumber );
|
||||
my @mtts;
|
||||
while ( my $mtt = $sth->fetchrow ) {
|
||||
push @mtts, $mtt;
|
||||
}
|
||||
|
||||
# Put 'print' in first if exists
|
||||
# It avoids sending a print notice with an email or sms template if no email or sms is defined
|
||||
@mtts = uniq( 'print', @mtts )
|
||||
if grep { $_ eq 'print' } @mtts;
|
||||
|
||||
return \@mtts;
|
||||
}
|
||||
|
||||
=head2 parse_overdues_letter
|
||||
|
||||
parses the letter template, replacing the placeholders with data
|
||||
|
|
|
|||
|
|
@ -919,11 +919,9 @@ sub marc2cites {
|
|||
$author = $field->subfield("a");
|
||||
}
|
||||
if ( $author =~ /([^,]+),?(.*)/ ) {
|
||||
my $surname = $1;
|
||||
my $forenames = $2;
|
||||
my %a;
|
||||
( $a{'surname'} = $surname ) =~ s/$re_clean//g;
|
||||
$a{'forenames'} = [ map { my $t = $_; $t =~ s/$re_clean//g; $t } split ' ', $forenames ];
|
||||
( $a{'surname'} = $1 ) =~ s/$re_clean//g;
|
||||
$a{'forenames'} = [ map { my $t = $_; $t =~ s/$re_clean//g; $t } split ' ', $2 ];
|
||||
push( @authors, \%a );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ use Koha::Report;
|
|||
use Koha::Reports;
|
||||
use Koha::SharedContent;
|
||||
use Koha::TemplateUtils qw( process_tt );
|
||||
use C4::Letters qw( GetPreparedLetter );
|
||||
|
||||
=head1 NAME
|
||||
|
||||
|
|
@ -881,54 +880,7 @@ sub get_saved_reports {
|
|||
|
||||
my $result = $dbh->selectall_arrayref( $query, { Slice => {} }, @args );
|
||||
|
||||
my $limit_pref_enabled = C4::Context->preference('LimitReportsByLibrary');
|
||||
|
||||
return $result unless $limit_pref_enabled;
|
||||
|
||||
# Preference enabled AND showing all: attach library limits metadata, but do not filter
|
||||
if ( $filter->{show_all_reports} ) {
|
||||
foreach my $report (@$result) {
|
||||
my $report_obj = Koha::Reports->find( $report->{id} );
|
||||
if ($report_obj) {
|
||||
my $limits_rs = $report_obj->get_library_limits;
|
||||
if ($limits_rs) {
|
||||
my @branches =
|
||||
map { { branchcode => $_->branchcode, branchname => $_->branchname } } $limits_rs->as_list;
|
||||
$report->{library_limits} = \@branches;
|
||||
} else {
|
||||
$report->{library_limits} = [];
|
||||
}
|
||||
} else {
|
||||
$report->{library_limits} = [];
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
# Preference enabled AND not showing all: apply library limits filtering
|
||||
my $limited_reports = Koha::Reports->search_with_library_limits( {}, {}, C4::Context::mybranch() );
|
||||
my %allowed = map { $_->{id} => 1 } @{ $limited_reports->unblessed };
|
||||
my @limited = grep { $allowed{ $_->{id} } } @$result;
|
||||
|
||||
# Attach library limits data for the filtered list
|
||||
foreach my $report (@limited) {
|
||||
my $report_obj = Koha::Reports->find( $report->{id} );
|
||||
if ($report_obj) {
|
||||
my $limits_rs = $report_obj->get_library_limits;
|
||||
if ($limits_rs) {
|
||||
my @branches =
|
||||
map { { branchcode => $_->branchcode, branchname => $_->branchname } } $limits_rs->as_list;
|
||||
$report->{library_limits} = \@branches;
|
||||
} else {
|
||||
$report->{library_limits} = [];
|
||||
}
|
||||
} else {
|
||||
$report->{library_limits} = [];
|
||||
}
|
||||
}
|
||||
|
||||
return \@limited;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
=head2 get_column_type($column)
|
||||
|
|
@ -1218,7 +1170,8 @@ sub EmailReport {
|
|||
my ( $sth, $errors ) = execute_query( { sql => $sql, report_id => $report_id } );
|
||||
return ( undef, [ { FATAL => "REPORT_FAIL" } ] ) if $errors;
|
||||
|
||||
my $counter = 1;
|
||||
my $counter = 1;
|
||||
my $template = $letter->{content};
|
||||
|
||||
while ( my $row = $sth->fetchrow_hashref() ) {
|
||||
my $email;
|
||||
|
|
@ -1231,16 +1184,13 @@ sub EmailReport {
|
|||
|
||||
my $from_address = $from || $row->{from};
|
||||
my $to_address = $row->{$email_col};
|
||||
my $letter = {%$letter}; # GetPreparedLetter mutates, so make a local copy beforehand
|
||||
push( @errors, { NOT_PARSE => $counter } ) unless my $rendered = GetPreparedLetter(
|
||||
letter => $letter,
|
||||
objects => $row
|
||||
);
|
||||
push( @errors, { NOT_PARSE => $counter } ) unless my $content = process_tt( $template, $row );
|
||||
$counter++;
|
||||
next if scalar @errors > $err_count; #If any problems, try next
|
||||
next if scalar @errors > $err_count; #If any problems, try next
|
||||
|
||||
$letter->{content} = $content;
|
||||
$email->{borrowernumber} = $row->{borrowernumber};
|
||||
$email->{letter} = $rendered;
|
||||
$email->{letter} = {%$letter};
|
||||
$email->{from_address} = $from_address;
|
||||
$email->{to_address} = $to_address;
|
||||
|
||||
|
|
|
|||
104
C4/Reserves.pm
104
C4/Reserves.pm
|
|
@ -66,8 +66,7 @@ use JSON qw( to_json );
|
|||
|
||||
use C4::Accounts;
|
||||
use C4::Biblio qw( GetMarcFromKohaField );
|
||||
use C4::Circulation qw( CheckIfIssuedToPatron GetBranchItemRule );
|
||||
use Koha::Policy::Biblio::AgeRestriction;
|
||||
use C4::Circulation qw( CheckIfIssuedToPatron GetAgeRestriction GetBranchItemRule );
|
||||
use C4::Context;
|
||||
use C4::Items qw( CartToShelf get_hostitemnumbers_of );
|
||||
use C4::Letters;
|
||||
|
|
@ -77,7 +76,7 @@ use C4::Members;
|
|||
use Koha::Account::Lines;
|
||||
use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
|
||||
use Koha::Biblios;
|
||||
use Koha::Library::Calendar;
|
||||
use Koha::Calendar;
|
||||
use Koha::Cache::Memory::Lite;
|
||||
use Koha::CirculationRules;
|
||||
use Koha::Database;
|
||||
|
|
@ -272,6 +271,7 @@ sub AddReserve {
|
|||
|
||||
# Log the hold creation
|
||||
if ( C4::Context->preference('HoldsLog') ) {
|
||||
|
||||
my $info = to_json(
|
||||
{
|
||||
hold => $hold->id,
|
||||
|
|
@ -289,11 +289,12 @@ sub AddReserve {
|
|||
patron_expiration_date => $hold->patron_expiration_date,
|
||||
priority => $hold->priority,
|
||||
reservenotes => $hold->reservenotes,
|
||||
confirmations => $confirmations || [],
|
||||
forced => $forced || []
|
||||
confirmations => $confirmations,
|
||||
forced => $forced
|
||||
},
|
||||
{ pretty => 1, canonical => 1 }
|
||||
);
|
||||
|
||||
logaction( 'HOLDS', 'CREATE', $hold->id, $info );
|
||||
}
|
||||
|
||||
|
|
@ -538,9 +539,9 @@ sub CanItemBeReserved {
|
|||
my $biblio = $item->biblio;
|
||||
|
||||
# Check for the age restriction
|
||||
my $age_check = Koha::Policy::Biblio::AgeRestriction->check( $biblio, $patron );
|
||||
my $ageRestriction = C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction );
|
||||
return _cache { status => 'ageRestricted' }
|
||||
if !$age_check;
|
||||
if $ageRestriction && $patron->dateofbirth && $ageRestriction > $patron->get_age();
|
||||
}
|
||||
|
||||
# Check that the patron doesn't have an item level hold on this item already
|
||||
|
|
@ -896,54 +897,6 @@ sub CheckReserves {
|
|||
}
|
||||
}
|
||||
|
||||
# Local holds exclusivity period: when the queue has
|
||||
# targeted a local-group item for this hold and we're still
|
||||
# within the exclusivity window, skip non-local items so the
|
||||
# targeted local item can fulfil it. If the queue has not yet
|
||||
# run for this reserve (no hold_fill_targets row), we fall
|
||||
# through and let the hold be filled normally.
|
||||
# This is independent of LocalHoldsPriority.
|
||||
my $excl_period = C4::Context->preference('LocalHoldsExclusivityPeriod');
|
||||
if ( $excl_period && $res->{reserve_id} ) {
|
||||
my $excl_item_control =
|
||||
C4::Context->preference('LocalHoldsExclusivityItemControl') || 'holdingbranch';
|
||||
my $excl_patron_control =
|
||||
C4::Context->preference('LocalHoldsExclusivityPatronControl') || 'PickupLibrary';
|
||||
|
||||
my $excl_item_branchcode = $item->$excl_item_control;
|
||||
$patron //= Koha::Patrons->find( $res->{borrowernumber} );
|
||||
my $excl_patron_branchcode =
|
||||
( $excl_patron_control eq 'PickupLibrary' ) ? $res->{branchcode}
|
||||
: ( $excl_patron_control eq 'HomeLibrary' ) ? $patron->branchcode
|
||||
: undef;
|
||||
|
||||
my $is_local_for_exclusivity = 0;
|
||||
if ( $excl_item_branchcode && $excl_patron_branchcode ) {
|
||||
if ( $excl_item_branchcode eq $excl_patron_branchcode ) {
|
||||
$is_local_for_exclusivity = 1;
|
||||
} else {
|
||||
my $excl_library = Koha::Libraries->find( { branchcode => $excl_item_branchcode } );
|
||||
$is_local_for_exclusivity = 1
|
||||
if $excl_library
|
||||
&& $excl_library->validate_hold_sibling( { branchcode => $excl_patron_branchcode } );
|
||||
}
|
||||
}
|
||||
|
||||
if ( !$is_local_for_exclusivity ) {
|
||||
my $reservedate_dt = eval { dt_from_string( $res->{reservedate} ) };
|
||||
if ($reservedate_dt) {
|
||||
my $age_days = dt_from_string->delta_days($reservedate_dt)->in_units('days');
|
||||
if ( $age_days < $excl_period ) {
|
||||
my $target =
|
||||
Koha::Database->new->schema->resultset('HoldFillTarget')
|
||||
->search( { reserve_id => $res->{reserve_id} }, { rows => 1 } )
|
||||
->next;
|
||||
next if $target && $target->local_holdgroup_match;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# See if this item is more important than what we've got so far
|
||||
if ( ( $res->{'priority'} && $res->{'priority'} < $priority )
|
||||
|| $local_hold_match
|
||||
|
|
@ -1089,7 +1042,7 @@ sub CancelExpiredReserves {
|
|||
my $cache_key = sprintf "Calendar_CancelExpiredReserves:%s", $hold->branchcode;
|
||||
my $calendar = $cache->get_from_cache($cache_key);
|
||||
if ( !$calendar ) {
|
||||
$calendar = Koha::Library::Calendar->new( branchcode => $hold->branchcode );
|
||||
$calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
|
||||
$cache->set_in_cache( $cache_key, $calendar );
|
||||
}
|
||||
|
||||
|
|
@ -1183,8 +1136,6 @@ for a bib.
|
|||
|
||||
If C<$rank> is 'del', the hold request is cancelled.
|
||||
|
||||
If C<$rank> is 'recall', the hold request is converted to a recall request.
|
||||
|
||||
If C<$rank> is an integer greater than zero, the priority of
|
||||
the request is set to that value. Since priority != 0 means
|
||||
that the item is not waiting on the hold shelf, setting the
|
||||
|
|
@ -1242,43 +1193,6 @@ sub ModReserve {
|
|||
|
||||
if ( $rank eq "del" ) {
|
||||
$hold->cancel( { cancellation_reason => $cancellation_reason } );
|
||||
} elsif ( $rank eq "recall" ) {
|
||||
if ( !$biblionumber ) {
|
||||
$biblionumber = $hold->biblionumber;
|
||||
}
|
||||
my $biblio = Koha::Biblios->find($biblionumber);
|
||||
if ( !$borrowernumber ) {
|
||||
$borrowernumber = $hold->borrowernumber;
|
||||
}
|
||||
my $patron = Koha::Patrons->find($borrowernumber);
|
||||
if ( $hold->item_level_hold ) {
|
||||
if ( $hold->item->can_be_recalled( { patron => $patron, hold_convert => 1 } ) ) {
|
||||
Koha::Recalls->add_recall(
|
||||
{
|
||||
patron => $patron,
|
||||
biblio => $biblio,
|
||||
branchcode => $hold->branchcode,
|
||||
expirationdate => $date,
|
||||
interface => 'intranet',
|
||||
item => $hold->item,
|
||||
}
|
||||
);
|
||||
$hold->cancel( { cancellation_reason => $cancellation_reason } );
|
||||
}
|
||||
} else {
|
||||
if ( $biblio->can_be_recalled( { patron => $patron, hold_convert => 1 } ) ) {
|
||||
Koha::Recalls->add_recall(
|
||||
{
|
||||
patron => $patron,
|
||||
biblio => $biblio,
|
||||
branchcode => $hold->branchcode,
|
||||
expirationdate => $date,
|
||||
interface => 'intranet',
|
||||
}
|
||||
);
|
||||
$hold->cancel( { cancellation_reason => $cancellation_reason } );
|
||||
}
|
||||
}
|
||||
} elsif ( $hold->found && $hold->priority eq '0' && $date ) {
|
||||
|
||||
# The only column that can be updated for a found hold is the expiration date
|
||||
|
|
|
|||
|
|
@ -106,10 +106,7 @@ sub new {
|
|||
my $it = $item->effective_itemtype;
|
||||
$self->{itemtype} = $it;
|
||||
my $itemtype = Koha::Database->new()->schema()->resultset('Itemtype')->find($it);
|
||||
if ($itemtype) {
|
||||
$self->{sip_media_type} = $itemtype->sip_media_type;
|
||||
$self->{magnetic_media} = $itemtype->sip_magnetic;
|
||||
}
|
||||
$self->{sip_media_type} = $itemtype->sip_media_type() if $itemtype;
|
||||
|
||||
# check if its on issue and if so get the borrower
|
||||
my $issue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );
|
||||
|
|
|
|||
|
|
@ -138,10 +138,6 @@ sub new {
|
|||
|
||||
my $circ_blocked =
|
||||
( C4::Context->preference('OverduesBlockCirc') ne "noblock" && defined $flags->{ODUES}->{itemlist} ) ? 1 : 0;
|
||||
|
||||
# Fetch holds once and derive hold_items and unavail_holds from the result
|
||||
my @holds = $patron->holds->as_list;
|
||||
|
||||
{
|
||||
no warnings; # any of these $kp->{fields} being concat'd could be undef
|
||||
%ilspatron = (
|
||||
|
|
@ -172,7 +168,7 @@ sub new {
|
|||
screen_msg => 'Greetings from Koha. ' . $kp->{opacnote} . $fines_msg,
|
||||
print_line => '',
|
||||
items => [],
|
||||
hold_items => [ map { $_->unblessed } @holds ],
|
||||
hold_items => $flags->{WAITING}->{itemlist},
|
||||
overdue_items => $flags->{ODUES}->{itemlist},
|
||||
too_many_overdue => $circ_blocked,
|
||||
fine_items => [],
|
||||
|
|
@ -227,7 +223,7 @@ sub new {
|
|||
}
|
||||
|
||||
# FIXME: populate recall_items
|
||||
$ilspatron{unavail_holds} = _get_outstanding_holds( \@holds );
|
||||
$ilspatron{unavail_holds} = _get_outstanding_holds( $kp->{borrowernumber} );
|
||||
|
||||
my $pending_checkouts = $patron->pending_checkouts;
|
||||
my @barcodes;
|
||||
|
|
@ -699,12 +695,12 @@ sub _get_address {
|
|||
}
|
||||
|
||||
sub _get_outstanding_holds {
|
||||
my $holds = shift;
|
||||
my $borrowernumber = shift;
|
||||
|
||||
my $patron = Koha::Patrons->find($borrowernumber);
|
||||
my $holds = $patron->holds->search( { -or => [ { found => undef }, { found => { '!=' => 'W' } } ] } );
|
||||
my @holds;
|
||||
foreach my $hold ( @{$holds} ) {
|
||||
next if ( $hold->found // '' ) eq 'W';
|
||||
|
||||
while ( my $hold = $holds->next ) {
|
||||
my $item;
|
||||
if ( $hold->itemnumber ) {
|
||||
$item = $hold->item;
|
||||
|
|
@ -925,6 +921,9 @@ __END__
|
|||
{message} "Yes"
|
||||
{itemlist} ref-to-array: list of overdue books
|
||||
{itemlisttext} Text list of overdue items
|
||||
{WAITING} Set if there are items available that the patron reserved
|
||||
{message} Message to this effect
|
||||
{itemlist} ref-to-array: list of available items
|
||||
|
||||
=cut
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,6 @@ sub do_checkin {
|
|||
unless $human_required || $checkin_blocked_by_holds;
|
||||
|
||||
if ($checked_in_ok) {
|
||||
delete $messages->{TransferArrived};
|
||||
delete $messages->{ItemLocationUpdated};
|
||||
delete $messages->{NotIssued};
|
||||
delete $messages->{LocalUse};
|
||||
|
|
|
|||
|
|
@ -55,10 +55,6 @@ my %transports = (
|
|||
# Read configuration
|
||||
#
|
||||
my $config = C4::SIP::Sip::Configuration->new( $ARGV[0] );
|
||||
|
||||
# Per-process record of when this worker loaded $config. Not shared since $config is per-process.
|
||||
our $sip2_config_read_timestamp = Koha::Caches->get_instance->get_from_cache('sip2_resource_last_modified');
|
||||
|
||||
my @params;
|
||||
|
||||
#
|
||||
|
|
@ -238,15 +234,13 @@ sub _config_up_to_date {
|
|||
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $sip2_resource_last_modified = $cache->get_from_cache("sip2_resource_last_modified");
|
||||
my $sip2_config_read_timestamp = $cache->get_from_cache("sip2_config_read_timestamp");
|
||||
|
||||
unless ($sip2_resource_last_modified) {
|
||||
siplog( "LOG_INFO", "Couldn't find sip2_resource_last_modified, considering configuration not up to date" );
|
||||
siplog( "LOG_WARNING", "Couldn't find sip2_resource_last_modified, considering configuration not up to date" );
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0 unless defined $sip2_config_read_timestamp;
|
||||
|
||||
return $sip2_config_read_timestamp >= $sip2_resource_last_modified ? 1 : 0;
|
||||
return $sip2_config_read_timestamp >= $sip2_resource_last_modified;
|
||||
}
|
||||
|
||||
#
|
||||
|
|
@ -266,17 +260,15 @@ sub process_request {
|
|||
my ( $sockaddr, $port, $proto );
|
||||
my $transport;
|
||||
|
||||
# Flush L1 before the freshness check so it sees the current shared value, not a stale one inherited at fork.
|
||||
Koha::Caches->flush_L1_caches();
|
||||
|
||||
$self->{config} = $config;
|
||||
unless ( $self->_config_up_to_date() ) {
|
||||
$self->{config} = C4::SIP::Sip::Configuration->get_configuration( undef, $self->{config} );
|
||||
$sip2_config_read_timestamp = Koha::Caches->get_instance->get_from_cache('sip2_resource_last_modified');
|
||||
}
|
||||
|
||||
# Flushing L1 to make sure the request will be processed using the correct data
|
||||
Koha::Caches->flush_L1_caches();
|
||||
|
||||
$self->{account} = undef; # Clear out the account from the last request, it may be different
|
||||
$self->{ils} = undef; # And the ILS handle, so a reused worker can't reuse a prior session's connection
|
||||
$self->{logger} = set_logger( Koha::Logger->get( { interface => 'sip' } ) );
|
||||
|
||||
# Flush previous MDCs to prevent accidentally leaking incorrect MDC-entries
|
||||
|
|
@ -476,12 +468,12 @@ sub telnet_transport {
|
|||
$pwd = get_clean_string($pwd);
|
||||
siplog( "LOG_DEBUG", "telnet_transport 2: uid length %s, pwd length %s", length($uid), length($pwd) );
|
||||
|
||||
# Only log the terminal in once login_core actually authenticates it
|
||||
if ( exists( $config->{accounts}->{$uid} )
|
||||
&& C4::SIP::Sip::MsgType::login_core( $self, $uid, $pwd ) )
|
||||
{
|
||||
# Check if user is authorized for SIP access, then authenticate via login_core
|
||||
if ( exists( $config->{accounts}->{$uid} ) ) {
|
||||
$account = $config->{accounts}->{$uid};
|
||||
last;
|
||||
if ( C4::SIP::Sip::MsgType::login_core( $self, $uid, $pwd ) ) {
|
||||
last;
|
||||
}
|
||||
}
|
||||
siplog( "LOG_WARNING", "Invalid login attempt: '%s'", ( $uid || '' ) );
|
||||
print("Invalid login$CRLF");
|
||||
|
|
|
|||
|
|
@ -97,7 +97,6 @@ sub add_field {
|
|||
);
|
||||
$value = '';
|
||||
}
|
||||
|
||||
$value =~ s/\r\n|\r|\n/ /g; # CR or LF terminates a sip message
|
||||
# Protect against them in sip text fields
|
||||
|
||||
|
|
@ -141,7 +140,7 @@ sub maybe_add {
|
|||
}
|
||||
|
||||
return ( defined($value) && length($value) )
|
||||
? add_field( $fid, $value, $server )
|
||||
? add_field( $fid, $value )
|
||||
: '';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,13 +41,17 @@ my $parser = XML::Simple->new(
|
|||
sub get_configuration {
|
||||
my ( $class, $config_file, $current_config ) = @_;
|
||||
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $cache = Koha::Caches->get_instance();
|
||||
my $sip2_resource_last_modified = $cache->get_from_cache("sip2_resource_last_modified");
|
||||
|
||||
unless ( $cache->get_from_cache("sip2_resource_last_modified") ) {
|
||||
if ($sip2_resource_last_modified) {
|
||||
$cache->set_in_cache( 'sip2_config_read_timestamp', $sip2_resource_last_modified );
|
||||
} else {
|
||||
|
||||
# Seed sip2_resource_last_modified so the configuration is not re-read
|
||||
# from the database on every request while the value is missing.
|
||||
$cache->set_in_cache( 'sip2_resource_last_modified', dt_from_string()->epoch );
|
||||
# Set sip2_resource_last_modified so config won't be read from db on each request
|
||||
my $now = dt_from_string()->epoch;
|
||||
$cache->set_in_cache( 'sip2_resource_last_modified', $now );
|
||||
$cache->set_in_cache( 'sip2_config_read_timestamp', $now );
|
||||
}
|
||||
|
||||
my $cfg;
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@ use Koha::Patrons;
|
|||
use Koha::Patron::Attributes;
|
||||
use Koha::Plugins;
|
||||
use Koha::Items;
|
||||
use Koha::DateUtils qw( output_pref );
|
||||
use Koha::TemplateUtils qw( process_tt );
|
||||
use Koha::DateUtils qw( output_pref );
|
||||
|
||||
use UNIVERSAL::can;
|
||||
|
||||
|
|
@ -626,10 +625,10 @@ sub handle_checkout {
|
|||
$resp .= timestamp;
|
||||
|
||||
# Now for the variable fields
|
||||
$resp .= add_field( FID_INST_ID, $inst, $server );
|
||||
$resp .= add_field( FID_PATRON_ID, $patron_id, $server );
|
||||
$resp .= add_field( FID_ITEM_ID, $item_id, $server );
|
||||
$resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ), $server );
|
||||
$resp .= add_field( FID_INST_ID, $inst, $server );
|
||||
$resp .= add_field( FID_PATRON_ID, $patron_id, $server );
|
||||
$resp .= add_field( FID_ITEM_ID, $item_id, $server );
|
||||
$resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
|
||||
if ( $item->due_date ) {
|
||||
my $due_date =
|
||||
$account->{format_due_date}
|
||||
|
|
@ -664,7 +663,7 @@ sub handle_checkout {
|
|||
|
||||
# If the item is valid, provide the title, otherwise
|
||||
# leave it blank
|
||||
$resp .= add_field( FID_TITLE_ID, $item ? _format_title( { item => $item, server => $server } ) : "", $server );
|
||||
$resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : '', $server );
|
||||
|
||||
# Due date is required. Since it didn't get checked out,
|
||||
# it's not due, so leave the date blank
|
||||
|
|
@ -757,14 +756,14 @@ sub handle_checkin {
|
|||
$resp .= 'U';
|
||||
}
|
||||
|
||||
$resp .= ( $status->alert || $item->magnetic_media ) ? 'Y' : 'N';
|
||||
$resp .= $status->alert ? 'Y' : 'N';
|
||||
$resp .= timestamp;
|
||||
$resp .= add_field( FID_INST_ID, $inst_id, $server );
|
||||
$resp .= add_field( FID_ITEM_ID, $item_id, $server );
|
||||
|
||||
if ($item) {
|
||||
$resp .= add_field( FID_PERM_LOCN, $item->permanent_location, $server );
|
||||
$resp .= maybe_add( FID_TITLE_ID, _format_title( { item => $item, server => $server } ), $server );
|
||||
$resp .= maybe_add( FID_TITLE_ID, $item->title_id, $server );
|
||||
$resp .= $item->build_additional_item_fields_string($server);
|
||||
} else {
|
||||
$resp .= add_field( FID_PERM_LOCN, "", $server );
|
||||
|
|
@ -933,19 +932,19 @@ sub login_core {
|
|||
my $pwd = shift;
|
||||
my $status = 1; # Assume it all works
|
||||
|
||||
# Clear any session state up front, including anything left over from a previous connection
|
||||
# on this preforked worker, so no login_core exit path can leave the caller acting as an
|
||||
# authenticated terminal. Only a successful api_auth below re-establishes the session.
|
||||
delete $server->{$_} foreach qw( account ils institution policy sip_username sip_password );
|
||||
|
||||
# Check if this userid is authorized for SIP access
|
||||
if ( !exists( $server->{config}->{accounts}->{$uid} ) ) {
|
||||
siplog( "LOG_WARNING", "MsgType::login_core: SIP access not authorized for user '$uid'" );
|
||||
return 0;
|
||||
}
|
||||
|
||||
# Authenticate before storing the account on the session
|
||||
my $inst = $server->{config}->{accounts}->{$uid}->{institution};
|
||||
# Store the active account configuration
|
||||
$server->{account} = $server->{config}->{accounts}->{$uid};
|
||||
my $inst = $server->{account}->{institution};
|
||||
$server->{institution} = $server->{config}->{institutions}->{$inst};
|
||||
$server->{policy} = $server->{institution}->{policy};
|
||||
$server->{sip_username} = $uid;
|
||||
$server->{sip_password} = $pwd;
|
||||
|
||||
# Authenticate using Koha's internal authentication (checks hashed password in borrowers table)
|
||||
my $auth_status = api_auth( $uid, $pwd, $inst );
|
||||
|
|
@ -957,13 +956,6 @@ sub login_core {
|
|||
return 0;
|
||||
}
|
||||
|
||||
# Store the active account configuration now user has authenticated
|
||||
$server->{account} = $server->{config}->{accounts}->{$uid};
|
||||
$server->{institution} = $server->{config}->{institutions}->{$inst};
|
||||
$server->{policy} = $server->{institution}->{policy};
|
||||
$server->{sip_username} = $uid;
|
||||
$server->{sip_password} = $pwd;
|
||||
|
||||
siplog( "LOG_INFO", "Successful login/auth for '%s' of '%s'", $server->{account}->{id}, $inst );
|
||||
|
||||
#
|
||||
|
|
@ -1344,8 +1336,8 @@ sub handle_item_information {
|
|||
$resp .= maybe_add( FID_SCREEN_MSG, "Item is damaged", $server );
|
||||
}
|
||||
|
||||
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
|
||||
$resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ), $server );
|
||||
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
|
||||
$resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
|
||||
|
||||
$resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type, $server );
|
||||
$resp .= maybe_add( FID_PERM_LOCN, $item->permanent_location, $server );
|
||||
|
|
@ -1430,8 +1422,8 @@ sub handle_item_status_update {
|
|||
$resp .= $status->ok ? '1' : '0';
|
||||
$resp .= timestamp;
|
||||
|
||||
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
|
||||
$resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ), $server );
|
||||
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
|
||||
$resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
|
||||
$resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
|
||||
}
|
||||
|
||||
|
|
@ -1552,10 +1544,10 @@ sub handle_hold {
|
|||
|
||||
( $status->expiration_date )
|
||||
and $resp .= maybe_add( FID_EXPIRATION, timestamp( $status->expiration_date ), $server );
|
||||
$resp .= maybe_add( FID_QUEUE_POS, $status->queue_position, $server );
|
||||
$resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location, $server );
|
||||
$resp .= maybe_add( FID_ITEM_ID, $status->item->id, $server );
|
||||
$resp .= maybe_add( FID_TITLE_ID, _format_title( { item => $status->item, server => $server } ), $server );
|
||||
$resp .= maybe_add( FID_QUEUE_POS, $status->queue_position, $server );
|
||||
$resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location, $server );
|
||||
$resp .= maybe_add( FID_ITEM_ID, $status->item->id, $server );
|
||||
$resp .= maybe_add( FID_TITLE_ID, $status->item->title_id, $server );
|
||||
} else {
|
||||
|
||||
# Not ok. still need required fields
|
||||
|
|
@ -1619,9 +1611,9 @@ sub handle_renew {
|
|||
}
|
||||
$resp .= sipbool( desensitize( { status => $status, patron => $patron, server => $server } ) );
|
||||
$resp .= timestamp;
|
||||
$resp .= add_field( FID_PATRON_ID, $patron->id, $server );
|
||||
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
|
||||
$resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ), $server );
|
||||
$resp .= add_field( FID_PATRON_ID, $patron->id, $server );
|
||||
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
|
||||
$resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
|
||||
if ( $item->due_date ) {
|
||||
$resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ), $server );
|
||||
} else {
|
||||
|
|
@ -1642,13 +1634,10 @@ sub handle_renew {
|
|||
# If we found the patron or the item, the return the ILS
|
||||
# information, otherwise echo back the information we received
|
||||
# from the terminal
|
||||
$resp .= add_field( FID_PATRON_ID, $patron ? $patron->id : $patron_id, $server );
|
||||
$resp .= add_field( FID_ITEM_ID, $item ? $item->id : $item_id, $server );
|
||||
$resp .= add_field(
|
||||
FID_TITLE_ID, $item ? _format_title( { item => $item, server => $server } ) : $title_id,
|
||||
$server
|
||||
);
|
||||
$resp .= add_field( FID_DUE_DATE, '', $server );
|
||||
$resp .= add_field( FID_PATRON_ID, $patron ? $patron->id : $patron_id, $server );
|
||||
$resp .= add_field( FID_ITEM_ID, $item ? $item->id : $item_id, $server );
|
||||
$resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : $title_id, $server );
|
||||
$resp .= add_field( FID_DUE_DATE, '', $server );
|
||||
}
|
||||
|
||||
if ( $status->fee_amount ) {
|
||||
|
|
@ -1904,19 +1893,6 @@ sub desensitize {
|
|||
return 1;
|
||||
}
|
||||
|
||||
sub _format_title {
|
||||
my ($params) = @_;
|
||||
my $server = $params->{server};
|
||||
my $item = $params->{item};
|
||||
if ( defined $server->{account}->{aj_field_template} && $server->{account}->{aj_field_template} ne '' ) {
|
||||
return process_tt(
|
||||
$server->{account}->{aj_field_template},
|
||||
{ biblio => $item->{object}->biblio, item => $item->{object} }
|
||||
);
|
||||
} else {
|
||||
return $item->title_id;
|
||||
}
|
||||
}
|
||||
1;
|
||||
__END__
|
||||
|
||||
|
|
|
|||
13
C4/Search.pm
13
C4/Search.pm
|
|
@ -35,8 +35,8 @@ BEGIN {
|
|||
|
||||
use C4::Context;
|
||||
use C4::Biblio qw( TransformMarcToKoha GetMarcFromKohaField GetFrameworkCode GetAuthorisedValueDesc GetBiblioData );
|
||||
use C4::Koha qw( getFacets GetVariationsOfISBN GetNormalizedISBN getitemtypeimagelocation );
|
||||
use Koha::Biblio::Metadata::Extractor;
|
||||
use C4::Koha
|
||||
qw( getFacets GetVariationsOfISBN GetNormalizedUPC GetNormalizedEAN GetNormalizedOCLCNumber GetNormalizedISBN getitemtypeimagelocation );
|
||||
use Koha::DateUtils;
|
||||
use Koha::Libraries;
|
||||
use Koha::SearchEngine::QueryBuilder;
|
||||
|
|
@ -1768,10 +1768,9 @@ sub searchResults {
|
|||
my $oldbiblio = TransformMarcToKoha( { record => $marcrecord, limit_table => 'no_items' } );
|
||||
$oldbiblio->{result_number} = $i + 1;
|
||||
|
||||
my $extractor = Koha::Biblio::Metadata::Extractor->new( { metadata => $marcrecord } );
|
||||
$oldbiblio->{normalized_upc} = $extractor->get_normalized_upc;
|
||||
$oldbiblio->{normalized_ean} = $extractor->get_normalized_ean;
|
||||
$oldbiblio->{normalized_oclc} = $extractor->get_normalized_oclc;
|
||||
$oldbiblio->{normalized_upc} = GetNormalizedUPC( $marcrecord, $marcflavour );
|
||||
$oldbiblio->{normalized_ean} = GetNormalizedEAN( $marcrecord, $marcflavour );
|
||||
$oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber( $marcrecord, $marcflavour );
|
||||
$oldbiblio->{normalized_isbn} = GetNormalizedISBN( $oldbiblio->{isbn}, $marcrecord, $marcflavour )
|
||||
; # Use existing ISBN from record if we got one
|
||||
$oldbiblio->{content_identifier_exists} = 1
|
||||
|
|
@ -1980,7 +1979,7 @@ sub searchResults {
|
|||
)
|
||||
: ( '', '', '' );
|
||||
$reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber} );
|
||||
if ( C4::Context->preference('UseRecalls') ne "off" ) {
|
||||
if ( C4::Context->preference('UseRecalls') ) {
|
||||
if ( Koha::Recalls->search( { item_id => $item->{itemnumber}, status => 'waiting' } )->count ) {
|
||||
$recallstatus = 'Waiting';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ BEGIN {
|
|||
|
||||
use C4::Biblio qw( GetAuthorisedValueDesc );
|
||||
use C4::Context;
|
||||
use C4::Koha qw( GetNormalizedISBN );
|
||||
use C4::Koha qw( GetNormalizedUPC GetNormalizedOCLCNumber GetNormalizedISBN GetNormalizedEAN );
|
||||
use Koha::Biblios;
|
||||
use Koha::Libraries;
|
||||
|
||||
|
|
@ -235,10 +235,10 @@ sub GetShelfInfo {
|
|||
$item->{part_number} = $biblio->part_number;
|
||||
$item->{part_name} = $biblio->part_name;
|
||||
my $this_record = $biblio->metadata->record;
|
||||
$item->{'browser_normalized_upc'} = $biblio->normalized_upc;
|
||||
$item->{'browser_normalized_oclc'} = $biblio->normalized_oclc;
|
||||
$item->{'browser_normalized_upc'} = GetNormalizedUPC( $this_record, $marcflavour );
|
||||
$item->{'browser_normalized_oclc'} = GetNormalizedOCLCNumber( $this_record, $marcflavour );
|
||||
$item->{'browser_normalized_isbn'} = GetNormalizedISBN( undef, $this_record, $marcflavour );
|
||||
$item->{'browser_normalized_ean'} = $biblio->normalized_ean;
|
||||
$item->{'browser_normalized_ean'} = GetNormalizedEAN( $this_record, $marcflavour );
|
||||
push @valid_items, $item;
|
||||
}
|
||||
return @valid_items;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package Koha::Patron::AccountLinks;
|
||||
package C4::Stats;
|
||||
|
||||
# Copyright 2000-2002 Katipo Communications
|
||||
#
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
|
|
@ -16,45 +18,49 @@ package Koha::Patron::AccountLinks;
|
|||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
use base 'Exporter';
|
||||
|
||||
use Koha::Patron::AccountLink;
|
||||
BEGIN {
|
||||
our @EXPORT_OK = qw(
|
||||
UpdateStats
|
||||
);
|
||||
}
|
||||
|
||||
use base qw(Koha::Objects);
|
||||
use Koha::Statistics;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Patron::AccountLinks - Koha Patron Account Link Object set class
|
||||
C4::Stats - Update Koha statistics (log)
|
||||
|
||||
=head1 API
|
||||
=head1 SYNOPSIS
|
||||
|
||||
=head2 Class Methods
|
||||
use C4::Stats;
|
||||
|
||||
=head3 get_next_group_id
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Returns the next available link_group_id for creating new link groups
|
||||
The functions of this module deals with statistics table of Koha database.
|
||||
|
||||
=head1 FUNCTIONS
|
||||
|
||||
=head2 UpdateStats
|
||||
|
||||
C4::Stats::UpdateStats($params);
|
||||
|
||||
This is a (legacy) alias for Koha::Statistic->new($params)->store.
|
||||
Please see Koha::Statistic module.
|
||||
|
||||
=cut
|
||||
|
||||
sub get_next_group_id {
|
||||
my ($class) = @_;
|
||||
my $max = $class->_resultset->get_column('link_group_id')->max || 0;
|
||||
return $max + 1;
|
||||
}
|
||||
|
||||
=head3 _type
|
||||
|
||||
=cut
|
||||
|
||||
sub _type {
|
||||
return 'PatronAccountLink';
|
||||
}
|
||||
|
||||
=head3 object_class
|
||||
|
||||
=cut
|
||||
|
||||
sub object_class {
|
||||
return 'Koha::Patron::AccountLink';
|
||||
sub UpdateStats {
|
||||
my $params = shift;
|
||||
Koha::Statistic->new($params)->store;
|
||||
}
|
||||
|
||||
1;
|
||||
__END__
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Koha Development Team <https://koha-community.org/>
|
||||
|
||||
=cut
|
||||
|
|
@ -23,10 +23,12 @@ use base 'Exporter';
|
|||
|
||||
BEGIN {
|
||||
our @EXPORT = qw(
|
||||
ConnectSuggestionAndBiblio
|
||||
DelSuggestion
|
||||
GetSuggestion
|
||||
ModStatus
|
||||
ModSuggestion
|
||||
DelSuggestionsOlderThan
|
||||
GetUnprocessedSuggestions
|
||||
MarcRecordFromNewSuggestion
|
||||
);
|
||||
|
|
@ -106,7 +108,7 @@ sub ModSuggestion {
|
|||
my $transport =
|
||||
( C4::Context->preference("FallbackToSMSIfNoEmail") )
|
||||
&& ( $patron->smsalertnumber )
|
||||
&& ( !$patron->notice_email_address ) ? 'sms' : 'email';
|
||||
&& ( !$patron->email ) ? 'sms' : 'email';
|
||||
|
||||
if (
|
||||
my $letter = C4::Letters::GetPreparedLetter(
|
||||
|
|
@ -138,6 +140,26 @@ sub ModSuggestion {
|
|||
return 1; # No useful if the exception is raised earlier
|
||||
}
|
||||
|
||||
=head2 ConnectSuggestionAndBiblio
|
||||
|
||||
&ConnectSuggestionAndBiblio($ordernumber,$biblionumber)
|
||||
|
||||
connect a suggestion to an existing biblio
|
||||
|
||||
=cut
|
||||
|
||||
sub ConnectSuggestionAndBiblio {
|
||||
my ( $suggestionid, $biblionumber ) = @_;
|
||||
my $dbh = C4::Context->dbh;
|
||||
my $query = q{
|
||||
UPDATE suggestions
|
||||
SET biblionumber=?
|
||||
WHERE suggestionid=?
|
||||
};
|
||||
my $sth = $dbh->prepare($query);
|
||||
$sth->execute( $biblionumber, $suggestionid );
|
||||
}
|
||||
|
||||
=head2 DelSuggestion
|
||||
|
||||
&DelSuggestion($borrowernumber,$ordernumber)
|
||||
|
|
@ -177,6 +199,28 @@ sub DelSuggestion {
|
|||
}
|
||||
}
|
||||
|
||||
=head2 DelSuggestionsOlderThan
|
||||
&DelSuggestionsOlderThan($days)
|
||||
|
||||
Delete all suggestions older than TODAY-$days , that have be accepted or rejected.
|
||||
We do now allow a negative number. If you want to delete all suggestions, just use Koha::Suggestions->delete or so.
|
||||
|
||||
=cut
|
||||
|
||||
sub DelSuggestionsOlderThan {
|
||||
my ($days) = @_;
|
||||
return unless $days && $days > 0;
|
||||
my $dbh = C4::Context->dbh;
|
||||
my $sth = $dbh->prepare(
|
||||
q{
|
||||
DELETE FROM suggestions
|
||||
WHERE STATUS<>'ASKED'
|
||||
AND manageddate < ADDDATE(NOW(), ?)
|
||||
}
|
||||
);
|
||||
$sth->execute("-$days");
|
||||
}
|
||||
|
||||
=head2 GetUnprocessedSuggestions
|
||||
|
||||
Missing POD for GetUnprocessedSuggestions.
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ use Modern::Perl;
|
|||
use base 'Exporter';
|
||||
use C4::Context;
|
||||
|
||||
use Module::Load::Conditional qw( check_install );
|
||||
|
||||
BEGIN {
|
||||
our @EXPORT_OK = qw(
|
||||
get_tags get_tag_rows
|
||||
|
|
@ -56,6 +54,7 @@ BEGIN {
|
|||
|
||||
use Carp qw( carp );
|
||||
|
||||
use Module::Load::Conditional qw( check_install );
|
||||
use Koha::Tags;
|
||||
use Koha::Tags::Approvals;
|
||||
use Koha::Tags::Indexes;
|
||||
|
|
|
|||
|
|
@ -241,7 +241,6 @@ sub _shared_preferences {
|
|||
AmazonCoverImages
|
||||
OPACAmazonCoverImages
|
||||
Babeltheque
|
||||
EnableZotero
|
||||
GoogleJackets
|
||||
HTML5MediaEnabled
|
||||
LibraryThingForLibrariesEnabled
|
||||
|
|
@ -321,7 +320,6 @@ sub _shared_preferences {
|
|||
autoMemberNum
|
||||
BorrowerRenewalPeriodBase
|
||||
EnableBorrowerFiles
|
||||
LimitReportsByLibrary
|
||||
EnhancedMessagingPreferences
|
||||
ExtendedPatronAttributes
|
||||
intranetreadinghistory
|
||||
|
|
@ -359,7 +357,6 @@ sub _shared_preferences {
|
|||
ILS-DI
|
||||
OAI-PMH
|
||||
version
|
||||
UseRecalls
|
||||
/;
|
||||
|
||||
return \@preferences;
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@ sub buildKohaItemsNamespace {
|
|||
my $substatus = '';
|
||||
my $recalls_count;
|
||||
|
||||
if ( C4::Context->preference('UseRecalls') ne "off" ) {
|
||||
if ( C4::Context->preference('UseRecalls') ) {
|
||||
$recalls_count = Koha::Recalls->search( { item_id => $item->itemnumber, status => 'waiting' } )->count;
|
||||
}
|
||||
|
||||
|
|
|
|||
2
Koha.pm
2
Koha.pm
|
|
@ -29,7 +29,7 @@ use vars qw{ $VERSION };
|
|||
# - #4 : the developer version. The 4th number is the database subversion.
|
||||
# used by developers when the database changes. updatedatabase take care of the changes itself
|
||||
# and is automatically called by Auth.pm when needed.
|
||||
$VERSION = "26.06.00.030";
|
||||
$VERSION = "26.05.01.000";
|
||||
|
||||
sub version {
|
||||
return $VERSION;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ use Try::Tiny qw( catch try );
|
|||
|
||||
use C4::Letters;
|
||||
use C4::Log qw( logaction );
|
||||
use C4::Stats qw( UpdateStats );
|
||||
use C4::Overdues qw(GetFine);
|
||||
use C4::Context;
|
||||
|
||||
|
|
@ -37,7 +38,6 @@ use Koha::Account::DebitTypes;
|
|||
use Koha::Exceptions;
|
||||
use Koha::Exceptions::Account;
|
||||
use Koha::Plugins;
|
||||
use Koha::Statistics;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
|
|
@ -286,7 +286,7 @@ sub add_credit {
|
|||
}
|
||||
)->store();
|
||||
|
||||
Koha::Statistic->new(
|
||||
C4::Stats::UpdateStats(
|
||||
{
|
||||
branch => $library_id,
|
||||
type => lc($credit_type),
|
||||
|
|
@ -294,8 +294,7 @@ sub add_credit {
|
|||
borrowernumber => $self->{patron_id},
|
||||
interface => $interface,
|
||||
}
|
||||
)->store()
|
||||
if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
|
||||
) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
|
||||
|
||||
Koha::Plugins->call(
|
||||
'after_account_action',
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ Returns the list of baskets for the vendor
|
|||
|
||||
sub baskets {
|
||||
my ($self) = @_;
|
||||
return Koha::Acquisition::Baskets->_new_from_dbic( scalar $self->_result->baskets );
|
||||
my $baskets_rs = $self->_result->aqbaskets;
|
||||
return Koha::Acquisition::Baskets->_new_from_dbic($baskets_rs);
|
||||
}
|
||||
|
||||
=head3 contacts
|
||||
|
|
@ -111,7 +112,8 @@ Returns the list of subscriptions for the vendor
|
|||
|
||||
sub subscriptions {
|
||||
my ($self) = @_;
|
||||
return Koha::Subscriptions->_new_from_dbic( scalar $self->_result->subscriptions );
|
||||
my $rs = $self->_result->subscriptions;
|
||||
return Koha::Subscriptions->_new_from_dbic($rs);
|
||||
}
|
||||
|
||||
=head3 aliases
|
||||
|
|
@ -196,7 +198,8 @@ Returns the list of invoices for the vendor
|
|||
|
||||
sub invoices {
|
||||
my ($self) = @_;
|
||||
return Koha::Acquisition::Invoices->_new_from_dbic( scalar $self->_result->invoices );
|
||||
my $invoices_rs = $self->_result->aqinvoices;
|
||||
return Koha::Acquisition::Invoices->_new_from_dbic($invoices_rs);
|
||||
}
|
||||
|
||||
=head3 to_api_mapping
|
||||
|
|
|
|||
|
|
@ -654,7 +654,6 @@ sub to_api_mapping {
|
|||
parent_ordernumber => 'parent_order_id',
|
||||
quantityreceived => 'quantity_received',
|
||||
replacementprice => 'replacement_price',
|
||||
servicing_instruction => undef, # EDIFACT related
|
||||
sort1 => 'statistics_1',
|
||||
sort1_authcat => 'statistics_1_authcat',
|
||||
sort2 => 'statistics_2',
|
||||
|
|
|
|||
|
|
@ -160,7 +160,6 @@ on this order (checking quantity and quantityreceived).
|
|||
|
||||
sub filter_by_active {
|
||||
my ($self) = @_;
|
||||
my $source_alias = $self->_resultset->current_source_alias;
|
||||
return $self->search(
|
||||
{
|
||||
'-or' => [
|
||||
|
|
@ -170,7 +169,7 @@ sub filter_by_active {
|
|||
},
|
||||
{ 'orderstatus' => [ 'ordered', 'partial' ] }
|
||||
],
|
||||
quantityreceived => { '<', \["COALESCE($source_alias.quantity,0)"] },
|
||||
quantityreceived => { '<', \['COALESCE(me.quantity,0)'] },
|
||||
},
|
||||
{ join => 'basket' }
|
||||
);
|
||||
|
|
|
|||
|
|
@ -19,14 +19,12 @@ package Koha::ActionLog;
|
|||
|
||||
use Modern::Perl;
|
||||
|
||||
use JSON qw( decode_json );
|
||||
|
||||
use C4::Context;
|
||||
|
||||
# Avoid `use Koha::Patrons` here: Koha::ActionLog is loaded transitively by
|
||||
# C4::Log very early in the dependency chain, before Koha::Patron has been
|
||||
# fully defined. The accessors below reach for Koha::Patron (and Koha::Items)
|
||||
# at call time, by which point they are reliably available.
|
||||
# fully defined. The accessors below reach for Koha::Patron at call time,
|
||||
# by which point it is reliably available.
|
||||
|
||||
use base qw(Koha::Object);
|
||||
|
||||
|
|
@ -72,42 +70,6 @@ sub patron {
|
|||
return Koha::Patron->_new_from_dbic($rs);
|
||||
}
|
||||
|
||||
=head3 item
|
||||
|
||||
my $item = $log->item;
|
||||
|
||||
Returns the related I<Koha::Item> for a CIRCULATION log entry, or I<undef>.
|
||||
|
||||
The itemnumber is not stored in a dedicated column; it lives inside the
|
||||
C<info> field, which for circulation is either a JSON payload carrying an
|
||||
C<itemnumber> key (ISSUE, RENEWAL) or a bare itemnumber string (RETURN).
|
||||
Returns I<undef> for non-circulation rows, when no itemnumber can be
|
||||
derived, or when the item has since been deleted.
|
||||
|
||||
=cut
|
||||
|
||||
sub item {
|
||||
my ($self) = @_;
|
||||
|
||||
return unless ( $self->module // q{} ) eq 'CIRCULATION';
|
||||
|
||||
my $info = $self->info;
|
||||
return unless defined $info;
|
||||
|
||||
my $itemnumber;
|
||||
my $decoded = eval { decode_json($info) };
|
||||
if ( ref($decoded) eq 'HASH' && $decoded->{itemnumber} ) {
|
||||
$itemnumber = $decoded->{itemnumber};
|
||||
} elsif ( $info =~ /^\s*(\d+)\s*$/ ) {
|
||||
$itemnumber = $1;
|
||||
}
|
||||
|
||||
return unless $itemnumber;
|
||||
|
||||
require Koha::Items;
|
||||
return Koha::Items->find($itemnumber);
|
||||
}
|
||||
|
||||
=head2 Internal methods
|
||||
|
||||
=head3 _type
|
||||
|
|
|
|||
|
|
@ -95,31 +95,19 @@ sub search_for_display {
|
|||
my ( $self, $params ) = @_;
|
||||
my $lang = $params->{lang} || q{};
|
||||
|
||||
# If lang is not default, we will search for entries matching $lang but fallback to default if $lang is not found
|
||||
# Then we need a subquery count in where clause; DBIx::Class/SQL::Abstract does not support it, fallback to literal SQL
|
||||
my $subquery =
|
||||
qq|(SELECT COUNT(*) FROM additional_contents_localizations WHERE lang='$lang' AND additional_content_id=me.additional_content_id)=0|;
|
||||
|
||||
my $search_params = get_public_query_search_params($params);
|
||||
$search_params->{lang} = 'default' if !$lang || $lang eq 'default';
|
||||
|
||||
unless ( $search_params->{lang} ) {
|
||||
$search_params->{-or} = [
|
||||
{ lang => $lang },
|
||||
{
|
||||
-and => [
|
||||
{ lang => 'default' },
|
||||
\[
|
||||
'(SELECT COUNT(*) FROM additional_contents_localizations WHERE lang = ? AND additional_content_id = me.additional_content_id) = 0',
|
||||
$lang
|
||||
]
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
$search_params->{-or} = [ { 'lang' => $lang }, '-and' => [ 'lang', 'default', \$subquery ] ]
|
||||
if !$search_params->{lang};
|
||||
|
||||
my $attribs = {
|
||||
prefetch => 'additional_content',
|
||||
order_by => [
|
||||
\['ISNULL(additional_content.number)'],
|
||||
{ -asc => 'additional_content.number' },
|
||||
{ -desc => 'additional_content.published_on' },
|
||||
],
|
||||
order_by => { -desc => 'additional_content.published_on' }
|
||||
};
|
||||
return Koha::AdditionalContentsLocalizations->search( $search_params, $attribs );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,10 @@ sub _get_data_and_patron {
|
|||
my $code = $tx->res->code || 'No response';
|
||||
|
||||
return if $code ne '200';
|
||||
my $claim = $tx->res->json // Mojo::Parameters->new( $tx->res->body )->to_hash;
|
||||
my $claim =
|
||||
$tx->res->headers->content_type =~ m!^(application/json|text/javascript)(;\s*charset=\S+)?$!
|
||||
? $tx->res->json
|
||||
: Mojo::Parameters->new( $tx->res->body )->to_hash;
|
||||
|
||||
foreach my $key ( keys %$mapping ) {
|
||||
my $pkey = $mapping->{$key};
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ use Modern::Perl;
|
|||
|
||||
use Scalar::Util qw( blessed );
|
||||
|
||||
use C4::Context;
|
||||
|
||||
use base qw(Koha::Object);
|
||||
|
||||
use Koha::Authority::ControlledIndicators;
|
||||
|
|
@ -294,112 +292,6 @@ sub record_schema {
|
|||
: 'MARC21';
|
||||
}
|
||||
|
||||
=head3 $MARC21_THESAURUS_TO_CONTROL_FIELD_008_11
|
||||
|
||||
my $code = $Koha::Authority::MARC21_THESAURUS_TO_CONTROL_FIELD_008_11->{$thesaurus};
|
||||
|
||||
The mapping of a subject heading's thesaurus name (as derived by
|
||||
C4::Heading from MARC indicator 2 / subfield $2, e.g. 'lcsh', 'mesh', or a
|
||||
raw $2 code such as 'fast') to its MARC21 authority 008/11 code ("Subject
|
||||
heading system/thesaurus"), per
|
||||
L<https://www.loc.gov/marc/authority/ad008.html>.
|
||||
|
||||
Note that C<sears> and C<aat> map to 008/11 values here but never appear
|
||||
as a thesaurus name derived by C4::Heading, because they don't have their
|
||||
own MARC21 indicator 2 value (see the LOC list linked above).
|
||||
|
||||
=cut
|
||||
|
||||
our $MARC21_THESAURUS_TO_CONTROL_FIELD_008_11 = {
|
||||
lcsh => 'a',
|
||||
lcac => 'b',
|
||||
mesh => 'c',
|
||||
nal => 'd',
|
||||
notapplicable => 'n',
|
||||
cash => 'k',
|
||||
rvm => 'v',
|
||||
aat => 'r',
|
||||
sears => 's',
|
||||
notdefined => 'z',
|
||||
notspecified => '|',
|
||||
};
|
||||
|
||||
# The subset of $MARC21_THESAURUS_TO_CONTROL_FIELD_008_11 keys that
|
||||
# C4::Heading uses to mean "no specific thesaurus could be identified from
|
||||
# the source heading's indicators" (a blank/unrecognised indicator 2, or
|
||||
# indicator 2 = 4), rather than a genuine, named thesaurus. Kept private -
|
||||
# callers should go through has_known_thesaurus() rather than consulting
|
||||
# this directly.
|
||||
my $UNDEFINED_THESAURUS_VALUES = {
|
||||
notdefined => 1,
|
||||
notspecified => 1,
|
||||
};
|
||||
|
||||
=head3 has_known_thesaurus
|
||||
|
||||
if ( Koha::Authority->has_known_thesaurus($thesaurus) ) { ... }
|
||||
|
||||
Returns true if C<$thesaurus> (as derived by C4::Heading from MARC
|
||||
indicator 2 / subfield $2, e.g. 'lcsh', 'mesh', or a raw $2 code such as
|
||||
'fast') identifies an actual thesaurus. Returns false if C<$thesaurus> is
|
||||
undefined, or is one of the placeholder values C4::Heading returns when
|
||||
no thesaurus could be identified from the source heading's indicators
|
||||
('notdefined', 'notspecified').
|
||||
|
||||
Callers that want to code a new authority's 008/11 (and 040$f) from a
|
||||
heading's thesaurus should check this first, and leave the site's own
|
||||
default (I<MARCAuthorityControlField008>) in place otherwise.
|
||||
|
||||
=cut
|
||||
|
||||
sub has_known_thesaurus {
|
||||
my ( $class, $thesaurus ) = @_;
|
||||
|
||||
return 0 unless defined $thesaurus;
|
||||
return $UNDEFINED_THESAURUS_VALUES->{$thesaurus} ? 0 : 1;
|
||||
}
|
||||
|
||||
=head3 default_marc21_008
|
||||
|
||||
my $default_008 = Koha::Authority->default_marc21_008;
|
||||
my $default_008 = Koha::Authority->default_marc21_008($thesaurus);
|
||||
|
||||
Returns the default 34-character body (i.e. everything after the
|
||||
6-character date) to use for a new MARC21 authority record's 008 field,
|
||||
honouring the I<MARCAuthorityControlField008> system preference and
|
||||
falling back to Koha's hardcoded default when it isn't set to a valid (at
|
||||
least 34 character) value.
|
||||
|
||||
If C<$thesaurus> is passed, position 11 ("Subject heading
|
||||
system/thesaurus") is set from
|
||||
L<$MARC21_THESAURUS_TO_CONTROL_FIELD_008_11> instead of whatever the
|
||||
default/syspref value carries, falling back to 'z' (Other) for a
|
||||
thesaurus with no dedicated code -- consistent with how Elasticsearch
|
||||
indexing already reconciles 008/11 vs 040$f (see
|
||||
Koha::SearchEngine::Elasticsearch::_process_mappings, where a 'z' is
|
||||
dropped in favour of a genuine 040$f value).
|
||||
|
||||
=cut
|
||||
|
||||
sub default_marc21_008 {
|
||||
my ( $class, $thesaurus ) = @_;
|
||||
|
||||
my $default_008 = C4::Context->preference('MARCAuthorityControlField008');
|
||||
if ( !$default_008 or length($default_008) < 34 ) {
|
||||
$default_008 = '|| aca||aabn | a|a d';
|
||||
} else {
|
||||
$default_008 = substr( $default_008, 0, 34 );
|
||||
}
|
||||
|
||||
if ( defined $thesaurus ) {
|
||||
|
||||
# Position 11 is absolute (over date + body); within the 34-char body alone that's index 11 - 6 = 5.
|
||||
substr( $default_008, 5, 1, $MARC21_THESAURUS_TO_CONTROL_FIELD_008_11->{$thesaurus} // 'z' );
|
||||
}
|
||||
|
||||
return $default_008;
|
||||
}
|
||||
|
||||
=head3 to_api_mapping
|
||||
|
||||
This method returns the mapping for representing a Koha::Authority object
|
||||
|
|
|
|||
|
|
@ -455,7 +455,6 @@ sub core_types_to_classes {
|
|||
marc_import_revert_batch => 'Koha::BackgroundJob::MARCImportRevertBatch',
|
||||
pseudonymize_statistic => 'Koha::BackgroundJob::PseudonymizeStatistic',
|
||||
import_from_kbart_file => 'Koha::BackgroundJob::ImportKBARTFile',
|
||||
patron_import => 'Koha::BackgroundJob::PatronImport',
|
||||
file_transport_test => 'Koha::BackgroundJob::TestTransport',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ sub process {
|
|||
my $use_transport_cost_matrix = C4::Context->preference("UseTransportCostMatrix");
|
||||
my $transport_cost_matrix = $use_transport_cost_matrix ? TransportCostMatrix() : undef;
|
||||
my $branches_to_use = load_branches_to_pull_from($use_transport_cost_matrix);
|
||||
my $unallocated = C4::Context->preference('RealTimeHoldsQueueUnallocated');
|
||||
|
||||
my @messages;
|
||||
|
||||
|
|
@ -83,9 +82,8 @@ sub process {
|
|||
{
|
||||
biblio_id => $biblio_id,
|
||||
branches_to_use => $branches_to_use,
|
||||
delete => $unallocated ? 0 : 1,
|
||||
transport_cost_matrix => $transport_cost_matrix,
|
||||
unallocated => $unallocated,
|
||||
delete => 1,
|
||||
transport_cost_matrix => $transport_cost_matrix
|
||||
}
|
||||
);
|
||||
push @messages,
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ sub process {
|
|||
};
|
||||
|
||||
try {
|
||||
my ($results) = Koha::Items->search( { itemnumber => { -in => \@record_ids } } )->batch_update(
|
||||
my ($results) = Koha::Items->search( { itemnumber => \@record_ids } )->batch_update(
|
||||
{
|
||||
regex_mod => $regex_mod,
|
||||
new_values => $new_values,
|
||||
|
|
|
|||
|
|
@ -1,138 +0,0 @@
|
|||
package Koha::BackgroundJob::PatronImport;
|
||||
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
use Try::Tiny qw( catch try );
|
||||
|
||||
use Koha::List::Patron qw( AddPatronList AddPatronsToList );
|
||||
use Koha::Patrons::Import;
|
||||
use Koha::UploadedFiles;
|
||||
|
||||
use base 'Koha::BackgroundJob';
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::BackgroundJob::PatronImport - Import patrons from a CSV file as a background job
|
||||
|
||||
This is a subclass of Koha::BackgroundJob.
|
||||
|
||||
=head1 API
|
||||
|
||||
=head2 Class methods
|
||||
|
||||
=head3 job_type
|
||||
|
||||
Define the job type of this job: patron_import
|
||||
|
||||
=cut
|
||||
|
||||
sub job_type { return 'patron_import' }
|
||||
|
||||
=head3 process
|
||||
|
||||
Process the job.
|
||||
|
||||
=cut
|
||||
|
||||
sub process {
|
||||
my ( $self, $args ) = @_;
|
||||
|
||||
return if $self->status eq 'cancelled';
|
||||
|
||||
$self->start;
|
||||
|
||||
my $upload = Koha::UploadedFiles->find( $args->{uploaded_file_id} );
|
||||
unless ($upload) {
|
||||
$self->status('failed')->store;
|
||||
return;
|
||||
}
|
||||
my $fh = $upload->file_handle;
|
||||
|
||||
my $result = try {
|
||||
Koha::Patrons::Import->new->import_patrons(
|
||||
{
|
||||
file => $fh,
|
||||
defaults => $args->{defaults},
|
||||
matchpoint => $args->{matchpoint},
|
||||
overwrite_cardnumber => $args->{overwrite_cardnumber},
|
||||
overwrite_passwords => $args->{overwrite_passwords},
|
||||
preserve_extended_attributes => $args->{preserve_extended_attributes},
|
||||
preserve_fields => $args->{preserve_fields},
|
||||
update_dateexpiry => $args->{update_dateexpiry},
|
||||
update_dateexpiry_from_today => $args->{update_dateexpiry_from_today},
|
||||
update_dateexpiry_from_existing => $args->{update_dateexpiry_from_existing},
|
||||
send_welcome => $args->{send_welcome},
|
||||
step_callback => sub { $self->step },
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
$self->status('failed')->store;
|
||||
return;
|
||||
};
|
||||
|
||||
my $data = $self->decoded_data;
|
||||
|
||||
if ($result) {
|
||||
if ( $args->{createpatronlist} && @{ $result->{imported_borrowers} // [] } ) {
|
||||
my $list = AddPatronList( { name => $args->{patronlistname} } );
|
||||
AddPatronsToList( { list => $list, borrowernumbers => $result->{imported_borrowers} } );
|
||||
$data->{patron_list_name} = $args->{patronlistname};
|
||||
}
|
||||
|
||||
$data->{feedback} = $result->{feedback};
|
||||
$data->{errors} = $result->{errors};
|
||||
$data->{imported} = $result->{imported};
|
||||
$data->{overwritten} = $result->{overwritten};
|
||||
$data->{already_in_db} = $result->{already_in_db};
|
||||
$data->{invalid} = $result->{invalid};
|
||||
$data->{total} = $result->{imported} + $result->{overwritten} + $result->{already_in_db} + $result->{invalid};
|
||||
}
|
||||
|
||||
$self->finish($data);
|
||||
}
|
||||
|
||||
=head3 enqueue
|
||||
|
||||
Enqueue the new job.
|
||||
|
||||
=cut
|
||||
|
||||
sub enqueue {
|
||||
my ( $self, $args ) = @_;
|
||||
|
||||
return unless $args->{uploaded_file_id};
|
||||
|
||||
my $upload = Koha::UploadedFiles->find( $args->{uploaded_file_id} );
|
||||
return unless $upload;
|
||||
|
||||
my $fh = $upload->file_handle;
|
||||
my $line_count = 0;
|
||||
while ( my $line = <$fh> ) {
|
||||
$line_count++ if $line =~ /\S/;
|
||||
}
|
||||
close $fh;
|
||||
|
||||
$self->SUPER::enqueue(
|
||||
{
|
||||
job_size => $line_count > 1 ? $line_count - 1 : 1,
|
||||
job_args => $args,
|
||||
job_queue => 'long_tasks',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
@ -18,7 +18,6 @@ package Koha::BackgroundJob::PseudonymizeStatistic;
|
|||
use Modern::Perl;
|
||||
|
||||
use Koha::PseudonymizedTransactions;
|
||||
use Koha::Statistics;
|
||||
|
||||
use base 'Koha::BackgroundJob';
|
||||
|
||||
|
|
|
|||
|
|
@ -53,10 +53,7 @@ sub process {
|
|||
|
||||
my $transport = Koha::File::Transports->find( $args->{transport_id} );
|
||||
$transport->test_connection;
|
||||
my $status = {
|
||||
status => 'ok', transport_id => $transport->id, transport_name => $transport->name,
|
||||
background_job_id => $self->id
|
||||
};
|
||||
my $status = { status => 'ok' };
|
||||
my $messages = $transport->object_messages;
|
||||
for my $message (@$messages) {
|
||||
$status->{status} = 'errors' if $message->{type} eq 'error';
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use URI;
|
|||
use URI::Escape qw( uri_escape_utf8 );
|
||||
use Try::Tiny;
|
||||
|
||||
use C4::Koha qw( GetNormalizedISBN );
|
||||
use C4::Koha qw( GetNormalizedISBN GetNormalizedUPC GetNormalizedOCLCNumber );
|
||||
use C4::Biblio qw( DelBiblio );
|
||||
use C4::Serials qw( CountSubscriptionFromBiblionumber );
|
||||
use C4::Reserves qw( MergeHolds );
|
||||
|
|
@ -412,14 +412,6 @@ sub check_booking {
|
|||
'-not_in' => $existing_bookings->_resultset->get_column('item_id')->as_query,
|
||||
'-in' => $bookable_item_ids,
|
||||
},
|
||||
|
||||
# A checkout linked to the booking under consideration must not
|
||||
# block that booking's own updates (see Koha::Item->check_booking)
|
||||
(
|
||||
defined($booking_id)
|
||||
? ( "me.booking_id" => [ undef, { '!=' => $booking_id } ] )
|
||||
: ()
|
||||
),
|
||||
}
|
||||
);
|
||||
$booked_count += $checkouts->count;
|
||||
|
|
@ -807,7 +799,7 @@ return the current holds placed on this record
|
|||
sub holds {
|
||||
my ( $self, $params, $attributes ) = @_;
|
||||
$attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
|
||||
my $hold_rs = $self->_result->holds->search( $params, $attributes );
|
||||
my $hold_rs = $self->_result->reserves->search( $params, $attributes );
|
||||
return Koha::Holds->_new_from_dbic($hold_rs);
|
||||
}
|
||||
|
||||
|
|
@ -1632,9 +1624,7 @@ sub get_marc_authors {
|
|||
if ( $marcflavour eq "UNIMARC" ) {
|
||||
$fields_filter = '200';
|
||||
} else { # marc21/normarc
|
||||
$fields_filter = '1..';
|
||||
$mintag = '100';
|
||||
$maxtag = '111';
|
||||
$fields_filter = '100';
|
||||
}
|
||||
|
||||
my @first_authors = @{
|
||||
|
|
@ -1711,19 +1701,6 @@ sub normalized_upc {
|
|||
return $self->metadata_extractor->get_normalized_upc;
|
||||
}
|
||||
|
||||
=head3 normalized_ean
|
||||
|
||||
my $normalized_ean = $biblio->normalized_ean
|
||||
|
||||
Normalizes and returns the EAN found in the MARC record.
|
||||
|
||||
=cut
|
||||
|
||||
sub normalized_ean {
|
||||
my ($self) = @_;
|
||||
return $self->metadata_extractor->get_normalized_ean;
|
||||
}
|
||||
|
||||
=head3 normalized_oclc
|
||||
|
||||
my $normalized_oclc = $biblio->normalized_oclc
|
||||
|
|
@ -2175,51 +2152,6 @@ sub link_marc_host {
|
|||
return $self;
|
||||
}
|
||||
|
||||
=head3 age_restriction
|
||||
|
||||
my $age = $biblio->age_restriction;
|
||||
|
||||
Extracts the numeric age restriction from the biblio's agerestriction field
|
||||
using the AgeRestrictionMarker system preference.
|
||||
|
||||
Returns the age (integer) or undef if no restriction is found.
|
||||
|
||||
=cut
|
||||
|
||||
sub age_restriction {
|
||||
my ($self) = @_;
|
||||
|
||||
my $restriction_string = $self->biblioitem->agerestriction;
|
||||
return unless $restriction_string;
|
||||
|
||||
my @values = split ' ', uc($restriction_string);
|
||||
return unless @values;
|
||||
|
||||
my @markers = map { my $m = uc($_); $m =~ s/^\s+//; $m =~ s/\s+$//; $m }
|
||||
@{ C4::Context->multivalue_preference('AgeRestrictionMarker') };
|
||||
return unless @markers;
|
||||
|
||||
my $index = 0;
|
||||
my $restriction_age = 0;
|
||||
for my $value (@values) {
|
||||
$index++;
|
||||
for my $marker (@markers) {
|
||||
if ( $marker eq $value ) {
|
||||
if ( $index <= $#values ) {
|
||||
$restriction_age += $values[$index];
|
||||
}
|
||||
last;
|
||||
} elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
|
||||
$restriction_age += $1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
last if ( $restriction_age > 0 );
|
||||
}
|
||||
|
||||
return $restriction_age || undef;
|
||||
}
|
||||
|
||||
=head3 recalls
|
||||
|
||||
my $recalls = $biblio->recalls;
|
||||
|
|
@ -2239,14 +2171,12 @@ sub recalls {
|
|||
|
||||
Does biblio-level checks and returns the items attached to this biblio that are available for recall
|
||||
|
||||
If hold_convert $param is included, this is a check to convert a hold to a recall, so we should not check for an existing hold.
|
||||
|
||||
=cut
|
||||
|
||||
sub can_be_recalled {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
return 0 if ( C4::Context->preference('UseRecalls') eq "off" );
|
||||
return 0 if !( C4::Context->preference('UseRecalls') );
|
||||
|
||||
my $patron = $params->{patron};
|
||||
|
||||
|
|
@ -2266,7 +2196,7 @@ sub can_be_recalled {
|
|||
my @all_itemnumbers;
|
||||
foreach my $item (@all_items) {
|
||||
push( @all_itemnumbers, $item->itemnumber );
|
||||
if ( $item->can_be_recalled( { patron => $patron, hold_convert => $params->{hold_convert} } ) ) {
|
||||
if ( $item->can_be_recalled( { patron => $patron } ) ) {
|
||||
push( @itemtypes, $item->effective_itemtype );
|
||||
push( @itemnumbers, $item->itemnumber );
|
||||
push( @items, $item );
|
||||
|
|
|
|||
|
|
@ -24,19 +24,14 @@ use C4::Biblio qw( GetMarcFromKohaField );
|
|||
use C4::Charset qw( StripNonXmlChars );
|
||||
use C4::Items qw( GetMarcItem );
|
||||
|
||||
use Koha::Biblio::Metadata::Error;
|
||||
use Koha::Biblio::Metadata::Errors;
|
||||
use Koha::Database;
|
||||
use Koha::Exceptions::Metadata;
|
||||
use Koha::Logger;
|
||||
use Koha::RecordSources;
|
||||
|
||||
use base qw(Koha::Object);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
=encoding utf-8
|
||||
|
||||
Koha::Metadata - Koha Metadata Object class
|
||||
|
||||
=head1 API
|
||||
|
|
@ -45,188 +40,6 @@ Koha::Metadata - Koha Metadata Object class
|
|||
|
||||
=cut
|
||||
|
||||
=head3 store
|
||||
|
||||
Metadata specific store method to catch errant characters prior
|
||||
to committing to the database.
|
||||
|
||||
If the MARCXML cannot be parsed but can be recovered, the recovered version is
|
||||
saved and an error row per fault is written to C<biblio_metadata_errors> so
|
||||
callers and the UI can notify the user. Two recoverable faults are currently
|
||||
handled, and either or both may apply to the same record:
|
||||
|
||||
=over 4
|
||||
|
||||
=item * non-XML characters (recorded as C<nonxml_stripped>, recovered via
|
||||
C<StripNonXmlChars>)
|
||||
|
||||
=item * datafields with no subfields at all, which C<MARC::File::XML> refuses
|
||||
to parse (recorded as C<empty_datafield_stripped>, recovered by removing the
|
||||
empty datafield)
|
||||
|
||||
=back
|
||||
|
||||
Any pre-existing error row is left alone when the record parses cleanly - they
|
||||
are review flags requiring explicit human resolution and should not be
|
||||
silently cleared by a routine re-save.
|
||||
|
||||
If the MARCXML cannot be recovered at all, a
|
||||
I<Koha::Exceptions::Metadata::Invalid> exception is thrown.
|
||||
|
||||
=cut
|
||||
|
||||
sub store {
|
||||
my $self = shift;
|
||||
|
||||
# Reset messages so object_messages reflects only this store call
|
||||
$self->{_messages} = [];
|
||||
|
||||
my %recoveries; # error_type => arrayref of occurrences, for faults that were fixed
|
||||
|
||||
# Check marcxml will roundtrip
|
||||
if ( $self->format eq 'marcxml' ) {
|
||||
|
||||
my $marcxml = eval {
|
||||
MARC::Record::new_from_xml(
|
||||
$self->metadata, 'UTF-8',
|
||||
$self->schema
|
||||
);
|
||||
};
|
||||
my $marcxml_error = $@;
|
||||
chomp $marcxml_error;
|
||||
|
||||
unless ($marcxml) {
|
||||
my ( $stripped_record, $stripped_metadata, $stripped_recoveries ) =
|
||||
$self->repair_marcxml( $self->metadata, $self->schema );
|
||||
|
||||
if ($stripped_record) {
|
||||
|
||||
Koha::Logger->get->warn(
|
||||
sprintf(
|
||||
"Metadata for bibliographic record (biblionumber=%s) was automatically repaired before saving: %s",
|
||||
$self->biblionumber // 'N/A', $marcxml_error
|
||||
)
|
||||
);
|
||||
$self->metadata($stripped_metadata);
|
||||
%recoveries = %$stripped_recoveries;
|
||||
} else {
|
||||
|
||||
# Truly unrecoverable
|
||||
Koha::Logger->get->warn($marcxml_error);
|
||||
Koha::Exceptions::Metadata::Invalid->throw(
|
||||
id => $self->id,
|
||||
biblionumber => $self->biblionumber,
|
||||
format => $self->format,
|
||||
schema => $self->schema,
|
||||
decoding_error => $marcxml_error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$self->SUPER::store;
|
||||
|
||||
# If this save triggered a fresh repair, record each fault as a DB error row and
|
||||
# signal the event via object_messages. A fault already on record for this metadata_id
|
||||
# (same error_type and message) is left as-is rather than duplicated, so re-harvesting or
|
||||
# re-importing a source record that is still dirty doesn't pile up repeat rows for the
|
||||
# same underlying issue.
|
||||
for my $error_type ( sort keys %recoveries ) {
|
||||
my $occurrences = $recoveries{$error_type};
|
||||
my $formatter = _error_message_formatter($error_type);
|
||||
if (@$occurrences) {
|
||||
for my $occ (@$occurrences) {
|
||||
$self->_add_error_once( $error_type, $formatter->($occ), $occ );
|
||||
}
|
||||
} else {
|
||||
|
||||
# Fallback: couldn't pinpoint individual occurrences – store a generic message
|
||||
$self->_add_error_once( $error_type, 'details unavailable' );
|
||||
}
|
||||
|
||||
$self->add_message(
|
||||
{
|
||||
message => $error_type,
|
||||
type => 'warning',
|
||||
payload => ( join( "\n", map { $formatter->($_) } @$occurrences ) || 'details unavailable' ),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
=head3 metadata_errors
|
||||
|
||||
my $errors = $metadata->metadata_errors;
|
||||
|
||||
Returns a I<Koha::Biblio::Metadata::Errors> resultset for errors associated
|
||||
with this metadata record.
|
||||
|
||||
=cut
|
||||
|
||||
sub metadata_errors {
|
||||
my ($self) = @_;
|
||||
return Koha::Biblio::Metadata::Errors->_new_from_dbic( scalar $self->_result->biblio_metadata_errors );
|
||||
}
|
||||
|
||||
=head3 _add_error_once
|
||||
|
||||
$self->_add_error_once( $error_type, $message, $occurrence );
|
||||
|
||||
Records a C<biblio_metadata_errors> row for this metadata record, unless an
|
||||
identical row (same C<error_type> and C<message>) already exists - so
|
||||
re-triggering the same fault on a source record that stays dirty across
|
||||
repeated saves doesn't pile up duplicate rows. C<$occurrence> is the optional
|
||||
occurrence hashref produced by C<_find_nonxml_chars>/C<_find_empty_datafields>;
|
||||
when its C<field> key looks like C<TAG$sub> or C<TAG>, the C<tag>/C<subfield>
|
||||
columns are populated so callers (e.g. the cataloguing UI) don't need to parse
|
||||
them back out of the free-text message.
|
||||
|
||||
=cut
|
||||
|
||||
sub _add_error_once {
|
||||
my ( $self, $error_type, $message, $occurrence ) = @_;
|
||||
|
||||
return
|
||||
if Koha::Biblio::Metadata::Errors->search(
|
||||
{
|
||||
metadata_id => $self->id,
|
||||
error_type => $error_type,
|
||||
message => $message,
|
||||
}
|
||||
)->count;
|
||||
|
||||
my ( $tag, $subfield ) =
|
||||
( $occurrence && $occurrence->{field} ) ? _split_field_ref( $occurrence->{field} ) : ( undef, undef );
|
||||
|
||||
Koha::Biblio::Metadata::Error->new(
|
||||
{
|
||||
metadata_id => $self->id,
|
||||
error_type => $error_type,
|
||||
message => $message,
|
||||
tag => $tag,
|
||||
subfield => $subfield,
|
||||
}
|
||||
)->store;
|
||||
}
|
||||
|
||||
=head3 _split_field_ref
|
||||
|
||||
my ( $tag, $subfield ) = _split_field_ref( $field_ref );
|
||||
|
||||
Splits a human-readable field reference such as C<245$a> into its tag and
|
||||
subfield parts. Field references with no subfield (control fields, or a
|
||||
datafield reported as a whole e.g. from C<_find_empty_datafields>) return an
|
||||
undefined subfield.
|
||||
|
||||
=cut
|
||||
|
||||
sub _split_field_ref {
|
||||
my ($field_ref) = @_;
|
||||
return $field_ref =~ /^(\w+)\$(\w)$/ ? ( $1, $2 ) : ( $field_ref, undef );
|
||||
}
|
||||
|
||||
=head3 record
|
||||
|
||||
my $record = $metadata->record;
|
||||
|
|
@ -442,249 +255,6 @@ sub _embed_items {
|
|||
return $record;
|
||||
}
|
||||
|
||||
=head3 _context_snippet
|
||||
|
||||
my $snippet = _context_snippet( \@chars, $pos_0, $char_ord );
|
||||
|
||||
Given an array-ref of characters C<\@chars>, a 0-based position C<$pos_0> of
|
||||
a bad character, and its ordinal C<$char_ord>, returns a two-line string:
|
||||
|
||||
=over 4
|
||||
|
||||
=item * Line 1 – up to 30 characters of context either side of the bad
|
||||
character, with the bad character replaced by a visible glyph (Unicode
|
||||
Control Pictures U+2400–U+241F for C0 controls, C<␡> for DEL, C<?>
|
||||
otherwise) and ellipses when the window is truncated.
|
||||
|
||||
=item * Line 2 – a caret (C<^>) aligned beneath the visible glyph.
|
||||
|
||||
=back
|
||||
|
||||
Example output (4-space indent):
|
||||
|
||||
t $xtac arr␈\ $btxt V $2rdacontent
|
||||
^
|
||||
|
||||
=cut
|
||||
|
||||
sub _context_snippet {
|
||||
my ( $chars_ref, $pos_0, $char_ord ) = @_;
|
||||
|
||||
# Visible stand-in for the removed character:
|
||||
# C0 controls (U+0000–U+001F) → Unicode Control Pictures (U+2400–U+241F)
|
||||
# DEL (U+007F) → U+2421 SYMBOL FOR DELETE (␡)
|
||||
# anything else → U+FFFD REPLACEMENT CHARACTER (?)
|
||||
my $visible =
|
||||
$char_ord <= 0x1F ? chr( $char_ord + 0x2400 )
|
||||
: $char_ord == 0x7F ? "\x{2421}"
|
||||
: "\x{FFFD}";
|
||||
|
||||
my $last = $#{$chars_ref};
|
||||
my $win = 30;
|
||||
|
||||
my $pre_start = ( $pos_0 > $win ) ? $pos_0 - $win : 0;
|
||||
my $post_end = ( $pos_0 + $win < $last ) ? $pos_0 + $win : $last;
|
||||
|
||||
my $pre = $pos_0 > 0 ? join( '', @{$chars_ref}[ $pre_start .. $pos_0 - 1 ] ) : '';
|
||||
my $post = $pos_0 < $last ? join( '', @{$chars_ref}[ $pos_0 + 1 .. $post_end ] ) : '';
|
||||
|
||||
my $prefix = ( $pos_0 > $win ) ? '...' : '';
|
||||
my $suffix = ( $pos_0 + $win < $last ) ? '...' : '';
|
||||
|
||||
my $indent = ' ';
|
||||
my $line = "$indent$prefix$pre$visible$post$suffix";
|
||||
my $caret_col = length($indent) + length($prefix) + length($pre);
|
||||
|
||||
return "$line\n" . ( ' ' x $caret_col ) . '^';
|
||||
}
|
||||
|
||||
=head3 repair_marcxml
|
||||
|
||||
my ( $record, $repaired_xml, $recoveries ) =
|
||||
Koha::Biblio::Metadata->repair_marcxml( $marcxml, $marc_schema );
|
||||
|
||||
Attempts to repair a MARCXML string that fails to parse, by stripping
|
||||
whichever of the known-recoverable faults (non-XML characters,
|
||||
subfield-less datafields) are present. This is the same recovery logic used
|
||||
by C<store>, exposed as a class method so other callers that parse MARCXML
|
||||
ahead of a C<Koha::Biblio::Metadata> save - such as C<Koha::Import::Record>
|
||||
staging records - benefit from the same repair capability instead of
|
||||
re-implementing a weaker version of it.
|
||||
|
||||
Returns, on success, the repaired C<MARC::Record>, the repaired XML string,
|
||||
and a hashref of C<< error_type => \@occurrences >> (see
|
||||
C<_find_nonxml_chars>/C<_find_empty_datafields>) describing what was fixed.
|
||||
Returns an empty list if the XML has none of the known-recoverable faults,
|
||||
or if it still doesn't parse once they're stripped.
|
||||
|
||||
=cut
|
||||
|
||||
sub repair_marcxml {
|
||||
my ( $class, $marcxml, $marc_schema ) = @_;
|
||||
|
||||
my @nonxml_chars = _find_nonxml_chars($marcxml);
|
||||
my @empty_datafields = _find_empty_datafields($marcxml);
|
||||
|
||||
return unless @nonxml_chars || @empty_datafields;
|
||||
|
||||
my $stripped_metadata = $marcxml;
|
||||
$stripped_metadata = StripNonXmlChars($stripped_metadata) if @nonxml_chars;
|
||||
$stripped_metadata = _strip_empty_datafields($stripped_metadata) if @empty_datafields;
|
||||
|
||||
my $stripped_record = eval { MARC::Record::new_from_xml( $stripped_metadata, 'UTF-8', $marc_schema ); };
|
||||
return unless $stripped_record;
|
||||
|
||||
my %recoveries;
|
||||
$recoveries{ Koha::Biblio::Metadata::Error::NONXML_STRIPPED() } = \@nonxml_chars if @nonxml_chars;
|
||||
$recoveries{ Koha::Biblio::Metadata::Error::EMPTY_DATAFIELD_STRIPPED() } = \@empty_datafields if @empty_datafields;
|
||||
|
||||
return ( $stripped_record, $stripped_metadata, \%recoveries );
|
||||
}
|
||||
|
||||
=head3 _find_nonxml_chars
|
||||
|
||||
my @occurrences = _find_nonxml_chars( $marcxml_string );
|
||||
|
||||
Scans a raw MARC XML string for every individual character that is illegal
|
||||
in XML 1.0. Returns a list of hashrefs (one per character occurrence) with
|
||||
the following keys:
|
||||
|
||||
=over 4
|
||||
|
||||
=item * C<field> - human-readable field reference, e.g. C<245$a> or C<001>
|
||||
|
||||
=item * C<char_ord> - ordinal (decimal) value of the bad character
|
||||
|
||||
=item * C<position> - 1-based character offset within the field value
|
||||
|
||||
=item * C<snippet> - two-line string with context window and caret pointer (see C<_context_snippet>)
|
||||
|
||||
=back
|
||||
|
||||
The same character class is used as L<C4::Charset/StripNonXmlChars>, so
|
||||
every occurrence returned here is exactly one that would be stripped.
|
||||
|
||||
=cut
|
||||
|
||||
sub _find_nonxml_chars {
|
||||
my ($marcxml) = @_;
|
||||
|
||||
# Characters illegal in XML 1.0 – identical to the set stripped by StripNonXmlChars
|
||||
my $non_xml_re = qr/[^\x09\x0A\x0D\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/;
|
||||
|
||||
my @occurrences;
|
||||
|
||||
my $scan = sub {
|
||||
my ( $field_ref, $val ) = @_;
|
||||
my @chars = split //, $val;
|
||||
for my $i ( 0 .. $#chars ) {
|
||||
if ( $chars[$i] =~ $non_xml_re ) {
|
||||
push @occurrences, {
|
||||
field => $field_ref,
|
||||
char_ord => ord( $chars[$i] ),
|
||||
position => $i + 1,
|
||||
snippet => _context_snippet( \@chars, $i, ord( $chars[$i] ) ),
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
# Scan every subfield inside every datafield
|
||||
while ( $marcxml =~ m{<datafield\b[^>]*\btag="(\w+)"[^>]*>(.*?)</datafield>}gs ) {
|
||||
my ( $tag, $content ) = ( $1, $2 );
|
||||
while ( $content =~ m{<subfield\b[^>]*\bcode="(\w)"[^>]*>(.*?)</subfield>}gs ) {
|
||||
$scan->( "$tag\$$1", $2 );
|
||||
}
|
||||
}
|
||||
|
||||
# Scan control fields
|
||||
while ( $marcxml =~ m{<controlfield\b[^>]*\btag="(\w+)"[^>]*>(.*?)</controlfield>}gs ) {
|
||||
$scan->( $1, $2 );
|
||||
}
|
||||
|
||||
return @occurrences;
|
||||
}
|
||||
|
||||
=head3 _find_empty_datafields
|
||||
|
||||
my @occurrences = _find_empty_datafields( $marcxml_string );
|
||||
|
||||
Scans a raw MARC XML string for C<< <datafield> >> elements with no
|
||||
C<< <subfield> >> children at all. C<MARC::File::XML> refuses to parse such
|
||||
a field (it dies with "Field NNN must have at least one subfield"), which
|
||||
happens with legacy data saved before this validation existed.
|
||||
|
||||
Returns a list of hashrefs (one per occurrence) with the single key
|
||||
C<field>, the datafield's tag.
|
||||
|
||||
=cut
|
||||
|
||||
sub _find_empty_datafields {
|
||||
my ($marcxml) = @_;
|
||||
|
||||
my @occurrences;
|
||||
while ( $marcxml =~ m{<datafield\b([^>]*)>\s*</datafield>|<datafield\b([^>]*)/>}gs ) {
|
||||
my $attrs = defined $1 ? $1 : $2;
|
||||
my ($tag) = $attrs =~ /\btag="(\w+)"/;
|
||||
push @occurrences, { field => $tag // 'unknown' };
|
||||
}
|
||||
|
||||
return @occurrences;
|
||||
}
|
||||
|
||||
=head3 _strip_empty_datafields
|
||||
|
||||
my $cleaned_marcxml = _strip_empty_datafields( $marcxml_string );
|
||||
|
||||
Removes every C<< <datafield> >> element with no C<< <subfield> >> children
|
||||
from a raw MARC XML string, so the record can be parsed and saved. The
|
||||
removed field's data cannot be recovered - callers should record an error
|
||||
row (see C<_find_empty_datafields>) so a cataloguer can review and re-add it.
|
||||
|
||||
=cut
|
||||
|
||||
sub _strip_empty_datafields {
|
||||
my ($marcxml) = @_;
|
||||
|
||||
return $marcxml unless defined $marcxml;
|
||||
$marcxml =~ s{<datafield\b[^>]*>\s*</datafield>}{}gs;
|
||||
$marcxml =~ s{<datafield\b[^>]*/>}{}gs;
|
||||
|
||||
return $marcxml;
|
||||
}
|
||||
|
||||
=head3 _error_message_formatter
|
||||
|
||||
my $formatter = _error_message_formatter( $error_type );
|
||||
my $message = $formatter->( $occurrence );
|
||||
|
||||
Returns a coderef that turns one occurrence hashref (as produced by
|
||||
C<_find_nonxml_chars> or C<_find_empty_datafields>) into a human-readable
|
||||
message line for the given C<$error_type>.
|
||||
|
||||
=cut
|
||||
|
||||
sub _error_message_formatter {
|
||||
my ($error_type) = @_;
|
||||
|
||||
return sub {
|
||||
my ($occ) = @_;
|
||||
return sprintf(
|
||||
"%s: invalid char value %d (U+%04X) at position %d\n%s",
|
||||
$occ->{field}, $occ->{char_ord}, $occ->{char_ord}, $occ->{position}, $occ->{snippet}
|
||||
);
|
||||
}
|
||||
if $error_type eq Koha::Biblio::Metadata::Error::NONXML_STRIPPED;
|
||||
|
||||
return sub {
|
||||
my ($occ) = @_;
|
||||
return sprintf( "%s: field removed, it had no subfields", $occ->{field} );
|
||||
}
|
||||
if $error_type eq Koha::Biblio::Metadata::Error::EMPTY_DATAFIELD_STRIPPED;
|
||||
|
||||
return sub { 'details unavailable' };
|
||||
}
|
||||
|
||||
=head3 _type
|
||||
|
||||
=cut
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
package Koha::Biblio::Metadata::Error;
|
||||
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use Koha::Exceptions;
|
||||
|
||||
use base qw(Koha::Object);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Biblio::Metadata::Error - Koha Biblio Metadata Error object class
|
||||
|
||||
=head1 Constants
|
||||
|
||||
=cut
|
||||
|
||||
use constant {
|
||||
NONXML_STRIPPED => 'nonxml_stripped',
|
||||
EMPTY_DATAFIELD_STRIPPED => 'empty_datafield_stripped',
|
||||
};
|
||||
|
||||
my %VALID_ERROR_TYPES = map { $_ => 1 } ( NONXML_STRIPPED, EMPTY_DATAFIELD_STRIPPED );
|
||||
|
||||
=head1 API
|
||||
|
||||
=head2 Class methods
|
||||
|
||||
=head3 store
|
||||
|
||||
Rejects an C<error_type> outside the known set, so a typo in a future caller
|
||||
fails loudly at store time rather than silently writing an unrecognised value
|
||||
into the database.
|
||||
|
||||
=cut
|
||||
|
||||
sub store {
|
||||
my ($self) = @_;
|
||||
|
||||
Koha::Exceptions::BadParameter->throw( parameter => 'error_type' )
|
||||
unless $VALID_ERROR_TYPES{ $self->error_type // '' };
|
||||
|
||||
return $self->SUPER::store;
|
||||
}
|
||||
|
||||
=head2 Internal methods
|
||||
|
||||
=head3 _type
|
||||
|
||||
=cut
|
||||
|
||||
sub _type {
|
||||
return 'BiblioMetadataError';
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package Koha::Biblio::Metadata::Errors;
|
||||
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use Koha::Biblio::Metadata::Error;
|
||||
|
||||
use base qw(Koha::Objects);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Biblio::Metadata::Errors - Koha Biblio Metadata Errors object set class
|
||||
|
||||
=head1 API
|
||||
|
||||
=head2 Class methods
|
||||
|
||||
=head3 by_type
|
||||
|
||||
my $errors = Koha::Biblio::Metadata::Errors->search(...)->by_type('nonxml_stripped');
|
||||
|
||||
Filters the resultset down to a single C<error_type>. See
|
||||
L<Koha::Biblio::Metadata::Error> for the known type constants.
|
||||
|
||||
=cut
|
||||
|
||||
sub by_type {
|
||||
my ( $self, $error_type ) = @_;
|
||||
return $self->search( { error_type => $error_type } );
|
||||
}
|
||||
|
||||
=head2 Internal methods
|
||||
|
||||
=head3 _type
|
||||
|
||||
=cut
|
||||
|
||||
sub _type {
|
||||
return 'BiblioMetadataError';
|
||||
}
|
||||
|
||||
=head3 object_class
|
||||
|
||||
=cut
|
||||
|
||||
sub object_class {
|
||||
return 'Koha::Biblio::Metadata::Error';
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
@ -104,29 +104,6 @@ sub get_control_number {
|
|||
return $control_number;
|
||||
}
|
||||
|
||||
=head3 get_normalized_oclc
|
||||
|
||||
my $normalized_oclc = $extractor->get_normalized_oclc();
|
||||
|
||||
Returns a normalized OCLC number from 035$a (field is flavor-independent).
|
||||
|
||||
=cut
|
||||
|
||||
sub get_normalized_oclc {
|
||||
my ($self) = @_;
|
||||
|
||||
my $record = $self->metadata;
|
||||
my @fields = $record->field('035');
|
||||
foreach my $field (@fields) {
|
||||
my $oclc = $field->subfield('a');
|
||||
if ( $oclc && $oclc =~ /OCoLC/i ) {
|
||||
$oclc =~ s/^\([^)]*\)\s*//;
|
||||
return $oclc;
|
||||
}
|
||||
}
|
||||
return q{};
|
||||
}
|
||||
|
||||
=head3 _normalize_string
|
||||
|
||||
my $normalized_string = $self->_normalize_string($string);
|
||||
|
|
|
|||
|
|
@ -74,27 +74,26 @@ sub get_normalized_upc {
|
|||
}
|
||||
}
|
||||
|
||||
=head2 get_normalized_ean
|
||||
=head2 get_normalized_oclc
|
||||
|
||||
my $normalized_ean = $extractor->get_normalized_ean();
|
||||
my $normalized_oclc = $extractor->get_normalized_oclc();
|
||||
|
||||
Returns a normalized EAN from 024$a with indicator 1 = 3.
|
||||
Returns a normalized OCLC number.
|
||||
|
||||
=cut
|
||||
|
||||
sub get_normalized_ean {
|
||||
sub get_normalized_oclc {
|
||||
my ($self) = @_;
|
||||
|
||||
my $record = $self->metadata;
|
||||
my @fields = $record->field('024');
|
||||
my @fields = $record->field('035');
|
||||
foreach my $field (@fields) {
|
||||
my $indicator = $field->indicator(1);
|
||||
my $ean = $self->_normalize_string( $field->subfield('a') );
|
||||
if ( $ean && $indicator eq '3' ) {
|
||||
return $ean;
|
||||
my $oclc = $field->subfield('a');
|
||||
if ( $oclc && $oclc =~ /OCoLC/ ) {
|
||||
$oclc =~ s/\(OCoLC\)//;
|
||||
return $oclc;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
=head2 check_fixed_length
|
||||
|
|
|
|||
|
|
@ -72,28 +72,6 @@ sub get_normalized_upc {
|
|||
}
|
||||
}
|
||||
|
||||
=head2 get_normalized_ean
|
||||
|
||||
my $normalized_ean = $extractor->get_normalized_ean();
|
||||
|
||||
Returns a normalized EAN from 073$a.
|
||||
|
||||
=cut
|
||||
|
||||
sub get_normalized_ean {
|
||||
my ($self) = @_;
|
||||
|
||||
my $record = $self->metadata;
|
||||
my @fields = $record->field('073');
|
||||
foreach my $field (@fields) {
|
||||
my $ean = $self->_normalize_string( $field->subfield('a') );
|
||||
if ($ean) {
|
||||
return $ean;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Tomas Cohen Arazi, E<lt>tomascohen@theke.ioE<gt>
|
||||
|
|
|
|||
|
|
@ -150,13 +150,6 @@ sub store {
|
|||
|
||||
$self->_result->result_source->schema->txn_do(
|
||||
sub {
|
||||
# Nothing changed, nothing to validate; circulation may issue
|
||||
# redundant status writes (e.g. when switching an on-site
|
||||
# checkout) that must not re-trigger clash detection
|
||||
if ( $self->in_storage && !( () = $self->_result->get_dirty_columns ) ) {
|
||||
return $self->SUPER::store;
|
||||
}
|
||||
|
||||
if ( $self->item_id ) {
|
||||
Koha::Exceptions::Object::FKConstraint->throw(
|
||||
broken_fk => 'item_id',
|
||||
|
|
@ -180,12 +173,12 @@ sub store {
|
|||
value => $self->biblio_id,
|
||||
) unless ( $self->biblio );
|
||||
|
||||
# Skip clash detection when the status transition cannot
|
||||
# introduce a new clash: terminal statuses (cancelled,
|
||||
# completed) and collection (issued) leave the booked
|
||||
# period untouched, and the checks can produce false
|
||||
# positives (e.g. against the booking's own checkout).
|
||||
unless ( $self->_skip_clash_detection ) {
|
||||
# Skip clash detection when transitioning to a final
|
||||
# status. During checkout C4::Circulation sets status
|
||||
# to 'completed' and calls ->store; the clash checks
|
||||
# are irrelevant at that point and can produce false
|
||||
# positives.
|
||||
unless ( $self->_is_final_status_transition ) {
|
||||
|
||||
# Throw exception for item level booking clash
|
||||
Koha::Exceptions::Booking::Clash->throw()
|
||||
|
|
@ -221,6 +214,7 @@ sub store {
|
|||
$self->_send_notice( { notice => 'BOOKING_CONFIRMATION' } );
|
||||
} else {
|
||||
my %updated_columns = $self->_result->get_dirty_columns;
|
||||
return $self->SUPER::store unless %updated_columns;
|
||||
|
||||
my $old_booking = $self->get_from_storage;
|
||||
$self->SUPER::store;
|
||||
|
|
@ -350,22 +344,17 @@ sub to_api_mapping {
|
|||
|
||||
=head2 Internal methods
|
||||
|
||||
=head3 _skip_clash_detection
|
||||
=head3 _is_final_status_transition
|
||||
|
||||
my $bool = $self->_skip_clash_detection;
|
||||
my $bool = $self->_is_final_status_transition;
|
||||
|
||||
Returns true when the booking is being transitioned to a status for which
|
||||
clash detection is not meaningful: the terminal statuses C<cancelled> and
|
||||
C<completed>, or C<issued> (collection). A booking being collected already
|
||||
occupies the period being validated, and the item's own checkout would
|
||||
register as a false clash. The transition to C<issued> only skips the
|
||||
checks while the booked period and item remain unchanged, and only from an
|
||||
active (C<new> or C<issued>) stored status; reactivating a terminal booking
|
||||
is still validated.
|
||||
Returns true when the booking is being transitioned to a final status
|
||||
(cancelled or completed). Used to skip clash detection that is only
|
||||
meaningful for active bookings.
|
||||
|
||||
=cut
|
||||
|
||||
sub _skip_clash_detection {
|
||||
sub _is_final_status_transition {
|
||||
my ($self) = @_;
|
||||
|
||||
return 0 unless $self->in_storage;
|
||||
|
|
@ -375,14 +364,6 @@ sub _skip_clash_detection {
|
|||
|
||||
return 0 unless $new_status;
|
||||
return 1 if any { $_ eq $new_status } qw( cancelled completed );
|
||||
|
||||
if ( $new_status eq 'issued' ) {
|
||||
return 0 if any { exists $updated_columns->{$_} } qw( start_date end_date item_id biblio_id );
|
||||
|
||||
my $stored_status = $self->get_from_storage->status;
|
||||
return 1 if any { $_ eq $stored_status } qw( new issued );
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,14 +36,18 @@ Koha::Bookings - Koha Booking object set class
|
|||
|
||||
$bookings->filter_by_active;
|
||||
|
||||
Returns bookings that are still active, i.e. awaiting collection (C<new>) or
|
||||
currently on loan (C<issued>). Cancelled and completed bookings are excluded.
|
||||
Will return the bookings that have not ended, were cancelled or are completed.
|
||||
|
||||
=cut
|
||||
|
||||
sub filter_by_active {
|
||||
my ($self) = @_;
|
||||
return $self->search( { status => { '-in' => [ 'new', 'issued' ] } } );
|
||||
return $self->search(
|
||||
{
|
||||
end_date => { '>=' => \'NOW()' },
|
||||
status => { '-not_in' => [ 'cancelled', 'completed' ] }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
=head2 Internal Methods
|
||||
|
|
|
|||
|
|
@ -45,8 +45,9 @@ use Module::Load::Conditional qw( can_load );
|
|||
use Sereal::Encoder;
|
||||
use Sereal::Decoder;
|
||||
|
||||
use Koha::Config;
|
||||
use C4::Context;
|
||||
use Koha::Cache::Object;
|
||||
use Koha::Config;
|
||||
|
||||
use base qw(Class::Accessor);
|
||||
|
||||
|
|
@ -75,8 +76,8 @@ sub new {
|
|||
# Should we continue to support MEMCACHED ENV vars?
|
||||
$self->{'namespace'} ||= $ENV{MEMCACHED_NAMESPACE};
|
||||
my @servers = split /,/, $ENV{MEMCACHED_SERVERS} || '';
|
||||
$self->{namespace} ||= Koha::Config->get_instance->get('memcached_namespace') || 'koha';
|
||||
@servers = split /,/, Koha::Config->get_instance->get('memcached_servers') // ''
|
||||
$self->{namespace} ||= C4::Context->config('memcached_namespace') || 'koha';
|
||||
@servers = split /,/, C4::Context->config('memcached_servers') // ''
|
||||
unless @servers;
|
||||
$self->{namespace} .= ":$subnamespace:";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package Koha::Library::Calendar;
|
||||
package Koha::Calendar;
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
|
|
@ -7,14 +7,8 @@ use DateTime;
|
|||
use DateTime::Duration;
|
||||
use C4::Context;
|
||||
use Koha::Caches;
|
||||
use Koha::Database;
|
||||
use Koha::DateUtils qw( dt_from_string );
|
||||
use Koha::Exceptions;
|
||||
use Koha::Exceptions::Calendar;
|
||||
use Koha::Library::Calendar::WeeklyClosures;
|
||||
use Koha::Library::Calendar::RepeatingClosures;
|
||||
use Koha::Library::Calendar::SingleClosures;
|
||||
use Koha::Library::Calendar::Exceptions;
|
||||
|
||||
# This limit avoids an infinite loop when searching for an open day in an
|
||||
# always closed library
|
||||
|
|
@ -33,7 +27,7 @@ sub new {
|
|||
$self->{$o} = $options{$o_name};
|
||||
}
|
||||
if ( !defined $self->{branchcode} ) {
|
||||
croak 'No branchcode argument passed to Koha::Library::Calendar->new';
|
||||
croak 'No branchcode argument passed to Koha::Calendar->new';
|
||||
}
|
||||
$self->_init();
|
||||
return $self;
|
||||
|
|
@ -42,17 +36,21 @@ sub new {
|
|||
sub _init {
|
||||
my $self = shift;
|
||||
my $branch = $self->{branchcode};
|
||||
|
||||
my $dbh = C4::Context->dbh();
|
||||
my $weekly_closed_days_sth =
|
||||
$dbh->prepare('SELECT weekday FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NOT NULL');
|
||||
$weekly_closed_days_sth->execute($branch);
|
||||
$self->{weekly_closed_days} = [ 0, 0, 0, 0, 0, 0, 0 ];
|
||||
my $weekly = Koha::Library::Calendar::WeeklyClosures->search( { library_id => $branch } );
|
||||
while ( my $row = $weekly->next ) {
|
||||
$self->{weekly_closed_days}->[ $row->weekday ] = 1;
|
||||
while ( my $tuple = $weekly_closed_days_sth->fetchrow_hashref ) {
|
||||
$self->{weekly_closed_days}->[ $tuple->{weekday} ] = 1;
|
||||
}
|
||||
|
||||
my $day_month_closed_days_sth =
|
||||
$dbh->prepare('SELECT day, month FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NULL');
|
||||
$day_month_closed_days_sth->execute($branch);
|
||||
$self->{day_month_closed_days} = {};
|
||||
my $repeating = Koha::Library::Calendar::RepeatingClosures->search( { library_id => $branch } );
|
||||
while ( my $row = $repeating->next ) {
|
||||
$self->{day_month_closed_days}->{ $row->month }->{ $row->day } = 1;
|
||||
while ( my $tuple = $day_month_closed_days_sth->fetchrow_hashref ) {
|
||||
$self->{day_month_closed_days}->{ $tuple->{month} }->{ $tuple->{day} } =
|
||||
1;
|
||||
}
|
||||
|
||||
$self->{test} = 0;
|
||||
|
|
@ -68,26 +66,27 @@ sub _holidays {
|
|||
|
||||
# $holidays looks like:
|
||||
# {
|
||||
# 20131122 => 1, # single_closure
|
||||
# 20131123 => 0, # exception (open override)
|
||||
# 20131122 => 1, # single_holiday
|
||||
# 20131123 => 0, # exception_holiday
|
||||
# ...
|
||||
# }
|
||||
|
||||
# Populate the cache if necessary
|
||||
unless ($holidays) {
|
||||
my $dbh = C4::Context->dbh;
|
||||
$holidays = {};
|
||||
|
||||
my $single = Koha::Library::Calendar::SingleClosures->search( { library_id => $self->{branchcode} } );
|
||||
while ( my $row = $single->next ) {
|
||||
( my $datestring = $row->date ) =~ s/-//g;
|
||||
$holidays->{$datestring} = 1;
|
||||
}
|
||||
# Add holidays for each branch
|
||||
my $holidays_sth = $dbh->prepare(
|
||||
'SELECT day, month, year, MAX(isexception) FROM special_holidays WHERE branchcode = ? GROUP BY day, month, year'
|
||||
);
|
||||
$holidays_sth->execute( $self->{branchcode} );
|
||||
|
||||
my $exceptions = Koha::Library::Calendar::Exceptions->search( { library_id => $self->{branchcode} } );
|
||||
while ( my $row = $exceptions->next ) {
|
||||
( my $datestring = $row->date ) =~ s/-//g;
|
||||
$holidays->{$datestring} = 0;
|
||||
}
|
||||
while ( my ( $day, $month, $year, $exception ) = $holidays_sth->fetchrow ) {
|
||||
my $datestring = sprintf( "%04d", $year ) . sprintf( "%02d", $month ) . sprintf( "%02d", $day );
|
||||
|
||||
$holidays->{$datestring} = $exception ? 0 : 1;
|
||||
}
|
||||
$cache->set_in_cache( $key, $holidays, { expiry => 76800 } );
|
||||
}
|
||||
return $holidays // {};
|
||||
|
|
@ -330,22 +329,8 @@ sub days_forward {
|
|||
|
||||
my $base_dt = $start_dt->clone();
|
||||
|
||||
# Advance one open day at a time, skipping any closed days encountered
|
||||
# along the way. days_mode is deliberately not consulted here: this
|
||||
# method's purpose is to count open days forward (e.g. for a hold
|
||||
# pickup window), not to align a recurring due-date to the same
|
||||
# weekday, which is what next_open_days does under 'Dayweek'.
|
||||
while ( $num_days-- ) {
|
||||
$base_dt->add( days => 1 );
|
||||
my $i = 0;
|
||||
while ( $self->is_holiday($base_dt) && $i < OPEN_DAYS_SEARCH_MAX_ITERATIONS ) {
|
||||
$base_dt->add( days => 1 );
|
||||
++$i;
|
||||
}
|
||||
if ( $self->is_holiday($base_dt) ) {
|
||||
Koha::Exceptions::Calendar::NoOpenDays->throw(
|
||||
sprintf( 'Unable to find an open day for library %s', $self->{branchcode} ) );
|
||||
}
|
||||
$base_dt = $self->next_open_days( $base_dt, 1 );
|
||||
}
|
||||
|
||||
return $base_dt;
|
||||
|
|
@ -438,197 +423,18 @@ sub clear_weekly_closed_days {
|
|||
return;
|
||||
}
|
||||
|
||||
sub add_weekly_closure {
|
||||
my ( $self, $params ) = @_;
|
||||
my $closure =
|
||||
Koha::Library::Calendar::WeeklyClosure->new( { library_id => $self->{branchcode}, %$params } )->store;
|
||||
$self->_init;
|
||||
return $closure;
|
||||
}
|
||||
|
||||
sub add_repeating_closure {
|
||||
my ( $self, $params ) = @_;
|
||||
my $closure =
|
||||
Koha::Library::Calendar::RepeatingClosure->new( { library_id => $self->{branchcode}, %$params } )->store;
|
||||
$self->_init;
|
||||
return $closure;
|
||||
}
|
||||
|
||||
sub add_single_closure {
|
||||
my ( $self, $params ) = @_;
|
||||
return Koha::Library::Calendar::SingleClosure->new( { library_id => $self->{branchcode}, %$params } )->store;
|
||||
}
|
||||
|
||||
sub add_exception {
|
||||
my ( $self, $params ) = @_;
|
||||
return Koha::Library::Calendar::Exception->new( { library_id => $self->{branchcode}, %$params } )->store;
|
||||
}
|
||||
|
||||
sub delete_weekly_closure {
|
||||
my ( $self, $params ) = @_;
|
||||
my $row = Koha::Library::Calendar::WeeklyClosures->search(
|
||||
{ library_id => $self->{branchcode}, weekday => $params->{weekday} },
|
||||
{ rows => 1 }
|
||||
)->single;
|
||||
if ($row) {
|
||||
$row->delete;
|
||||
$self->_init;
|
||||
}
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub delete_repeating_closure {
|
||||
my ( $self, $params ) = @_;
|
||||
my $row = Koha::Library::Calendar::RepeatingClosures->search(
|
||||
{ library_id => $self->{branchcode}, day => $params->{day}, month => $params->{month} },
|
||||
{ rows => 1 }
|
||||
)->single;
|
||||
if ($row) {
|
||||
$row->delete;
|
||||
$self->_init;
|
||||
}
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub delete_single_closure {
|
||||
my ( $self, $params ) = @_;
|
||||
my $row = Koha::Library::Calendar::SingleClosures->search(
|
||||
{ library_id => $self->{branchcode}, date => $params->{date} },
|
||||
{ rows => 1 }
|
||||
)->single;
|
||||
$row->delete if $row;
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub delete_exception {
|
||||
my ( $self, $params ) = @_;
|
||||
my $row = Koha::Library::Calendar::Exceptions->search(
|
||||
{ library_id => $self->{branchcode}, date => $params->{date} },
|
||||
{ rows => 1 }
|
||||
)->single;
|
||||
$row->delete if $row;
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub closed_dates_in_range {
|
||||
my ( $self, $from_dt, $to_dt ) = @_;
|
||||
|
||||
croak 'Missing from_dt in closed_dates_in_range' unless $from_dt;
|
||||
croak 'Missing to_dt in closed_dates_in_range' unless $to_dt;
|
||||
|
||||
my $holidays = $self->_holidays;
|
||||
|
||||
my @closed;
|
||||
my $current = $from_dt->clone;
|
||||
while ( $current <= $to_dt ) {
|
||||
my $ymd = $current->ymd('');
|
||||
|
||||
# Exception (open override) wins over any other closure rule.
|
||||
if ( defined $holidays->{$ymd} && $holidays->{$ymd} == 0 ) {
|
||||
$current->add( days => 1 );
|
||||
next;
|
||||
}
|
||||
|
||||
if ( ( defined $holidays->{$ymd} && $holidays->{$ymd} == 1 )
|
||||
|| $self->{weekly_closed_days}->[ $current->day_of_week % 7 ]
|
||||
|| $self->{day_month_closed_days}->{ $current->month }->{ $current->day } )
|
||||
{
|
||||
push @closed, $current->ymd;
|
||||
}
|
||||
|
||||
$current->add( days => 1 );
|
||||
}
|
||||
|
||||
return \@closed;
|
||||
}
|
||||
|
||||
sub copy_to {
|
||||
my ( $self, $target_branchcode ) = @_;
|
||||
croak "No target_branchcode" unless $target_branchcode;
|
||||
|
||||
my $today = dt_from_string()->ymd;
|
||||
my $schema = Koha::Database->new->schema;
|
||||
|
||||
$schema->txn_do(
|
||||
sub {
|
||||
my $weekly = Koha::Library::Calendar::WeeklyClosures->search( { library_id => $self->{branchcode} } );
|
||||
while ( my $row = $weekly->next ) {
|
||||
next
|
||||
if Koha::Library::Calendar::WeeklyClosures->search(
|
||||
{ library_id => $target_branchcode, weekday => $row->weekday } )->count;
|
||||
Koha::Library::Calendar::WeeklyClosure->new(
|
||||
{
|
||||
library_id => $target_branchcode, weekday => $row->weekday,
|
||||
title => $row->title, description => $row->description,
|
||||
}
|
||||
)->store;
|
||||
}
|
||||
|
||||
my $repeating = Koha::Library::Calendar::RepeatingClosures->search( { library_id => $self->{branchcode} } );
|
||||
while ( my $row = $repeating->next ) {
|
||||
next
|
||||
if Koha::Library::Calendar::RepeatingClosures->search(
|
||||
{ library_id => $target_branchcode, day => $row->day, month => $row->month } )->count;
|
||||
Koha::Library::Calendar::RepeatingClosure->new(
|
||||
{
|
||||
library_id => $target_branchcode, day => $row->day, month => $row->month,
|
||||
title => $row->title, description => $row->description,
|
||||
}
|
||||
)->store;
|
||||
}
|
||||
|
||||
my $singles = Koha::Library::Calendar::SingleClosures->search(
|
||||
{ library_id => $self->{branchcode}, date => { '>=' => $today } } );
|
||||
while ( my $row = $singles->next ) {
|
||||
next
|
||||
if Koha::Library::Calendar::SingleClosures->search(
|
||||
{ library_id => $target_branchcode, date => $row->date } )->count;
|
||||
Koha::Library::Calendar::SingleClosure->new(
|
||||
{
|
||||
library_id => $target_branchcode, date => $row->date,
|
||||
title => $row->title, description => $row->description,
|
||||
}
|
||||
)->store;
|
||||
}
|
||||
|
||||
my $exceptions =
|
||||
Koha::Library::Calendar::Exceptions->search(
|
||||
{ library_id => $self->{branchcode}, date => { '>=' => $today } } );
|
||||
while ( my $row = $exceptions->next ) {
|
||||
next
|
||||
if Koha::Library::Calendar::Exceptions->search(
|
||||
{ library_id => $target_branchcode, date => $row->date } )->count;
|
||||
Koha::Library::Calendar::Exception->new(
|
||||
{
|
||||
library_id => $target_branchcode, date => $row->date,
|
||||
title => $row->title, description => $row->description,
|
||||
}
|
||||
)->store;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
# Per-row store() in SingleClosure/Exception flushes the target's
|
||||
# _holidays cache on insert; flush defensively here in case copy_to
|
||||
# added no new single/exception rows but the caller expects a clean
|
||||
# cache state for the target.
|
||||
Koha::Caches->get_instance()->clear_from_cache( $target_branchcode . '_holidays' );
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
1;
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Library::Calendar - Object containing a library's calendar
|
||||
Koha::Calendar - Object containing a branches calendar
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Koha::Library::Calendar
|
||||
use Koha::Calendar
|
||||
|
||||
my $c = Koha::Library::Calendar->new( branchcode => 'MAIN' );
|
||||
my $c = Koha::Calendar->new( branchcode => 'MAIN' );
|
||||
my $dt = dt_from_string();
|
||||
|
||||
# are we open
|
||||
|
|
@ -659,7 +465,7 @@ C<YYYYMMDD> date strings. Results are cached.
|
|||
|
||||
=head2 new : Create a calendar object
|
||||
|
||||
my $calendar = Koha::Library::Calendar->new( branchcode => 'MAIN' );
|
||||
my $calendar = Koha::Calendar->new( branchcode => 'MAIN' );
|
||||
|
||||
The option branchcode is required
|
||||
|
||||
|
|
@ -775,16 +581,10 @@ syspref is set to either 'Datedue', 'Calendar' or 'Dayweek'.
|
|||
|
||||
$datetime = $calendar->days_forward($start_dt, $to_add)
|
||||
|
||||
Passed a DateTime and a number of days, returns a DateTime representing that
|
||||
many open days after the starting date. Closed days (weekly closures and
|
||||
holidays) are skipped without being counted toward $to_add, so the result is
|
||||
always $to_add open days on from $start_dt.
|
||||
|
||||
Unlike L</next_open_days>, this method does not consult C<useDaysMode>: it
|
||||
always advances one calendar day at a time. It is intended for windows that
|
||||
should give the patron a fixed number of open days, such as a hold's pickup
|
||||
shelf expiration -- not for aligning a recurring due-date to a particular
|
||||
weekday.
|
||||
Passed a Datetime and number of days, returns another Datetime representing
|
||||
the next open day after adding the passed number of days. It is intended for
|
||||
use to calculate the due date when useDaysMode syspref is set to either
|
||||
'Datedue', 'Calendar' or 'Dayweek'.
|
||||
|
||||
=head2 set_daysmode
|
||||
|
||||
|
|
@ -799,83 +599,9 @@ allow testing of more configurations
|
|||
=head2 add_holiday
|
||||
|
||||
Passed a datetime object this will add it to the calendar's list of
|
||||
closed days. This is for testing so that we can alter the Calendar object's
|
||||
closed days. This is for testing so that we can alter the Calenfar object's
|
||||
list of specified dates
|
||||
|
||||
=head2 CRUD methods
|
||||
|
||||
=head3 add_weekly_closure
|
||||
|
||||
$calendar->add_weekly_closure({ weekday => 0, title => 'Sundays', description => '' });
|
||||
|
||||
=head3 add_repeating_closure
|
||||
|
||||
$calendar->add_repeating_closure({ day => 25, month => 12, title => 'Christmas', description => '' });
|
||||
|
||||
=head3 add_single_closure
|
||||
|
||||
$calendar->add_single_closure({ date => '2026-06-15', title => 'Staff day', description => '' });
|
||||
|
||||
=head3 add_exception
|
||||
|
||||
$calendar->add_exception({ date => '2026-12-25', title => 'Special opening', description => '' });
|
||||
|
||||
=head3 delete_weekly_closure
|
||||
|
||||
$calendar->delete_weekly_closure({ weekday => 0 });
|
||||
|
||||
=head3 delete_repeating_closure
|
||||
|
||||
$calendar->delete_repeating_closure({ day => 25, month => 12 });
|
||||
|
||||
=head3 delete_single_closure
|
||||
|
||||
$calendar->delete_single_closure({ date => '2026-06-15' });
|
||||
|
||||
=head3 delete_exception
|
||||
|
||||
$calendar->delete_exception({ date => '2026-12-25' });
|
||||
|
||||
=head3 closed_dates_in_range
|
||||
|
||||
my $dates = $calendar->closed_dates_in_range($from_dt, $to_dt);
|
||||
|
||||
Returns an arrayref of ISO date strings (YYYY-MM-DD) representing every
|
||||
closed day between C<$from_dt> and C<$to_dt> (inclusive), taking
|
||||
weekly, annual-repeating, single and exception (open override)
|
||||
closures into account. Exceptions win: a date listed as an exception
|
||||
is never returned even if a weekly or repeating rule would otherwise
|
||||
close it.
|
||||
|
||||
=head3 copy_to
|
||||
|
||||
$calendar->copy_to($target_branchcode);
|
||||
|
||||
Copies closure rules to another branch inside a single transaction.
|
||||
|
||||
Weekly and repeating closures are always copied. Single closures and
|
||||
exceptions are only copied when their date is today or later (past
|
||||
dates are filtered out by design).
|
||||
|
||||
Rows already present on the target (matched by (library_id, weekday),
|
||||
(library_id, day, month) or (library_id, date)) are skipped so the
|
||||
operation is idempotent.
|
||||
|
||||
Flushes the target library's C<_holidays> cache before returning.
|
||||
|
||||
=head2 Internal methods
|
||||
|
||||
=head3 _init
|
||||
|
||||
Loads weekly closed days and repeating annual closures from the database
|
||||
into the object's internal data structures.
|
||||
|
||||
=head3 _holidays
|
||||
|
||||
Returns a hashref of single closures and exceptions keyed by YYYYMMDD.
|
||||
Values are 1 for closures and 0 for exceptions (open overrides).
|
||||
Results are cached in memcache.
|
||||
|
||||
=head1 DIAGNOSTICS
|
||||
|
||||
Will croak if not passed a branchcode in new
|
||||
|
|
@ -16,19 +16,12 @@ package Koha::Cash::Register;
|
|||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
use DateTime;
|
||||
use Scalar::Util qw( looks_like_number );
|
||||
use Try::Tiny qw( catch try );
|
||||
|
||||
use Koha::Account;
|
||||
use Koha::Account::Line;
|
||||
use Koha::Account::Lines;
|
||||
use Koha::Account::Offset;
|
||||
use Koha::Account::Offsets;
|
||||
use Koha::Cash::Register::Actions;
|
||||
use Koha::Cash::Register::Cashups;
|
||||
use Koha::Database;
|
||||
use Koha::DateUtils qw( dt_from_string );
|
||||
|
||||
use base qw(Koha::Object);
|
||||
|
||||
|
|
@ -119,14 +112,19 @@ Return a set of accountlines linked to this cash register since the last cashup
|
|||
sub outstanding_accountlines {
|
||||
my ( $self, $conditions, $attrs ) = @_;
|
||||
|
||||
# Find the start timestamp for the current "open" session
|
||||
my $start_timestamp = $self->_get_session_start_timestamp;
|
||||
my $since = $self->_result->search_related(
|
||||
'cash_register_actions',
|
||||
{ 'code' => 'CASHUP' },
|
||||
{
|
||||
order_by => { '-desc' => [ 'timestamp', 'id' ] },
|
||||
rows => 1
|
||||
}
|
||||
);
|
||||
|
||||
my $local_conditions =
|
||||
$start_timestamp
|
||||
? { 'date' => { '>' => $start_timestamp } }
|
||||
$since->count
|
||||
? { 'date' => { '>' => $since->get_column('timestamp')->as_query } }
|
||||
: {};
|
||||
|
||||
my $merged_conditions =
|
||||
$conditions
|
||||
? { %{$conditions}, %{$local_conditions} }
|
||||
|
|
@ -140,40 +138,6 @@ sub outstanding_accountlines {
|
|||
return Koha::Account::Lines->_new_from_dbic($rs);
|
||||
}
|
||||
|
||||
=head3 cashup_in_progress
|
||||
|
||||
Check if there is currently a cashup in progress (CASHUP_START without corresponding CASHUP).
|
||||
Returns the CASHUP_START action if in progress, undef otherwise.
|
||||
|
||||
=cut
|
||||
|
||||
sub cashup_in_progress {
|
||||
my ($self) = @_;
|
||||
|
||||
my $last_start = $self->_result->search_related(
|
||||
'cash_register_actions',
|
||||
{ 'code' => 'CASHUP_START' },
|
||||
{ order_by => { '-desc' => [ 'timestamp', 'id' ] }, rows => 1 }
|
||||
)->single;
|
||||
|
||||
return unless $last_start;
|
||||
|
||||
my $last_completion = $self->cashups(
|
||||
{},
|
||||
{ order_by => { '-desc' => [ 'timestamp', 'id' ] }, rows => 1 }
|
||||
)->single;
|
||||
|
||||
# If we have a start but no completion, or the start is more recent than completion
|
||||
if ( !$last_completion
|
||||
|| DateTime->compare( dt_from_string( $last_start->timestamp ), dt_from_string( $last_completion->timestamp ) )
|
||||
> 0 )
|
||||
{
|
||||
return Koha::Cash::Register::Action->_new_from_dbic($last_start);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
=head3 store
|
||||
|
||||
Local store method to prevent direct manipulation of the 'branch_default' field
|
||||
|
|
@ -240,380 +204,31 @@ sub drop_default {
|
|||
return $self;
|
||||
}
|
||||
|
||||
=head3 start_cashup
|
||||
|
||||
my $cashup_start = $cash_register->start_cashup(
|
||||
{
|
||||
manager_id => $logged_in_user->id,
|
||||
}
|
||||
);
|
||||
|
||||
Start a new cashup period. This marks the beginning of the cash counting process
|
||||
and creates a snapshot point for calculating outstanding amounts. Returns the
|
||||
CASHUP_START action.
|
||||
|
||||
=cut
|
||||
|
||||
sub start_cashup {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
# check for mandatory params
|
||||
my @mandatory = ('manager_id');
|
||||
for my $param (@mandatory) {
|
||||
unless ( defined( $params->{$param} ) ) {
|
||||
Koha::Exceptions::MissingParameter->throw( error => "The $param parameter is mandatory" );
|
||||
}
|
||||
}
|
||||
my $manager_id = $params->{manager_id};
|
||||
|
||||
# Check if there's already a cashup in progress
|
||||
my $last_cashup_start_rs = $self->_result->search_related(
|
||||
'cash_register_actions',
|
||||
{ 'code' => 'CASHUP_START' },
|
||||
{ order_by => { '-desc' => [ 'timestamp', 'id' ] }, rows => 1 }
|
||||
)->single;
|
||||
|
||||
my $last_cashup_completed = $self->cashups(
|
||||
{},
|
||||
{ order_by => { '-desc' => [ 'timestamp', 'id' ] }, rows => 1 }
|
||||
)->single;
|
||||
|
||||
# If we have a CASHUP_START that's more recent than the last CASHUP, there's already an active cashup
|
||||
if (
|
||||
$last_cashup_start_rs
|
||||
&& (
|
||||
!$last_cashup_completed || DateTime->compare(
|
||||
dt_from_string( $last_cashup_start_rs->timestamp ),
|
||||
dt_from_string( $last_cashup_completed->timestamp )
|
||||
) > 0
|
||||
)
|
||||
)
|
||||
{
|
||||
Koha::Exceptions::Object::DuplicateID->throw( error => "A cashup is already in progress for this register" );
|
||||
}
|
||||
|
||||
my $expected_amount = $self->outstanding_accountlines->total( { payment_type => [ 'CASH', 'SIP00' ] } ) * -1;
|
||||
|
||||
# Prevent starting a cashup when there are no transactions at all
|
||||
my $total_transactions = $self->outstanding_accountlines->total() * -1;
|
||||
unless ( $total_transactions != 0 ) {
|
||||
Koha::Exceptions::Object::BadValue->throw(
|
||||
error => "Cannot start cashup with no transactions",
|
||||
type => 'amount',
|
||||
value => $total_transactions
|
||||
);
|
||||
}
|
||||
|
||||
# Create the CASHUP_START action, translating raw DBIC exceptions
|
||||
my $schema = $self->_result->result_source->schema;
|
||||
my $rs;
|
||||
try {
|
||||
$rs = $self->_result->add_to_cash_register_actions(
|
||||
{
|
||||
code => 'CASHUP_START',
|
||||
manager_id => $manager_id,
|
||||
amount => $expected_amount
|
||||
}
|
||||
)->discard_changes;
|
||||
} catch {
|
||||
$schema->translate_exception($_) if ref($_) eq 'DBIx::Class::Exception';
|
||||
$_->rethrow();
|
||||
};
|
||||
|
||||
return Koha::Cash::Register::Cashup->_new_from_dbic($rs);
|
||||
}
|
||||
|
||||
=head3 add_cashup
|
||||
|
||||
my $cashup = $cash_register->add_cashup(
|
||||
{
|
||||
manager_id => $logged_in_user->id,
|
||||
amount => $amount_removed_from_register,
|
||||
[ reconciliation_note => $reconciliation_note ]
|
||||
manager_id => $logged_in_user->id,
|
||||
amount => $cash_register->outstanding_accountlines->total
|
||||
}
|
||||
);
|
||||
|
||||
Complete a cashup period started with start_cashup(). This performs the actual
|
||||
reconciliation against the amount counted and creates surplus/deficit accountlines
|
||||
if needed. Returns the completed CASHUP action.
|
||||
Add a new cashup action to the till, returns the added action.
|
||||
|
||||
=cut
|
||||
|
||||
sub add_cashup {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
# check for mandatory params
|
||||
my @mandatory = ( 'manager_id', 'amount' );
|
||||
for my $param (@mandatory) {
|
||||
unless ( defined( $params->{$param} ) ) {
|
||||
Koha::Exceptions::MissingParameter->throw( error => "The $param parameter is mandatory" );
|
||||
}
|
||||
}
|
||||
my $manager_id = $params->{manager_id};
|
||||
|
||||
# Validate amount is a valid number
|
||||
my $amount = $params->{amount};
|
||||
unless ( looks_like_number($amount) ) {
|
||||
Koha::Exceptions::Account::AmountNotPositive->throw( error => 'Cashup amount must be a valid number' );
|
||||
}
|
||||
|
||||
# Sanitize reconciliation note - treat empty/whitespace-only as undef
|
||||
my $reconciliation_note = $params->{reconciliation_note};
|
||||
if ( defined $reconciliation_note ) {
|
||||
$reconciliation_note = substr( $reconciliation_note, 0, 1000 ); # Limit length
|
||||
$reconciliation_note =~ s/^\s+|\s+$//g; # Trim whitespace
|
||||
$reconciliation_note = undef if $reconciliation_note eq ''; # Empty after trim = undef
|
||||
}
|
||||
|
||||
# Find the most recent CASHUP_START to determine if we're in two-phase mode
|
||||
my $cashup_start;
|
||||
my $cashup_start_rs = $self->_result->search_related(
|
||||
'cash_register_actions',
|
||||
{ 'code' => 'CASHUP_START' },
|
||||
{ order_by => { '-desc' => [ 'timestamp', 'id' ] }, rows => 1 }
|
||||
)->single;
|
||||
|
||||
if ($cashup_start_rs) {
|
||||
|
||||
# Two-phase mode: Check if this CASHUP_START has already been completed
|
||||
my $existing_completion = $self->_result->search_related(
|
||||
'cash_register_actions',
|
||||
{
|
||||
'code' => 'CASHUP',
|
||||
'timestamp' => { '>' => $cashup_start_rs->timestamp }
|
||||
},
|
||||
{ rows => 1 }
|
||||
)->single;
|
||||
|
||||
if ( !$existing_completion ) {
|
||||
$cashup_start = Koha::Cash::Register::Cashup->_new_from_dbic($cashup_start_rs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
# Calculate expected amount from session accountlines
|
||||
my $expected_amount = (
|
||||
$cashup_start
|
||||
? $cashup_start->accountlines->total( { payment_type => [ 'CASH', 'SIP00' ] } )
|
||||
: $self->outstanding_accountlines->total( { payment_type => [ 'CASH', 'SIP00' ] } )
|
||||
) * -1;
|
||||
|
||||
# Calculate difference (actual - expected)
|
||||
my $difference = $amount - $expected_amount;
|
||||
|
||||
# Validate reconciliation note requirement if there's a discrepancy
|
||||
if ( $difference != 0 ) {
|
||||
my $note_required = C4::Context->preference('CashupReconciliationNoteRequired') // 0;
|
||||
|
||||
if ( $note_required && !defined $reconciliation_note ) {
|
||||
Koha::Exceptions::MissingParameter->throw(
|
||||
error => "Reconciliation note is required when cashup amount differs from expected amount" );
|
||||
}
|
||||
|
||||
# If notes are restricted to an authorised value category, reject anything
|
||||
# that isn't one of the allowed values (the UI only offers a matching
|
||||
# select, but the API/form param isn't otherwise constrained server-side)
|
||||
my $note_av_category = C4::Context->preference('CashupReconciliationNoteAuthorisedValue');
|
||||
if ( $note_av_category && defined $reconciliation_note ) {
|
||||
require Koha::AuthorisedValues;
|
||||
my $valid = Koha::AuthorisedValues->search(
|
||||
{
|
||||
category => $note_av_category,
|
||||
authorised_value => $reconciliation_note,
|
||||
}
|
||||
)->count;
|
||||
unless ($valid) {
|
||||
Koha::Exceptions::BadParameter->throw(
|
||||
error => "Reconciliation note is not a valid authorised value for this category",
|
||||
parameter => 'reconciliation_note',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Use database transaction to ensure consistency
|
||||
my $schema = $self->_result->result_source->schema;
|
||||
my $cashup;
|
||||
|
||||
$schema->txn_do(
|
||||
sub {
|
||||
try {
|
||||
# Create the cashup action (raw DBIC call; Koha::Object stores below are already translated)
|
||||
my $rs = $self->_result->add_to_cash_register_actions(
|
||||
{
|
||||
code => 'CASHUP',
|
||||
manager_id => $manager_id,
|
||||
amount => $amount
|
||||
}
|
||||
)->discard_changes;
|
||||
$cashup = Koha::Cash::Register::Cashup->_new_from_dbic($rs);
|
||||
|
||||
# Create reconciliation accountline if there's a difference
|
||||
if ( $difference != 0 ) {
|
||||
|
||||
# Determine reconciliation date based on mode
|
||||
my $reconciliation_date;
|
||||
if ($cashup_start) {
|
||||
|
||||
# Two-phase mode: Backdate reconciliation lines to just before the CASHUP_START timestamp
|
||||
# This ensures they belong to the previous session, not the current one
|
||||
$reconciliation_date = \[ 'DATE_SUB(?, INTERVAL 1 SECOND)', $cashup_start->timestamp ];
|
||||
} else {
|
||||
|
||||
# Legacy mode: Use the original backdating approach
|
||||
$reconciliation_date = \'DATE_SUB(NOW(), INTERVAL 1 SECOND)';
|
||||
}
|
||||
|
||||
if ( $difference > 0 ) {
|
||||
|
||||
# Surplus: more cash found than expected (credits are negative amounts)
|
||||
my $surplus = Koha::Account::Line->new(
|
||||
{
|
||||
date => $reconciliation_date,
|
||||
amount => -abs($difference), # Credits are negative
|
||||
amountoutstanding => 0,
|
||||
credit_type_code => 'CASHUP_SURPLUS',
|
||||
manager_id => $manager_id,
|
||||
interface => 'intranet',
|
||||
branchcode => $self->branch,
|
||||
register_id => $self->id,
|
||||
payment_type => 'CASH',
|
||||
note => $reconciliation_note
|
||||
}
|
||||
)->store();
|
||||
|
||||
# Record the account offset
|
||||
my $account_offset = Koha::Account::Offset->new(
|
||||
{
|
||||
credit_id => $surplus->id,
|
||||
type => 'CREATE',
|
||||
amount => -abs($difference) # Offsets match the line amount
|
||||
}
|
||||
)->store();
|
||||
|
||||
} else {
|
||||
|
||||
# Deficit: less cash found than expected
|
||||
my $deficit = Koha::Account::Line->new(
|
||||
{
|
||||
date => $reconciliation_date,
|
||||
amount => abs($difference),
|
||||
amountoutstanding => 0,
|
||||
debit_type_code => 'CASHUP_DEFICIT',
|
||||
manager_id => $manager_id,
|
||||
interface => 'intranet',
|
||||
branchcode => $self->branch,
|
||||
register_id => $self->id,
|
||||
payment_type => 'CASH',
|
||||
note => $reconciliation_note
|
||||
}
|
||||
)->store();
|
||||
my $account_offset = Koha::Account::Offset->new(
|
||||
{
|
||||
debit_id => $deficit->id,
|
||||
type => 'CREATE',
|
||||
amount => abs($difference) # Debits have positive offsets
|
||||
}
|
||||
)->store();
|
||||
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
$schema->translate_exception($_) if ref($_) eq 'DBIx::Class::Exception';
|
||||
$_->rethrow();
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return $cashup;
|
||||
}
|
||||
|
||||
=head3 _get_session_start_timestamp
|
||||
|
||||
Internal method to determine the start timestamp for the current "open" session.
|
||||
This handles the following cashup scenarios:
|
||||
|
||||
=over 4
|
||||
|
||||
=item 1. No cashups ever → undef (returns all accountlines)
|
||||
|
||||
=item 2. Quick cashup completed → Uses CASHUP timestamp
|
||||
|
||||
=item 3. Two-phase started → Uses CASHUP_START timestamp
|
||||
|
||||
=item 4. Two-phase completed → Uses the CASHUP_START timestamp that led to the last CASHUP
|
||||
|
||||
=item 5. Mixed workflows → Correctly distinguishes between quick and two-phase cashups
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub _get_session_start_timestamp {
|
||||
my ($self) = @_;
|
||||
|
||||
# Check if there's a cashup in progress (CASHUP_START without corresponding CASHUP)
|
||||
my $cashup_in_progress = $self->cashup_in_progress;
|
||||
|
||||
if ($cashup_in_progress) {
|
||||
|
||||
# Scenario 3: Two-phase cashup started - return accountlines since CASHUP_START
|
||||
return $cashup_in_progress->timestamp;
|
||||
}
|
||||
|
||||
# No cashup in progress - find the most recent cashup completion
|
||||
my $last_cashup = $self->cashups(
|
||||
{},
|
||||
my $rs = $self->_result->add_to_cash_register_actions(
|
||||
{
|
||||
order_by => { '-desc' => [ 'timestamp', 'id' ] },
|
||||
rows => 1
|
||||
code => 'CASHUP',
|
||||
manager_id => $params->{manager_id},
|
||||
amount => $params->{amount}
|
||||
}
|
||||
)->single;
|
||||
)->discard_changes;
|
||||
|
||||
if ( !$last_cashup ) {
|
||||
|
||||
# Scenario 1: No cashups have ever taken place - return all accountlines
|
||||
return;
|
||||
}
|
||||
|
||||
# Find if this CASHUP was part of a two-phase workflow
|
||||
my $corresponding_start = $self->_result->search_related(
|
||||
'cash_register_actions',
|
||||
{
|
||||
'code' => 'CASHUP_START',
|
||||
'timestamp' => { '<' => $last_cashup->timestamp }
|
||||
},
|
||||
{
|
||||
order_by => { '-desc' => [ 'timestamp', 'id' ] },
|
||||
rows => 1
|
||||
}
|
||||
)->single;
|
||||
|
||||
if ($corresponding_start) {
|
||||
|
||||
# Check if this CASHUP_START was completed by this CASHUP
|
||||
# (no other CASHUP between them)
|
||||
my $intervening_cashup = $self->_result->search_related(
|
||||
'cash_register_actions',
|
||||
{
|
||||
'code' => 'CASHUP',
|
||||
'timestamp' => {
|
||||
'>' => $corresponding_start->timestamp,
|
||||
'<' => $last_cashup->timestamp
|
||||
}
|
||||
},
|
||||
{ rows => 1 }
|
||||
)->single;
|
||||
|
||||
if ( !$intervening_cashup ) {
|
||||
|
||||
# Scenario 4: Two-phase cashup completed - return accountlines since the CASHUP_START
|
||||
return $corresponding_start->timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
# Scenarios 2 & 5: Quick cashup (or orphaned CASHUP) - return accountlines since CASHUP
|
||||
return $last_cashup->timestamp;
|
||||
return Koha::Cash::Register::Cashup->_new_from_dbic($rs);
|
||||
}
|
||||
|
||||
=head3 to_api_mapping
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use base qw(Koha::Cash::Register::Action);
|
|||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Cash::Register::Cashup - Koha Cash Register Cashup Object class
|
||||
Koha::Cash::Register::Actions - Koha Cash Register Action Object set class
|
||||
|
||||
=head1 API
|
||||
|
||||
|
|
@ -61,43 +61,29 @@ Return a hashref containing a summary of transactions that make up this cashup.
|
|||
sub summary {
|
||||
my ($self) = @_;
|
||||
my $summary;
|
||||
my $prior_cashup = Koha::Cash::Register::Cashups->search(
|
||||
{
|
||||
'timestamp' => { '<' => $self->timestamp },
|
||||
register_id => $self->register_id
|
||||
},
|
||||
{
|
||||
order_by => { '-desc' => [ 'timestamp', 'id' ] },
|
||||
rows => 1
|
||||
}
|
||||
);
|
||||
|
||||
# Get the session boundaries for this cashup
|
||||
my ( $session_start, $session_end ) = $self->_get_session_boundaries;
|
||||
my $previous = $prior_cashup->single;
|
||||
|
||||
my $conditions;
|
||||
if ( $session_start && $session_end ) {
|
||||
|
||||
# Complete session: between start and end (exclusive)
|
||||
$conditions = {
|
||||
'date' => {
|
||||
'>' => $session_start,
|
||||
'<' => $session_end
|
||||
}
|
||||
};
|
||||
} elsif ($session_end) {
|
||||
|
||||
# Session from beginning to end
|
||||
$conditions = { 'date' => { '<' => $session_end } };
|
||||
} else {
|
||||
|
||||
# Shouldn't happen for a completed cashup, but fallback
|
||||
$conditions = { 'date' => { '<' => $self->timestamp } };
|
||||
}
|
||||
my $conditions =
|
||||
$previous
|
||||
? { 'date' => { '-between' => [ $previous->_result->get_column('timestamp'), $self->timestamp ] } }
|
||||
: { 'date' => { '<' => $self->timestamp } };
|
||||
|
||||
my $payout_transactions = $self->register->accountlines->search(
|
||||
{
|
||||
%{$conditions},
|
||||
credit_type_code => undef,
|
||||
debit_type_code => { '!=' => 'CASHUP_DEFICIT' }
|
||||
},
|
||||
{ %{$conditions}, credit_type_code => undef },
|
||||
);
|
||||
my $income_transactions = $self->register->accountlines->search(
|
||||
{
|
||||
%{$conditions},
|
||||
debit_type_code => undef,
|
||||
credit_type_code => { '!=' => 'CASHUP_SURPLUS' }
|
||||
},
|
||||
{ %{$conditions}, debit_type_code => undef },
|
||||
);
|
||||
|
||||
my $income_summary = Koha::Account::Offsets->search(
|
||||
|
|
@ -184,168 +170,23 @@ sub summary {
|
|||
my $typed_income = $income_transactions->total( { payment_type => $type->authorised_value } );
|
||||
my $typed_payout = $payout_transactions->total( { payment_type => $type->authorised_value } );
|
||||
my $typed_total = ( $typed_income + $typed_payout );
|
||||
|
||||
# Flip sign to match the convention used for `total` above:
|
||||
# positive = net amount collected (i.e. removed from register at cashup),
|
||||
# negative = net amount paid out (i.e. needed to be added to register).
|
||||
push @total_grouped, { payment_type => $type->lib, total => $typed_total * -1 };
|
||||
push @total_grouped, { payment_type => $type->lib, total => $typed_total };
|
||||
}
|
||||
|
||||
# Check for reconciliation lines separately (for footer display only)
|
||||
my $surplus_lines =
|
||||
$self->register->accountlines->search( { %{$conditions}, credit_type_code => 'CASHUP_SURPLUS' } );
|
||||
my $deficit_lines =
|
||||
$self->register->accountlines->search( { %{$conditions}, debit_type_code => 'CASHUP_DEFICIT' } );
|
||||
|
||||
my $surplus_total = $surplus_lines->count ? $surplus_lines->total : undef;
|
||||
my $deficit_total = $deficit_lines->count ? $deficit_lines->total : undef;
|
||||
|
||||
# Extract notes from reconciliation lines
|
||||
my ($surplus_record) = $surplus_lines->_resultset->search( {}, { rows => 1 } )->all;
|
||||
my $surplus_note = $surplus_record ? $surplus_record->note : undef;
|
||||
|
||||
my ($deficit_record) = $deficit_lines->_resultset->search( {}, { rows => 1 } )->all;
|
||||
my $deficit_note = $deficit_record ? $deficit_record->note : undef;
|
||||
|
||||
$summary = {
|
||||
from_date => $session_start,
|
||||
to_date => $session_end,
|
||||
from_date => $previous ? $previous->timestamp : undef,
|
||||
to_date => $self->timestamp,
|
||||
income_grouped => \@income,
|
||||
income_total => abs($income_total),
|
||||
payout_grouped => \@payout,
|
||||
payout_total => abs($payout_total),
|
||||
total => $total * -1,
|
||||
total_grouped => \@total_grouped,
|
||||
|
||||
# Reconciliation data for footer display
|
||||
surplus_total => $surplus_total ? $surplus_total * 1 : undef,
|
||||
deficit_total => $deficit_total ? $deficit_total * 1 : undef,
|
||||
surplus_note => $surplus_note,
|
||||
deficit_note => $deficit_note
|
||||
total_grouped => \@total_grouped
|
||||
};
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
=head3 accountlines
|
||||
|
||||
Fetch the accountlines associated with this cashup
|
||||
|
||||
=cut
|
||||
|
||||
sub accountlines {
|
||||
my ($self) = @_;
|
||||
|
||||
# Get the session boundaries for this cashup
|
||||
my ( $session_start, $session_end ) = $self->_get_session_boundaries;
|
||||
|
||||
my $conditions;
|
||||
if ( $session_start && $session_end ) {
|
||||
|
||||
# Complete session: between start and end (exclusive)
|
||||
$conditions = {
|
||||
'date' => {
|
||||
'>' => $session_start,
|
||||
'<' => $session_end
|
||||
}
|
||||
};
|
||||
} elsif ($session_end) {
|
||||
|
||||
# Session from beginning to end
|
||||
$conditions = { 'date' => { '<' => $session_end } };
|
||||
} else {
|
||||
|
||||
# Shouldn't happen for a completed cashup, but fallback
|
||||
$conditions = { 'date' => { '<' => $self->timestamp } };
|
||||
}
|
||||
|
||||
return $self->register->accountlines->search($conditions);
|
||||
}
|
||||
|
||||
=head3 _get_session_boundaries
|
||||
|
||||
Internal method to determine the session boundaries for this cashup.
|
||||
Returns ($session_start, $session_end) timestamps.
|
||||
|
||||
=cut
|
||||
|
||||
sub _get_session_boundaries {
|
||||
my ($self) = @_;
|
||||
|
||||
my $session_end = $self->_get_session_end;
|
||||
|
||||
# Find the previous CASHUP
|
||||
my $session_start;
|
||||
my $previous_cashup = $self->register->cashups(
|
||||
{ 'timestamp' => { '<' => $session_end } },
|
||||
{
|
||||
order_by => { '-desc' => [ 'timestamp', 'id' ] },
|
||||
rows => 1
|
||||
}
|
||||
)->single;
|
||||
|
||||
$session_start = $previous_cashup ? $previous_cashup->_get_session_end : undef;
|
||||
|
||||
return ( $session_start, $session_end );
|
||||
}
|
||||
|
||||
=head3 _get_session_end
|
||||
|
||||
Internal method to determine the effective end timestamp of the session
|
||||
this cashup closes.
|
||||
|
||||
For a one-phase cashup, the session ends at C<< $self->timestamp >>.
|
||||
For a two-phase workflow (a preceding C<CASHUP_START> with no intervening
|
||||
C<CASHUP>), the session ends at the C<CASHUP_START> timestamp so that
|
||||
transactions made between C<CASHUP_START> and C<CASHUP> are excluded
|
||||
from this cashup's summary.
|
||||
|
||||
Returns the session end timestamp.
|
||||
|
||||
=cut
|
||||
|
||||
sub _get_session_end {
|
||||
my ($self) = @_;
|
||||
|
||||
my $session_end = $self->timestamp;
|
||||
|
||||
# Find if this CASHUP was part of a two-phase workflow
|
||||
my $nearest_start = $self->register->_result->search_related(
|
||||
'cash_register_actions',
|
||||
{
|
||||
'code' => 'CASHUP_START',
|
||||
'timestamp' => { '<' => $session_end }
|
||||
},
|
||||
{
|
||||
order_by => { '-desc' => [ 'timestamp', 'id' ] },
|
||||
rows => 1
|
||||
}
|
||||
)->single;
|
||||
|
||||
if ($nearest_start) {
|
||||
|
||||
# Check if this CASHUP_START was completed by this CASHUP
|
||||
# (no other CASHUP between them)
|
||||
my $intervening_cashup = $self->register->cashups(
|
||||
{
|
||||
'timestamp' => {
|
||||
'>' => $nearest_start->timestamp,
|
||||
'<' => $session_end
|
||||
}
|
||||
},
|
||||
{ rows => 1 }
|
||||
)->single;
|
||||
|
||||
if ( !$intervening_cashup ) {
|
||||
|
||||
# Two-phase workflow: session runs to CASHUP_START
|
||||
$session_end = $nearest_start->timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
return $session_end;
|
||||
}
|
||||
|
||||
=head3 to_api_mapping
|
||||
|
||||
This method returns the mapping for representing a Koha::Cash::Register::Cashup object
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ use base qw(Koha::Cash::Register::Actions);
|
|||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Cash::Register::Cashups - Koha Cash Register Cashup Object set class
|
||||
Koha::Cash::Register::Actions - Koha Cash Register Action Object set class
|
||||
|
||||
=head1 API
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ use Modern::Perl;
|
|||
|
||||
use Carp;
|
||||
|
||||
use Koha::Calendar;
|
||||
use Koha::DateUtils qw( dt_from_string );
|
||||
use Koha::Exceptions;
|
||||
use Koha::Library::Calendar;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ sub accumulate_rentalcharge {
|
|||
return 0 unless $rentalcharge_increment && $rentalcharge_increment > 0;
|
||||
|
||||
my $duration;
|
||||
my $calendar = Koha::Library::Calendar->new( branchcode => $self->library->id );
|
||||
my $calendar = Koha::Calendar->new( branchcode => $self->library->id );
|
||||
|
||||
if ( $units eq 'hours' ) {
|
||||
if ( $itemtype->rentalcharge_hourly_calendar ) {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ sub calculate_dropbox_date {
|
|||
branchcode => $branchcode,
|
||||
}
|
||||
);
|
||||
my $calendar = Koha::Library::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
|
||||
my $calendar = Koha::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
|
||||
my $today = dt_from_string;
|
||||
my $dropbox_date = $calendar->addDuration( $today, -1 );
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ sub automatic_checkin {
|
|||
}
|
||||
}
|
||||
|
||||
=head3 _type
|
||||
=head3 type
|
||||
|
||||
=cut
|
||||
|
||||
|
|
|
|||
|
|
@ -186,22 +186,6 @@ our $RULE_KINDS = {
|
|||
is_monetary => 1,
|
||||
can_be_blank => 1,
|
||||
},
|
||||
overdue_X_delay => {
|
||||
scope => [ 'branchcode', 'categorycode', 'itemtype' ],
|
||||
can_be_blank => 0,
|
||||
},
|
||||
overdue_X_mtt => {
|
||||
scope => [ 'branchcode', 'categorycode', 'itemtype' ],
|
||||
can_be_blank => 1,
|
||||
},
|
||||
overdue_X_notice => {
|
||||
scope => [ 'branchcode', 'categorycode', 'itemtype' ],
|
||||
can_be_blank => 1,
|
||||
},
|
||||
overdue_X_restrict => {
|
||||
scope => [ 'branchcode', 'categorycode', 'itemtype' ],
|
||||
can_be_blank => 0,
|
||||
},
|
||||
renewalperiod => {
|
||||
scope => [ 'branchcode', 'categorycode', 'itemtype' ],
|
||||
},
|
||||
|
|
@ -402,8 +386,7 @@ sub set_rule {
|
|||
unless exists $params->{$mandatory_parameter};
|
||||
}
|
||||
|
||||
( my $rule_key = $params->{rule_name} ) =~ s/_\d+_/_X_/;
|
||||
my $kind_info = $RULE_KINDS->{$rule_key};
|
||||
my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
|
||||
Koha::Exceptions::MissingParameter->throw("set_rule given unknown rule '$params->{rule_name}'!")
|
||||
unless defined $kind_info;
|
||||
|
||||
|
|
@ -819,7 +802,7 @@ sub get_effective_expire_reserves_charge {
|
|||
|
||||
}
|
||||
|
||||
=head3 _type
|
||||
=head3 type
|
||||
|
||||
=cut
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ use base qw(Koha::Object);
|
|||
use C4::Circulation qw( CanBookBeIssued AddIssue );
|
||||
use C4::Members::Messaging qw( GetMessagingPreferences );
|
||||
use C4::Letters qw( GetPreparedLetter EnqueueLetter );
|
||||
use Koha::Library::Calendar;
|
||||
use Koha::Calendar;
|
||||
use Koha::DateUtils qw( dt_from_string );
|
||||
use Koha::Patron;
|
||||
use Koha::Library;
|
||||
|
|
@ -56,7 +56,7 @@ sub new {
|
|||
Koha::Exceptions::CurbsidePickup::NotEnabled->throw
|
||||
unless $policy && $policy->enabled;
|
||||
|
||||
my $calendar = Koha::Library::Calendar->new( branchcode => $params->{branchcode} );
|
||||
my $calendar = Koha::Calendar->new( branchcode => $params->{branchcode} );
|
||||
Koha::Exceptions::CurbsidePickup::LibraryIsClosed->throw
|
||||
if $calendar->is_holiday( $params->{scheduled_pickup_datetime} );
|
||||
|
||||
|
|
|
|||
|
|
@ -68,8 +68,7 @@ sub invalid_item_library {
|
|||
|
||||
=head2 ids
|
||||
|
||||
Build a set of biblionumbers suitable for an IN search from a Koha::Object or Koha::Objects-based object.
|
||||
Returns a subquery for a Koha::Objects set and an arrayref for a single Koha::Object.
|
||||
Build a hashref of IDs from a Koha::Object Koha::Objects-based object.
|
||||
|
||||
=cut
|
||||
|
||||
|
|
@ -77,8 +76,8 @@ sub ids {
|
|||
my ( $self, $object ) = @_;
|
||||
if ( $object->can('_resultset') ) {
|
||||
|
||||
# It's a Koha::Objects, don't materialize the ids, let the database resolve the set
|
||||
return $object->_resultset->get_column('biblionumber')->as_query;
|
||||
# It's a Koha::Objects
|
||||
return [ $object->get_column('biblionumber') ];
|
||||
} else {
|
||||
|
||||
# It's a single Koha::Object
|
||||
|
|
@ -99,7 +98,7 @@ sub no_item_type {
|
|||
if ( C4::Context->preference('item-level_itypes') ) {
|
||||
my $items_without_itype = Koha::Items->search(
|
||||
{
|
||||
biblionumber => { -in => $ids },
|
||||
biblionumber => $ids,
|
||||
-or => [ itype => undef, itype => '' ]
|
||||
}
|
||||
);
|
||||
|
|
@ -123,7 +122,7 @@ sub no_item_type {
|
|||
} else {
|
||||
my $biblioitems_without_itemtype = Koha::Biblioitems->search(
|
||||
{
|
||||
biblionumber => { -in => $ids },
|
||||
biblionumber => $ids,
|
||||
-or => [ itemtype => undef, itemtype => '' ]
|
||||
}
|
||||
);
|
||||
|
|
@ -153,7 +152,7 @@ sub invalid_item_type {
|
|||
if ( C4::Context->preference('item-level_itypes') ) {
|
||||
my $items_with_invalid_itype = Koha::Items->search(
|
||||
{
|
||||
biblionumber => { -in => $ids },
|
||||
biblionumber => $ids,
|
||||
-and => [ itype => { not_in => \@itemtypes }, itype => { '!=' => '' } ]
|
||||
}
|
||||
);
|
||||
|
|
@ -170,7 +169,7 @@ sub invalid_item_type {
|
|||
} else {
|
||||
my $biblioitems_with_invalid_itemtype = Koha::Biblioitems->search(
|
||||
{
|
||||
biblionumber => { -in => $ids },
|
||||
biblionumber => $ids,
|
||||
-and => [ itemtype => { not_in => \@itemtypes }, itemtype => { '!=' => '' } ]
|
||||
}
|
||||
);
|
||||
|
|
@ -201,7 +200,7 @@ sub errors_in_marc {
|
|||
my ( $item_tag, $item_subfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
|
||||
my $search_string = q{ExtractValue(metadata,'count(//datafield[@tag="} . $item_tag . q{"])')>0};
|
||||
my $biblio_metadatas_with_item_fields =
|
||||
Koha::Biblio::Metadatas->search( { biblionumber => { -in => $ids } } )->search( \$search_string );
|
||||
Koha::Biblio::Metadatas->search( { biblionumber => $ids } )->search( \$search_string );
|
||||
if ( $biblio_metadatas_with_item_fields->count ) {
|
||||
while ( my $biblio_metadata_with_item_fields = $biblio_metadatas_with_item_fields->next ) {
|
||||
push @item_fields_in_marc,
|
||||
|
|
@ -213,7 +212,7 @@ sub errors_in_marc {
|
|||
|
||||
my ( $biblio_tag, $biblio_subfield ) = C4::Biblio::GetMarcFromKohaField("biblio.biblionumber");
|
||||
my ( $biblioitem_tag, $biblioitem_subfield ) = C4::Biblio::GetMarcFromKohaField("biblioitems.biblioitemnumber");
|
||||
$biblios = Koha::Biblios->search( { biblionumber => { -in => $ids } } );
|
||||
$biblios = Koha::Biblios->search( { biblionumber => $ids } );
|
||||
while ( my $biblio = $biblios->next ) {
|
||||
my $record = eval { $biblio->metadata->record; };
|
||||
if ($@) {
|
||||
|
|
@ -249,7 +248,7 @@ sub errors_in_marc {
|
|||
__x(
|
||||
q{Biblionumber {biblionumber} has biblioitemnumber '{biblioitemnumber_in_marc}' but should be '{biblioitemnumber}' in {biblioitem_tag}${biblioitem_subfield}},
|
||||
biblionumber => $biblio->biblionumber,
|
||||
biblioitemnumber_in_marc => $biblioitemnumber,
|
||||
biblioitemnumber_in_marc => $biblionumber,
|
||||
biblioitemnumber => $biblio->biblioitem->biblioitemnumber,
|
||||
biblioitem_tag => $biblioitem_tag,
|
||||
biblioitem_subfield => $biblioitem_subfield,
|
||||
|
|
@ -317,7 +316,7 @@ sub nonexistent_AV {
|
|||
# We are only checking biblios with items
|
||||
my $items = Koha::Items->search(
|
||||
{
|
||||
'me.biblionumber' => { -in => $ids },
|
||||
'me.biblionumber' => $ids,
|
||||
$tmp_kohafield => {
|
||||
-not_in => [ $avs->get_column('authorised_value'), '' ],
|
||||
'!=' => undef,
|
||||
|
|
@ -372,7 +371,7 @@ sub empty_title {
|
|||
my @errors;
|
||||
$biblios = Koha::Biblios->search(
|
||||
{
|
||||
biblionumber => { -in => $ids },
|
||||
biblionumber => $ids,
|
||||
-or => [
|
||||
title => '',
|
||||
title => undef,
|
||||
|
|
|
|||
176
Koha/EDI.pm
176
Koha/EDI.pm
|
|
@ -34,7 +34,6 @@ use utf8;
|
|||
use English qw{ -no_match_vars };
|
||||
use Business::ISBN;
|
||||
use DateTime;
|
||||
use JSON qw( encode_json );
|
||||
use C4::Context;
|
||||
use Koha::Database;
|
||||
use Koha::DateUtils qw( dt_from_string );
|
||||
|
|
@ -55,7 +54,6 @@ use Koha::Plugins; # Adds plugin dirs to @INC
|
|||
use Koha::Plugins::Handler;
|
||||
use Koha::Acquisition::Baskets;
|
||||
use Koha::Acquisition::Booksellers;
|
||||
use Koha::Acquisition::Orders;
|
||||
use Koha::AuthorisedValues;
|
||||
use Koha::Util::FrameworkPlugin qw( biblio_008 );
|
||||
|
||||
|
|
@ -218,26 +216,14 @@ sub process_ordrsp {
|
|||
my $action = $line->action_notification();
|
||||
if ( $action eq 'cancelled' ) {
|
||||
my $reason = $line->coded_orderline_text();
|
||||
my $order = Koha::Acquisition::Orders->find($ordernumber);
|
||||
if ($order) {
|
||||
$order->cancel( { reason => $reason } );
|
||||
|
||||
# cancel() records item deletion failures ( e.g. an item still has holds )
|
||||
# as object messages instead of dying; log any such messages so they are
|
||||
# not lost in the automated flow
|
||||
for my $message ( @{ $order->object_messages } ) {
|
||||
my $item = $message->payload->{item};
|
||||
$logger->warn(
|
||||
sprintf(
|
||||
'EDI ORDRSP: order %s cancellation issue: %s%s',
|
||||
$ordernumber, $message->message,
|
||||
$item ? ' for itemnumber ' . $item->itemnumber : q{}
|
||||
)
|
||||
);
|
||||
ModOrder(
|
||||
{
|
||||
ordernumber => $ordernumber,
|
||||
cancellationreason => $reason,
|
||||
orderstatus => 'cancelled',
|
||||
datecancellationprinted => dt_from_string()->ymd(),
|
||||
}
|
||||
} else {
|
||||
$logger->error("EDI ORDRSP: ordernumber $ordernumber not found, cannot process cancellation");
|
||||
}
|
||||
);
|
||||
} else { # record order as due with possible further info
|
||||
|
||||
my $report = $line->coded_orderline_text();
|
||||
|
|
@ -333,9 +319,8 @@ sub process_invoice {
|
|||
if ( !$vendor_acct ) {
|
||||
$invoice_message->add_to_edifact_errors(
|
||||
{
|
||||
invoicenumber => $invoicenumber,
|
||||
section => "NAD+SU+" . $msg->supplier_ean,
|
||||
details => "Skipped invoice $invoicenumber with unmatched vendor san: $vendor_ean"
|
||||
section => "NAD+SU+" . $msg->supplier_ean,
|
||||
details => "Skipped invoice $invoicenumber with unmatched vendor san: $vendor_ean"
|
||||
}
|
||||
);
|
||||
$logger->error( "Cannot find vendor with ean $vendor_ean for invoice $invoicenumber in "
|
||||
|
|
@ -364,9 +349,8 @@ sub process_invoice {
|
|||
if ( !$ordernumber ) {
|
||||
$invoice_message->add_to_edifact_errors(
|
||||
{
|
||||
invoicenumber => $invoicenumber,
|
||||
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
|
||||
details => "Skipped invoice line " . $line->line_item_number . ", missing ordernumber"
|
||||
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
|
||||
details => "Skipped invoice line " . $line->line_item_number . ", missing ordernumber"
|
||||
}
|
||||
);
|
||||
$logger->error("Skipping invoice line, no associated ordernumber");
|
||||
|
|
@ -378,9 +362,8 @@ sub process_invoice {
|
|||
if ( !$order ) {
|
||||
$invoice_message->add_to_edifact_errors(
|
||||
{
|
||||
invoicenumber => $invoicenumber,
|
||||
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
|
||||
details => "Skipped invoice line "
|
||||
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
|
||||
details => "Skipped invoice line "
|
||||
. $line->line_item_number
|
||||
. ", cannot find order with ordernumber "
|
||||
. $ordernumber
|
||||
|
|
@ -394,9 +377,8 @@ sub process_invoice {
|
|||
if ( !$bib ) {
|
||||
$invoice_message->add_to_edifact_errors(
|
||||
{
|
||||
invoicenumber => $invoicenumber,
|
||||
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
|
||||
details => "Skipped invoice line "
|
||||
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
|
||||
details => "Skipped invoice line "
|
||||
. $line->line_item_number
|
||||
. ", cannot find biblio for ordernumber "
|
||||
. $ordernumber
|
||||
|
|
@ -411,24 +393,6 @@ sub process_invoice {
|
|||
$logger->trace( "Receipting order:$ordernumber Qty: " . $line->quantity );
|
||||
$logger->trace( "Updating bib:" . $bib->biblionumber . " id:" . $line->item_number_id );
|
||||
|
||||
if ( $order->orderstatus eq 'complete' ) {
|
||||
$invoice_message->add_to_edifact_errors(
|
||||
{
|
||||
invoicenumber => $invoicenumber,
|
||||
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
|
||||
details => "Warning: invoice line "
|
||||
. $line->line_item_number
|
||||
. " references ordernumber "
|
||||
. $ordernumber
|
||||
. " which is already marked as complete (previously received on "
|
||||
. ( $order->datereceived // 'unknown date' )
|
||||
. "). The ordernumber in this invoice may be incorrect."
|
||||
}
|
||||
);
|
||||
$logger->warn(
|
||||
"Invoice line references already-complete order $ordernumber (invoice $invoicenumber)");
|
||||
}
|
||||
|
||||
# check suggestions
|
||||
my $s = $schema->resultset('Suggestion')->search(
|
||||
{
|
||||
|
|
@ -483,8 +447,7 @@ sub process_invoice {
|
|||
transfer_items( $schema, $line, $order, $received_order, $quantity );
|
||||
receipt_items(
|
||||
$schema, $line,
|
||||
$received_order->ordernumber, $quantity, $invoice_message,
|
||||
$invoicenumber
|
||||
$received_order->ordernumber, $quantity, $invoice_message
|
||||
);
|
||||
} else { # simple receipt all copies on order
|
||||
$order->quantityreceived($quantity);
|
||||
|
|
@ -498,7 +461,7 @@ sub process_invoice {
|
|||
$quantity * $price_excl_tax * ( $tax_rate ? $tax_rate->{rate} : 0 ) );
|
||||
$order->orderstatus('complete');
|
||||
$order->update;
|
||||
receipt_items( $schema, $line, $ordernumber, $quantity, $invoice_message, $invoicenumber );
|
||||
receipt_items( $schema, $line, $ordernumber, $quantity, $invoice_message );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -535,7 +498,7 @@ sub _get_invoiced_price {
|
|||
}
|
||||
|
||||
sub receipt_items {
|
||||
my ( $schema, $inv_line, $ordernumber, $quantity, $invoice_message, $invoicenumber ) = @_;
|
||||
my ( $schema, $inv_line, $ordernumber, $quantity, $invoice_message ) = @_;
|
||||
my $logger = Koha::Logger->get( { interface => 'edi' } );
|
||||
|
||||
# itemnumber is not a foreign key ??? makes this a bit cumbersome
|
||||
|
|
@ -551,13 +514,12 @@ sub receipt_items {
|
|||
my $i = $ilink->itemnumber;
|
||||
$invoice_message->add_to_edifact_errors(
|
||||
{
|
||||
invoicenumber => $invoicenumber,
|
||||
section => join( "\n", map { $_->as_string } @{ $inv_line->{segs} } ),
|
||||
details => "Skipped invoice line "
|
||||
section => join( "\n", map { $_->as_string } @{ $inv_line->{segs} } ),
|
||||
details => "Skipped invoice line "
|
||||
. $inv_line->line_item_number
|
||||
. ", Koha item with itemnumber "
|
||||
. $i
|
||||
. " is missing"
|
||||
. "is missing"
|
||||
}
|
||||
);
|
||||
$logger->warn("Cannot find aqorder item for $i: Order: $ordernumber");
|
||||
|
|
@ -600,11 +562,10 @@ sub receipt_items {
|
|||
if ( $rs->count > 0 ) {
|
||||
$invoice_message->add_to_edifact_errors(
|
||||
{
|
||||
invoicenumber => $invoicenumber,
|
||||
section => join( "\n", map { $_->as_string } @{ $inv_line->{segs} } ),
|
||||
details => "Failed to assign barcode "
|
||||
section => join( "\n", map { $_->as_string } @{ $inv_line->{segs} } ),
|
||||
details => "Failed to assign barcode "
|
||||
. $barcode
|
||||
. " for invoice line "
|
||||
. "for invoice line "
|
||||
. $inv_line->line_item_number
|
||||
. ", duplicate found"
|
||||
}
|
||||
|
|
@ -631,20 +592,16 @@ sub receipt_items {
|
|||
|
||||
$item->update;
|
||||
} else {
|
||||
my $available_branches = join( ', ', sort keys %branch_map ) || 'none';
|
||||
$invoice_message->add_to_edifact_errors(
|
||||
{
|
||||
invoicenumber => $invoicenumber,
|
||||
section => join( "\n", map { $_->as_string } @{ $inv_line->{segs} } ),
|
||||
details => "No matching item found for invoice line "
|
||||
section => join( "\n", map { $_->as_string } @{ $inv_line->{segs} } ),
|
||||
details => "No matching item found for invoice line "
|
||||
. $inv_line->line_item_number . ":"
|
||||
. $gir_occurrence
|
||||
. " at branch $branch"
|
||||
. " (order $ordernumber has items at: $available_branches)."
|
||||
. " Check whether the ordernumber in the invoice (RFF+LI) is correct."
|
||||
}
|
||||
);
|
||||
$logger->warn("Unmatched item at branch:$branch for order:$ordernumber (available: $available_branches)");
|
||||
$logger->warn("Unmatched item at branch:$branch");
|
||||
}
|
||||
++$gir_occurrence;
|
||||
}
|
||||
|
|
@ -959,55 +916,17 @@ sub quote_item {
|
|||
$order_hash->{line_item_id} = $item->item_number_id;
|
||||
}
|
||||
|
||||
# Helper function to extract servicing instructions for a specific GIR occurrence
|
||||
my $extract_servicing_instructions = sub {
|
||||
my ($occurrence) = @_;
|
||||
my @group;
|
||||
|
||||
# Read LVC (coded) instruction for this occurrence
|
||||
my $lvc_val = $item->girfield( 'coded_servicing_instruction', $occurrence );
|
||||
if ($lvc_val) {
|
||||
|
||||
# Validate against EDIFACT_SI authorized values
|
||||
my $av = Koha::AuthorisedValues->find(
|
||||
{
|
||||
category => 'EDIFACT_SI',
|
||||
authorised_value => $lvc_val
|
||||
}
|
||||
);
|
||||
if ($av) {
|
||||
push @group, { type => 'LVC', value => $lvc_val };
|
||||
} else {
|
||||
$quote_message->add_to_edifact_errors(
|
||||
{
|
||||
section => "GIR+LVC validation",
|
||||
details =>
|
||||
"Invalid servicing instruction code '$lvc_val' - not found in EDIFACT_SI authorized values"
|
||||
}
|
||||
);
|
||||
$logger->warn("Invalid servicing instruction code: $lvc_val");
|
||||
if ( $item->girfield('servicing_instruction') ) {
|
||||
my $occ = 0;
|
||||
my $txt = q{};
|
||||
my $si;
|
||||
while ( $si = $item->girfield( 'servicing_instruction', $occ ) ) {
|
||||
if ($occ) {
|
||||
$txt .= q{: };
|
||||
}
|
||||
$txt .= $si;
|
||||
++$occ;
|
||||
}
|
||||
|
||||
# Read LVT (freetext) instruction for this occurrence
|
||||
my $lvt_val = $item->girfield( 'servicing_instruction', $occurrence );
|
||||
if ($lvt_val) {
|
||||
push @group, { type => 'LVT', value => $lvt_val };
|
||||
}
|
||||
|
||||
return \@group;
|
||||
};
|
||||
|
||||
# Extract servicing instructions for first order (occurrence 0)
|
||||
my @servicing_groups;
|
||||
my $current_group = $extract_servicing_instructions->(0);
|
||||
|
||||
# Store as JSON if we have any instructions
|
||||
if (@$current_group) {
|
||||
push @servicing_groups, $current_group;
|
||||
$order_hash->{servicing_instruction} = encode_json( \@servicing_groups );
|
||||
} else {
|
||||
$order_hash->{servicing_instruction} = undef;
|
||||
}
|
||||
if ($order_note) {
|
||||
$order_hash->{order_vendornote} = $order_note;
|
||||
|
|
@ -1137,15 +1056,6 @@ sub quote_item {
|
|||
|
||||
$order_hash->{budget_id} = $budget->budget_id;
|
||||
|
||||
# Extract servicing instructions for this occurrence
|
||||
my $si_group = $extract_servicing_instructions->($occurrence);
|
||||
if (@$si_group) {
|
||||
my @si_groups = ($si_group);
|
||||
$order_hash->{servicing_instruction} = encode_json( \@si_groups );
|
||||
} else {
|
||||
$order_hash->{servicing_instruction} = undef;
|
||||
}
|
||||
|
||||
my $new_order = $schema->resultset('Aqorder')->create($order_hash);
|
||||
my $o = $new_order->ordernumber();
|
||||
$logger->trace("Order created: $o");
|
||||
|
|
@ -1759,20 +1669,6 @@ Koha::EDI
|
|||
|
||||
Returns the bib_record
|
||||
|
||||
=head2 _validate_location_code
|
||||
|
||||
ok = _validate_location_code($location_code, $quote_message)
|
||||
|
||||
Returns 1 if $location_code is empty or matches an authorised value in
|
||||
category LOC. Returns 0 and records an error on $quote_message otherwise.
|
||||
|
||||
=head2 _validate_collection_code
|
||||
|
||||
ok = _validate_collection_code($ccode, $quote_message)
|
||||
|
||||
Returns 1 if $ccode is empty or matches an authorised value in category
|
||||
CCODE. Returns 0 and records an error on $quote_message otherwise.
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Colin Campbell <colin.campbell@ptfs-europe.com>
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@ Returns the resources from this package
|
|||
|
||||
sub resources {
|
||||
my ($self) = @_;
|
||||
return Koha::ERM::EHoldings::Resources->_new_from_dbic( scalar $self->_result->resources );
|
||||
my $rs = $self->_result->erm_eholdings_resources;
|
||||
return Koha::ERM::EHoldings::Resources->_new_from_dbic($rs);
|
||||
}
|
||||
|
||||
=head3 vendor
|
||||
|
|
|
|||
|
|
@ -26,9 +26,7 @@ use DateTime;
|
|||
use Readonly qw( Readonly );
|
||||
use Koha::Database;
|
||||
use Koha::DateUtils qw( dt_from_string );
|
||||
use Koha::Logger;
|
||||
use C4::Budgets qw( GetBudget );
|
||||
use JSON qw( decode_json );
|
||||
use C4::Budgets qw( GetBudget );
|
||||
|
||||
use Koha::Acquisition::Orders;
|
||||
|
||||
|
|
@ -434,10 +432,7 @@ sub order_line {
|
|||
}
|
||||
}
|
||||
my $budget = GetBudget( $orderline->budget_id );
|
||||
my $ol_fields = {
|
||||
budget_code => $budget->{budget_code},
|
||||
servicing_instruction => $orderline->servicing_instruction,
|
||||
};
|
||||
my $ol_fields = { budget_code => $budget->{budget_code}, };
|
||||
|
||||
my $item_fields = [];
|
||||
for my $item (@items) {
|
||||
|
|
@ -473,7 +468,7 @@ sub order_line {
|
|||
# FTX free text for current orderline
|
||||
# Pass vendor note in FTX free text segment
|
||||
if ( $orderline->order_vendornote ) {
|
||||
my $vendornote = encode_text( $orderline->order_vendornote );
|
||||
my $vendornote = $orderline->order_vendornote;
|
||||
chomp $vendornote;
|
||||
my $ftx = 'FTX+LIN+++';
|
||||
$ftx .= $vendornote;
|
||||
|
|
@ -497,7 +492,7 @@ sub order_line {
|
|||
# RFF : suppliers unique quotation reference number
|
||||
if ( $orderline->suppliers_reference_number ) {
|
||||
$rff = join q{}, 'RFF+', $orderline->suppliers_reference_qualifier,
|
||||
':', encode_text( $orderline->suppliers_reference_number ), $seg_terminator;
|
||||
':', $orderline->suppliers_reference_number, $seg_terminator;
|
||||
$self->add_seg($rff);
|
||||
}
|
||||
|
||||
|
|
@ -584,7 +579,7 @@ sub gir_segments {
|
|||
my $orderfields = $params->{ol_fields};
|
||||
my @onorderitems = @{ $params->{items} };
|
||||
|
||||
my $budget_code = encode_text( $orderfields->{budget_code} );
|
||||
my $budget_code = $orderfields->{budget_code};
|
||||
my @segments;
|
||||
my $sequence_no = 1;
|
||||
my $lsq_field = C4::Context->preference('EdifactLSQ');
|
||||
|
|
@ -598,61 +593,36 @@ sub gir_segments {
|
|||
}
|
||||
if ( $item->{branchcode} ) {
|
||||
push @gir_elements,
|
||||
{ identity_number => 'LLO', data => encode_text( $item->{branchcode} ) };
|
||||
{ identity_number => 'LLO', data => $item->{branchcode} };
|
||||
}
|
||||
if ( $item->{itype} ) {
|
||||
push @gir_elements,
|
||||
{ identity_number => 'LST', data => encode_text( $item->{itype} ) };
|
||||
{ identity_number => 'LST', data => $item->{itype} };
|
||||
}
|
||||
|
||||
# Handle LSQ mapping
|
||||
if ( $lsq_field && $item->{$lsq_field} ) {
|
||||
push @gir_elements,
|
||||
{ identity_number => 'LSQ', data => encode_text( $item->{$lsq_field} ) };
|
||||
{ identity_number => 'LSQ', data => $item->{$lsq_field} };
|
||||
}
|
||||
|
||||
# Handle LSL mapping
|
||||
if ( $lsl_field && $item->{$lsl_field} ) {
|
||||
push @gir_elements,
|
||||
{ identity_number => 'LSL', data => encode_text( $item->{$lsl_field} ) };
|
||||
{ identity_number => 'LSL', data => $item->{$lsl_field} };
|
||||
}
|
||||
if ( $item->{itemcallnumber} ) {
|
||||
push @gir_elements,
|
||||
{ identity_number => 'LSM', data => encode_text( $item->{itemcallnumber} ) };
|
||||
{ identity_number => 'LSM', data => $item->{itemcallnumber} };
|
||||
}
|
||||
|
||||
# Servicing instructions (LVT freetext and/or LVC coded)
|
||||
# Format: JSON array of arrays: [[{type,value},...],[{type,value},...]]
|
||||
# Each inner array is a group of instructions that work together
|
||||
# itemcallnumber -> shelfmark
|
||||
if ( $orderfields->{servicing_instruction} ) {
|
||||
my $si_data = $orderfields->{servicing_instruction};
|
||||
my $servicing_groups;
|
||||
|
||||
eval { $servicing_groups = decode_json($si_data); };
|
||||
if ($@) {
|
||||
my $logger = Koha::Logger->get( { interface => 'edi' } );
|
||||
$logger->warn("Failed to parse servicing_instruction JSON: $@");
|
||||
}
|
||||
|
||||
# Add all servicing instruction groups
|
||||
if ( ref $servicing_groups eq 'ARRAY' ) {
|
||||
foreach my $group (@$servicing_groups) {
|
||||
if ( ref $group eq 'ARRAY' ) {
|
||||
foreach my $instruction (@$group) {
|
||||
if ( ref $instruction eq 'HASH'
|
||||
&& $instruction->{type}
|
||||
&& $instruction->{value} )
|
||||
{
|
||||
push @gir_elements,
|
||||
{
|
||||
identity_number => $instruction->{type},
|
||||
data => encode_text( $instruction->{value} )
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
push @gir_elements,
|
||||
{
|
||||
identity_number => 'LVT',
|
||||
data => $orderfields->{servicing_instruction}
|
||||
};
|
||||
}
|
||||
my $e_cnt = 0; # count number of elements so we dont exceed 5 per segment
|
||||
my $copy_no = sprintf 'GIR+%03d', $sequence_no;
|
||||
|
|
@ -660,8 +630,7 @@ sub gir_segments {
|
|||
foreach my $e (@gir_elements) {
|
||||
if ( $e_cnt == 5 ) {
|
||||
push @segments, $seg;
|
||||
$seg = $copy_no;
|
||||
$e_cnt = 0;
|
||||
$seg = $copy_no;
|
||||
}
|
||||
$seg .=
|
||||
add_gir_identity_number( $e->{identity_number}, $e->{data} );
|
||||
|
|
@ -697,7 +666,7 @@ sub lin_segment {
|
|||
my ( $line_number, $item_number_id ) = @_;
|
||||
|
||||
if ($item_number_id) {
|
||||
$item_number_id = "++" . encode_text($item_number_id) . ":EN";
|
||||
$item_number_id = "++${item_number_id}:EN";
|
||||
} else {
|
||||
$item_number_id = q||;
|
||||
}
|
||||
|
|
@ -722,7 +691,7 @@ sub additional_product_id {
|
|||
}
|
||||
|
||||
# function id set to 5 states this is the main product id
|
||||
return "PIA+5+" . encode_text($product_id) . ":$product_code$seg_terminator";
|
||||
return "PIA+5+$product_id:$product_code$seg_terminator";
|
||||
}
|
||||
|
||||
sub message_date_segment {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ use File::Slurp qw( read_file );
|
|||
use Koha::Database;
|
||||
use Koha::DateUtils qw( dt_from_string );
|
||||
use Koha::File::Transports;
|
||||
use Koha::Logger;
|
||||
|
||||
sub new {
|
||||
my ( $class, $account_id ) = @_;
|
||||
|
|
@ -43,9 +42,8 @@ sub new {
|
|||
account => $acct,
|
||||
schema => $schema,
|
||||
file_transport => $file_transport,
|
||||
working_dir => C4::Context::temporary_directory, #temporary work directory
|
||||
working_dir => C4::Context::temporary_directory, #temporary work directory
|
||||
transfer_date => dt_from_string(),
|
||||
logger => Koha::Logger->get( { interface => 'edi' } ),
|
||||
};
|
||||
|
||||
bless $self, $class;
|
||||
|
|
@ -65,7 +63,7 @@ sub download_messages {
|
|||
$self->{message_type} = $message_type;
|
||||
|
||||
unless ( $self->{file_transport} ) {
|
||||
$self->{logger}->error( "No file transport configured for EDI account " . $self->{account}->id );
|
||||
carp "No file transport configured for EDI account " . $self->{account}->id;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -75,21 +73,21 @@ sub download_messages {
|
|||
|
||||
# Connect to the transport
|
||||
unless ( $self->{file_transport}->connect() ) {
|
||||
$self->{logger}->error( "Failed to connect to file transport: " . $self->{file_transport}->id );
|
||||
carp "Failed to connect to file transport: " . $self->{file_transport}->id;
|
||||
return;
|
||||
}
|
||||
|
||||
# Change to download directory
|
||||
my $download_dir = $self->{file_transport}->download_directory;
|
||||
if ( $download_dir && !$self->{file_transport}->change_directory($download_dir) ) {
|
||||
$self->{logger}->error("Failed to change to download directory: $download_dir");
|
||||
carp "Failed to change to download directory: $download_dir";
|
||||
return;
|
||||
}
|
||||
|
||||
# Get file list
|
||||
my $file_list = $self->{file_transport}->list_files();
|
||||
unless ($file_list) {
|
||||
$self->{logger}->error("Failed to get file list from transport");
|
||||
carp "Failed to get file list from transport";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +109,7 @@ sub download_messages {
|
|||
# Mark file as processed using the transport's rename functionality
|
||||
$self->{file_transport}->rename_file( $filename, $processed_name );
|
||||
} else {
|
||||
$self->{logger}->error("Failed to download file: $filename");
|
||||
carp "Failed to download file: $filename";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -133,20 +131,20 @@ sub upload_messages {
|
|||
}
|
||||
|
||||
unless ( $self->{file_transport} ) {
|
||||
$self->{logger}->error( "No file transport configured for EDI account " . $self->{account}->id );
|
||||
carp "No file transport configured for EDI account " . $self->{account}->id;
|
||||
return;
|
||||
}
|
||||
|
||||
# Connect to the transport
|
||||
unless ( $self->{file_transport}->connect() ) {
|
||||
$self->{logger}->error( "Failed to connect to file transport: " . $self->{file_transport}->id );
|
||||
carp "Failed to connect to file transport: " . $self->{file_transport}->id;
|
||||
return;
|
||||
}
|
||||
|
||||
# Change to upload directory
|
||||
my $upload_dir = $self->{file_transport}->upload_directory;
|
||||
if ( $upload_dir && !$self->{file_transport}->change_directory($upload_dir) ) {
|
||||
$self->{logger}->error("Failed to change to upload directory: $upload_dir");
|
||||
carp "Failed to change to upload directory: $upload_dir";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -167,13 +165,13 @@ sub upload_messages {
|
|||
$m->status('sent');
|
||||
$m->update;
|
||||
} else {
|
||||
$self->{logger}->error( "Failed to upload file: " . $m->filename );
|
||||
carp "Failed to upload file: " . $m->filename;
|
||||
}
|
||||
|
||||
# Clean up temp file
|
||||
unlink $temp_file;
|
||||
} else {
|
||||
$self->{logger}->error( "Could not create temporary file for upload: " . $m->filename );
|
||||
carp "Could not create temporary file for upload: " . $m->filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ sub full_message {
|
|||
if ( $self->isa('Koha::Exceptions::Metadata::Invalid') ) {
|
||||
$msg = sprintf(
|
||||
"Invalid data, cannot decode metadata object (biblio_metadata.id=%s, biblionumber=%s, format=%s, schema=%s, decoding_error='%s')",
|
||||
$self->id // '', $self->biblionumber // '', $self->format // '', $self->schema // '',
|
||||
$self->decoding_error // ''
|
||||
$self->id, $self->biblionumber, $self->format, $self->schema, $self->decoding_error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,15 +69,6 @@ use Exception::Class (
|
|||
isa => 'Koha::Exceptions::Object',
|
||||
description => 'The object is not in storage yet',
|
||||
},
|
||||
'Koha::Exceptions::Object::NotNull' => {
|
||||
isa => 'Koha::Exceptions::Object',
|
||||
description => 'A required column value was not provided',
|
||||
fields => ['property'],
|
||||
},
|
||||
'Koha::Exceptions::Object::UnhandledDBError' => {
|
||||
isa => 'Koha::Exceptions::Object',
|
||||
description => 'An unhandled database error occurred',
|
||||
},
|
||||
);
|
||||
|
||||
sub full_message {
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
package Koha::Exceptions::PatronAccountLink;
|
||||
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use Koha::Exception;
|
||||
|
||||
use Exception::Class (
|
||||
'Koha::Exceptions::PatronAccountLink' => {
|
||||
isa => 'Koha::Exception',
|
||||
},
|
||||
'Koha::Exceptions::PatronAccountLink::AlreadyLinked' => {
|
||||
isa => 'Koha::Exceptions::PatronAccountLink',
|
||||
description => "Patrons are already linked together",
|
||||
},
|
||||
'Koha::Exceptions::PatronAccountLink::DifferentGroups' => {
|
||||
isa => 'Koha::Exceptions::PatronAccountLink',
|
||||
description => "Both patrons already belong to different link groups",
|
||||
},
|
||||
);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Exceptions::PatronAccountLink - Base class for PatronAccountLink exceptions
|
||||
|
||||
=head1 Exceptions
|
||||
|
||||
=head2 Koha::Exceptions::PatronAccountLink
|
||||
|
||||
Generic PatronAccountLink exception
|
||||
|
||||
=head2 Koha::Exceptions::PatronAccountLink::AlreadyLinked
|
||||
|
||||
Exception to be used when patrons are already linked together.
|
||||
|
||||
=head2 Koha::Exceptions::PatronAccountLink::DifferentGroups
|
||||
|
||||
Exception to be used when both patrons already belong to different link groups.
|
||||
|
||||
=cut
|
||||
|
||||
1;
|
||||
|
|
@ -2,13 +2,8 @@ package Koha::Exceptions::Report;
|
|||
|
||||
use Modern::Perl;
|
||||
|
||||
use Koha::Exception;
|
||||
|
||||
use Exception::Class (
|
||||
'Koha::Exceptions::Report' => {
|
||||
isa => 'Koha::Exception',
|
||||
description => 'A report operation failed',
|
||||
},
|
||||
'Koha::Exceptions::Report' => { description => "Something went wrong!" },
|
||||
'Koha::Exceptions::Report::DuplicateRunning' => {
|
||||
isa => 'Koha::Exceptions::Report',
|
||||
description => "The configured per-user limit on simultaneous runs of this report has been reached",
|
||||
|
|
@ -19,46 +14,6 @@ use Exception::Class (
|
|||
description => "The configured per-user limit on total simultaneous report runs has been reached",
|
||||
fields => [ 'user_id', 'limit' ],
|
||||
},
|
||||
'Koha::Exceptions::Report::InstanceTotalRunning' => {
|
||||
isa => 'Koha::Exceptions::Report',
|
||||
description => "The configured instance-wide limit on total simultaneous report runs has been reached",
|
||||
fields => ['limit'],
|
||||
},
|
||||
'Koha::Exceptions::Report::EditPermission' => {
|
||||
isa => 'Koha::Exceptions::Report',
|
||||
description => 'You do not have permission to edit this report',
|
||||
fields => ['report_id'],
|
||||
},
|
||||
);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Exceptions::Report - Base class for report exceptions
|
||||
|
||||
=head1 Exceptions
|
||||
|
||||
=head2 Koha::Exceptions::Report
|
||||
|
||||
Generic report exception.
|
||||
|
||||
=head2 Koha::Exceptions::Report::DuplicateRunning
|
||||
|
||||
The configured per-user limit on simultaneous runs of this report has been reached.
|
||||
|
||||
=head2 Koha::Exceptions::Report::TotalRunning
|
||||
|
||||
The configured per-user limit on total simultaneous report runs has been reached.
|
||||
|
||||
=head2 Koha::Exceptions::Report::EditPermission
|
||||
|
||||
Exception raised when the logged-in user may not edit a report.
|
||||
|
||||
=head1 Class methods
|
||||
|
||||
=head2 description
|
||||
|
||||
Returns the description of the exception.
|
||||
|
||||
=cut
|
||||
|
||||
1;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ use List::MoreUtils qw( any );
|
|||
use Koha::Database;
|
||||
use Koha::Exceptions::Object;
|
||||
use Koha::Encryption;
|
||||
use Koha::Logger;
|
||||
|
||||
use base qw(Koha::Object);
|
||||
|
||||
|
|
@ -245,103 +244,6 @@ sub _is_connected {
|
|||
die "Subclass must implement _is_connected";
|
||||
}
|
||||
|
||||
=head3 current_directory
|
||||
|
||||
my $path = $transport->current_directory();
|
||||
|
||||
Returns the transport's current remote working directory as a plain string,
|
||||
without side effects. Automatically establishes a connection if needed.
|
||||
|
||||
B<Note:> This is a read-only query. It must never navigate the connection -
|
||||
unlike some underlying libraries' own C<cwd()>/C<pwd()> methods, which differ
|
||||
in meaning between backends (see subclass implementations).
|
||||
|
||||
B<Returns:> The current directory path on success, undef on failure. Check
|
||||
object_messages() for details.
|
||||
|
||||
=cut
|
||||
|
||||
sub current_directory {
|
||||
my ($self) = @_;
|
||||
|
||||
return unless $self->_ensure_connected();
|
||||
|
||||
return $self->_current_directory();
|
||||
}
|
||||
|
||||
=head3 _current_directory
|
||||
|
||||
my $path = $transport->_current_directory();
|
||||
|
||||
Internal method that returns the protocol-specific current working
|
||||
directory. Must be implemented by subclasses. Called by current_directory()
|
||||
after connection verification.
|
||||
|
||||
=cut
|
||||
|
||||
sub _current_directory {
|
||||
my ($self) = @_;
|
||||
die "Subclass must implement _current_directory";
|
||||
}
|
||||
|
||||
=head3 _record_error
|
||||
|
||||
$transport->_record_error( $operation, \%payload );
|
||||
|
||||
Records an operation failure consistently across all transport backends:
|
||||
adds an in-memory 'error' message (as add_message() already does), persists
|
||||
a snapshot of the failure to the C<status> column, and logs it, so that a
|
||||
transport's displayed status reflects real usage (not only explicit
|
||||
test_connection() runs).
|
||||
|
||||
Before this existed, only Koha::File::Transport::SFTP persisted status on
|
||||
every failed operation; FTP and Local never did, so an SFTP transport's
|
||||
status could reflect a single transient failure from live usage (e.g. an
|
||||
EDI cron job) while the same failure on FTP/Local left the stored status
|
||||
untouched. Subclasses should call this from their C<_abort_operation>
|
||||
instead of writing to C<status> themselves, so all three stay in sync.
|
||||
|
||||
The persisted C<operations> array carries every message recorded on this
|
||||
transport object so far this session (in order), not just the failing one -
|
||||
the same "full trace" shape TestTransport::process() builds for an explicit
|
||||
test_connection() run. A single long-lived transport object (e.g. across an
|
||||
EDI download loop) will therefore show every prior successful step leading
|
||||
up to the failure, not just the failure in isolation.
|
||||
|
||||
=cut
|
||||
|
||||
sub _record_error {
|
||||
my ( $self, $operation, $payload ) = @_;
|
||||
|
||||
$self->add_message(
|
||||
{
|
||||
message => $operation || 'operation',
|
||||
type => 'error',
|
||||
payload => $payload,
|
||||
}
|
||||
);
|
||||
|
||||
my $operations =
|
||||
[ map { { code => $_->message, status => $_->type, detail => $_->payload } } @{ $self->object_messages } ];
|
||||
|
||||
my $status = {
|
||||
status => 'errors',
|
||||
operations => $operations,
|
||||
};
|
||||
$self->set( { status => encode_json($status) } )->store();
|
||||
|
||||
Koha::Logger->get( { category => __PACKAGE__ } )->warn(
|
||||
sprintf(
|
||||
"File transport %s (%s) operation '%s' failed: %s. Trace: %s",
|
||||
$self->id, $self->transport, $operation || 'operation',
|
||||
$payload->{error} // 'unknown error',
|
||||
encode_json($operations)
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
=head3 _auto_change_directory
|
||||
|
||||
$transport->_auto_change_directory($dir_type, $custom_path);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ package Koha::File::Transport::FTP;
|
|||
|
||||
use Modern::Perl;
|
||||
use Net::FTP;
|
||||
use Time::Local qw( timegm );
|
||||
use Try::Tiny;
|
||||
|
||||
use base qw(Koha::File::Transport);
|
||||
|
|
@ -123,26 +122,6 @@ sub _download_file {
|
|||
return 1;
|
||||
}
|
||||
|
||||
=head3 _current_directory
|
||||
|
||||
my $path = $self->_current_directory;
|
||||
|
||||
Internal method that returns the FTP connection's current working directory.
|
||||
|
||||
Net::FTP's own C<cwd()> method is a mutator: called with no argument it
|
||||
defaults to "/" and issues a real C<CWD /> command, changing the connection's
|
||||
directory as a side effect and returning a boolean rather than a path. The
|
||||
read-only equivalent is C<pwd()>, which sends PWD and parses the path out of
|
||||
the response without navigating anywhere.
|
||||
|
||||
=cut
|
||||
|
||||
sub _current_directory {
|
||||
my ($self) = @_;
|
||||
|
||||
return $self->{connection}->pwd;
|
||||
}
|
||||
|
||||
=head3 _change_directory
|
||||
|
||||
my $success = $server->_change_directory($directory);
|
||||
|
|
@ -157,15 +136,15 @@ sub _change_directory {
|
|||
my ( $self, $remote_directory ) = @_;
|
||||
my $operation = "change_directory";
|
||||
|
||||
$self->{connection}->cwd($remote_directory) or return $self->_abort_operation( $operation, $remote_directory );
|
||||
$self->{connection}->cwd($remote_directory) or return $self->_abort_operation($operation);
|
||||
|
||||
$self->add_message(
|
||||
{
|
||||
message => $operation,
|
||||
type => 'success',
|
||||
payload => {
|
||||
path => $remote_directory,
|
||||
pwd => $self->_current_directory
|
||||
directory => $remote_directory,
|
||||
pwd => $self->{connection}->pwd
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -177,22 +156,7 @@ sub _change_directory {
|
|||
|
||||
Internal method that performs the FTP-specific file listing operation.
|
||||
Returns an array reference of hashrefs with file information.
|
||||
Each hashref contains: filename, longname, size, perms, mtime, type.
|
||||
|
||||
The listing is obtained with MLSD (RFC 3659), whose machine-readable output
|
||||
is locale- and platform-independent and gives us the filename unambiguously
|
||||
(the pathname is the entire text after the "facts" and a single space, so
|
||||
filenames containing spaces are handled correctly). Servers that do not
|
||||
support MLSD fall back to NLST, which returns bare filenames only.
|
||||
|
||||
This mirrors what Net::FTP does internally for rmdir(). Net::FTP 3.x exposes
|
||||
no public MLSD method, so we drive it through its _list_cmd() helper - the
|
||||
same private method that backs the public ls()/dir() and that Net::FTP's own
|
||||
rmdir() uses for MLSD. That interface has been stable for many releases.
|
||||
|
||||
The hashref shape is kept consistent with the SFTP and Local transports so
|
||||
that consumers of list_files() (including out-of-tree ones) can rely on the
|
||||
same structure regardless of transport type.
|
||||
Each hashref contains: filename, longname, size, perms.
|
||||
|
||||
=cut
|
||||
|
||||
|
|
@ -200,63 +164,26 @@ sub _list_files {
|
|||
my ($self) = @_;
|
||||
my $operation = "list";
|
||||
|
||||
# Get detailed listing using dir() for consistency with SFTP format
|
||||
my $detailed_list = $self->{connection}->dir or return $self->_abort_operation($operation);
|
||||
|
||||
# Convert to hash format consistent with SFTP
|
||||
my @file_list;
|
||||
foreach my $line ( @{$detailed_list} ) {
|
||||
|
||||
# Prefer MLSD: machine-readable, no locale/format guessing. Net::FTP has
|
||||
# no public MLSD method, so use its internal _list_cmd() (as its own
|
||||
# rmdir() does). Returns an arrayref of "facts filename" lines, or undef.
|
||||
my $mlsd = $self->{connection}->_list_cmd("MLSD");
|
||||
# Parse FTP dir output (similar to ls -l format)
|
||||
# Example: "-rw-r--r-- 1 user group 1234 Jan 01 12:00 filename.txt"
|
||||
if ( $line =~ /^([d\-rwx]+)\s+\S+\s+\S+\s+\S+\s+(\d+)\s+(.+?)\s+(.+)$/ ) {
|
||||
my ( $perms, $size, $date_part, $filename ) = ( $1, $2, $3, $4 );
|
||||
|
||||
if ( $mlsd && @{$mlsd} ) {
|
||||
foreach my $line ( @{$mlsd} ) {
|
||||
# Skip directories (start with 'd')
|
||||
next if $perms =~ /^d/;
|
||||
|
||||
# Each line is "fact1=val;fact2=val;...; pathname"; the pathname is
|
||||
# everything after the facts and the single separating space.
|
||||
next unless $line =~ /^(.*?;)\s(.+)$/;
|
||||
my ( $facts_str, $filename ) = ( $1, $2 );
|
||||
|
||||
my %fact =
|
||||
map { my ( $k, $v ) = split /=/, $_, 2; ( lc($k) => $v ) }
|
||||
grep { length } split /;/, $facts_str;
|
||||
|
||||
# Skip only the current/parent directory pseudo-entries; real
|
||||
# subdirectories (type=dir) are now included like Local and SFTP.
|
||||
next if ( $fact{type} // q{} ) =~ /^(?:cdir|pdir)$/i;
|
||||
|
||||
my $type =
|
||||
( $fact{type} // q{} ) eq 'file' ? 'file'
|
||||
: ( $fact{type} // q{} ) eq 'dir' ? 'directory'
|
||||
: 'other';
|
||||
|
||||
my $perms = $self->_perms_from_mlsd_facts( \%fact, $type );
|
||||
push @file_list, {
|
||||
filename => $filename,
|
||||
longname => $line,
|
||||
size => $fact{size},
|
||||
perms => $perms,
|
||||
mtime => $self->_mtime_from_mlsd_fact( $fact{modify} ),
|
||||
type => $type,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
|
||||
# Fallback for servers without MLSD: NLST returns bare filenames
|
||||
# only, so probe each entry with SIZE/MDTM (both plain Net::FTP
|
||||
# methods) to recover size, mtime and a best-effort type. A
|
||||
# permission-denied file would be misclassified as a directory here
|
||||
# (SIZE failing is the only signal available) - a rare, accepted
|
||||
# limitation of this legacy fallback path. perms stays undef: there
|
||||
# is no way to get Unix permission bits without MLSD/UNIX.mode.
|
||||
my $names = $self->{connection}->ls or return $self->_abort_operation($operation);
|
||||
foreach my $name ( grep { !/^[.]{1,2}$/ } @{$names} ) {
|
||||
my $size = $self->{connection}->size($name);
|
||||
push @file_list, {
|
||||
filename => $name,
|
||||
longname => undef,
|
||||
size => $size,
|
||||
perms => undef,
|
||||
mtime => $self->{connection}->mdtm($name),
|
||||
type => defined $size ? 'file' : 'directory',
|
||||
perms => $perms
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -267,7 +194,7 @@ sub _list_files {
|
|||
type => 'success',
|
||||
payload => {
|
||||
count => scalar @file_list,
|
||||
pwd => $self->_current_directory
|
||||
pwd => $self->{connection}->pwd
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -275,60 +202,6 @@ sub _list_files {
|
|||
return \@file_list;
|
||||
}
|
||||
|
||||
=head3 _perms_from_mlsd_facts
|
||||
|
||||
my $perms = $self->_perms_from_mlsd_facts( \%fact, $type );
|
||||
|
||||
Converts MLSD facts into the same C<"%04o"> Unix permission-bits string used
|
||||
by the Local and SFTP transports.
|
||||
|
||||
Prefers the C<UNIX.mode> vendor-extension fact (exact Unix octal
|
||||
permissions) when the server provides it. Otherwise approximates from the
|
||||
RFC 3659 C<perm> fact, which describes only what the connected user is
|
||||
allowed to do (append/create/delete/rename/list/mkdir/read/write/etc.), not
|
||||
a real owner/group/other split - the resulting triad is mirrored across
|
||||
owner/group/other since that distinction isn't available. Returns undef if
|
||||
neither fact is present.
|
||||
|
||||
=cut
|
||||
|
||||
sub _perms_from_mlsd_facts {
|
||||
my ( $self, $fact, $type ) = @_;
|
||||
|
||||
if ( defined $fact->{'unix.mode'} ) {
|
||||
return sprintf( "%04o", oct( $fact->{'unix.mode'} ) & oct('07777') );
|
||||
}
|
||||
|
||||
my $perm = $fact->{perm};
|
||||
return unless defined $perm;
|
||||
|
||||
my $bits = 0;
|
||||
$bits |= 4 if $perm =~ /r/;
|
||||
$bits |= 2 if $perm =~ /[wa]/;
|
||||
$bits |= 1 if $type eq 'directory' && $perm =~ /e/;
|
||||
|
||||
return sprintf( "%04o", ( $bits << 6 ) | ( $bits << 3 ) | $bits );
|
||||
}
|
||||
|
||||
=head3 _mtime_from_mlsd_fact
|
||||
|
||||
my $mtime = $self->_mtime_from_mlsd_fact( $fact->{modify} );
|
||||
|
||||
Converts an MLSD C<modify> fact (RFC 3659 C<YYYYMMDDHHMMSS[.sss]> format,
|
||||
always UTC) into a Unix epoch timestamp. Returns undef if the fact is
|
||||
missing or doesn't match the expected format.
|
||||
|
||||
=cut
|
||||
|
||||
sub _mtime_from_mlsd_fact {
|
||||
my ( $self, $modify ) = @_;
|
||||
|
||||
return unless defined $modify;
|
||||
return unless $modify =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/;
|
||||
|
||||
return eval { timegm( $6, $5, $4, $3, $2 - 1, $1 ) };
|
||||
}
|
||||
|
||||
=head3 _rename_file
|
||||
|
||||
Internal method that performs the FTP-specific file rename operation.
|
||||
|
|
@ -382,35 +255,20 @@ Internal method to check if transport is currently connected.
|
|||
sub _is_connected {
|
||||
my ($self) = @_;
|
||||
|
||||
return $self->{connection} && $self->_current_directory();
|
||||
return $self->{connection} && $self->{connection}->pwd();
|
||||
}
|
||||
|
||||
=head3 _abort_operation
|
||||
|
||||
return $self->_abort_operation($operation, $path);
|
||||
|
||||
Records an error message for the named operation (persisting a status
|
||||
snapshot via the shared _record_error(), see Koha::File::Transport), aborts
|
||||
any in-progress transfer on the current connection and returns nothing, so
|
||||
callers can C<return $self-E<gt>_abort_operation($operation)> on failure.
|
||||
|
||||
C<$path> is optional and carries the directory/file the operation was
|
||||
acting on, for display purposes (e.g. which directory a failed
|
||||
change_directory() was trying to reach). Uses the C<path> key, matching
|
||||
SFTP and Local, so templates can read C<operation.detail.path> the same way
|
||||
regardless of transport type.
|
||||
|
||||
=cut
|
||||
|
||||
sub _abort_operation {
|
||||
my ( $self, $operation, $path ) = @_;
|
||||
my ( $self, $operation ) = @_;
|
||||
|
||||
$self->_record_error(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
detail => $self->{connection} ? $self->{connection}->status : '',
|
||||
error => $self->{connection} ? $self->{connection}->message : $@,
|
||||
path => $path,
|
||||
message => $operation || 'operation',
|
||||
type => 'error',
|
||||
payload => {
|
||||
detail => $self->{connection} ? $self->{connection}->status : '',
|
||||
error => $self->{connection} ? $self->{connection}->message : $@
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ use Modern::Perl;
|
|||
use File::Copy qw( copy move );
|
||||
use File::Spec;
|
||||
use IO::Dir;
|
||||
use Fcntl qw( S_ISDIR S_ISREG );
|
||||
|
||||
use base qw(Koha::File::Transport);
|
||||
|
||||
|
|
@ -44,46 +43,62 @@ sub _connect {
|
|||
# Check download directory if configured
|
||||
if ( my $download_dir = $self->download_directory ) {
|
||||
unless ( -d $download_dir ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => "Download directory does not exist: $download_dir",
|
||||
path => $download_dir
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => "Download directory does not exist: $download_dir",
|
||||
path => $download_dir
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
unless ( -r $download_dir ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => "Download directory is not readable: $download_dir",
|
||||
path => $download_dir
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => "Download directory is not readable: $download_dir",
|
||||
path => $download_dir
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# Check upload directory if configured
|
||||
if ( my $upload_dir = $self->upload_directory ) {
|
||||
unless ( -d $upload_dir ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => "Upload directory does not exist: $upload_dir",
|
||||
path => $upload_dir
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => "Upload directory does not exist: $upload_dir",
|
||||
path => $upload_dir
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
unless ( -w $upload_dir ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => "Upload directory is not writable: $upload_dir",
|
||||
path => $upload_dir
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => "Upload directory is not writable: $upload_dir",
|
||||
path => $upload_dir
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,28 +117,6 @@ sub _connect {
|
|||
return 1;
|
||||
}
|
||||
|
||||
=head3 _working_directory
|
||||
|
||||
my $directory = $self->_working_directory('download_directory');
|
||||
|
||||
Returns the directory a Local operation should act in: the directory most
|
||||
recently set via change_directory(), falling back to the given configured
|
||||
directory field (C<download_directory> or C<upload_directory>).
|
||||
|
||||
Returns undef if neither is set. Callers must not fall back to '.' -
|
||||
that's the Koha process's own working directory, which has no relation to
|
||||
any configured transport location and would silently list/act on whatever
|
||||
directory the process happens to be running in (e.g. the Koha install
|
||||
root) instead of failing cleanly.
|
||||
|
||||
=cut
|
||||
|
||||
sub _working_directory {
|
||||
my ( $self, $configured_field ) = @_;
|
||||
|
||||
return $self->{current_directory} || $self->$configured_field;
|
||||
}
|
||||
|
||||
=head3 _upload_file
|
||||
|
||||
Internal method that performs the local file system upload operation.
|
||||
|
|
@ -136,24 +129,21 @@ sub _upload_file {
|
|||
my ( $self, $local_file, $remote_file ) = @_;
|
||||
my $operation = "upload";
|
||||
|
||||
my $upload_dir = $self->_working_directory('upload_directory');
|
||||
unless ( defined $upload_dir ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
{ error => 'No upload directory configured or set via change_directory()' }
|
||||
);
|
||||
}
|
||||
|
||||
my $upload_dir = $self->{current_directory} || $self->upload_directory || '.';
|
||||
my $destination = File::Spec->catfile( $upload_dir, $remote_file );
|
||||
|
||||
unless ( copy( $local_file, $destination ) ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => $!,
|
||||
path => $destination
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => $!,
|
||||
path => $destination
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$self->add_message(
|
||||
|
|
@ -179,34 +169,35 @@ sub _download_file {
|
|||
my ( $self, $remote_file, $local_file ) = @_;
|
||||
my $operation = 'download';
|
||||
|
||||
my $download_dir = $self->_working_directory('download_directory');
|
||||
unless ( defined $download_dir ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
{ error => 'No download directory configured or set via change_directory()' }
|
||||
);
|
||||
}
|
||||
|
||||
my $source = File::Spec->catfile( $download_dir, $remote_file );
|
||||
my $download_dir = $self->{current_directory} || $self->download_directory || '.';
|
||||
my $source = File::Spec->catfile( $download_dir, $remote_file );
|
||||
|
||||
unless ( -f $source ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => "File not found: $source",
|
||||
path => $source
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => "File not found: $source",
|
||||
path => $source
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
unless ( copy( $source, $local_file ) ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => $!,
|
||||
path => $source
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => $!,
|
||||
path => $source
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$self->add_message(
|
||||
|
|
@ -236,13 +227,17 @@ sub _change_directory {
|
|||
|
||||
# For local file transport, we just track the current directory
|
||||
if ( $remote_directory && !-d $remote_directory ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => "Directory not found: $remote_directory",
|
||||
path => $remote_directory
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => "Directory not found: $remote_directory",
|
||||
path => $remote_directory
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$self->{current_directory} = $remote_directory;
|
||||
|
|
@ -258,27 +253,11 @@ sub _change_directory {
|
|||
return 1;
|
||||
}
|
||||
|
||||
=head3 _current_directory
|
||||
|
||||
my $path = $self->_current_directory;
|
||||
|
||||
Internal method that returns the local transport's current working
|
||||
directory: the directory most recently set via change_directory(), falling
|
||||
back to the configured download_directory, then to '.'.
|
||||
|
||||
=cut
|
||||
|
||||
sub _current_directory {
|
||||
my ($self) = @_;
|
||||
|
||||
return $self->{current_directory} || $self->download_directory || '.';
|
||||
}
|
||||
|
||||
=head3 _list_files
|
||||
|
||||
Internal method that performs the local file system file listing operation.
|
||||
Returns an array reference of hashrefs with file information.
|
||||
Each hashref contains: filename, longname, size, perms, mtime, type.
|
||||
Each hashref contains: filename, longname, size, perms, mtime.
|
||||
|
||||
=cut
|
||||
|
||||
|
|
@ -286,53 +265,49 @@ sub _list_files {
|
|||
my ($self) = @_;
|
||||
my $operation = "list";
|
||||
|
||||
my $directory = $self->_working_directory('download_directory');
|
||||
unless ( defined $directory ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
{ error => 'No download directory configured or set via change_directory()' }
|
||||
);
|
||||
}
|
||||
my $directory = $self->{current_directory} || $self->download_directory || '.';
|
||||
|
||||
unless ( -d $directory ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => "Directory not found: $directory",
|
||||
path => $directory
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => "Directory not found: $directory",
|
||||
path => $directory
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
my $dir_handle = IO::Dir->new($directory);
|
||||
unless ($dir_handle) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => "Cannot open directory: $!",
|
||||
path => $directory
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => "Cannot open directory: $!",
|
||||
path => $directory
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
my @files;
|
||||
while ( defined( my $file = $dir_handle->read ) ) {
|
||||
next if $file =~ /^\.\.?$/; # Skip . and ..
|
||||
my $full_path = File::Spec->catfile( $directory, $file );
|
||||
next unless -f $full_path; # Only files
|
||||
|
||||
# Get file stats for consistency with the SFTP/FTP format
|
||||
my @stat = stat($full_path);
|
||||
next unless @stat; # Skip entries we can't stat (e.g. broken symlinks)
|
||||
|
||||
# Get file stats for consistency with SFTP format
|
||||
my @stat = stat($full_path);
|
||||
my $size = $stat[7] || 0;
|
||||
my $mtime = $stat[9] || 0;
|
||||
my $mode = $stat[2] || 0;
|
||||
|
||||
my $type =
|
||||
S_ISDIR($mode) ? 'directory'
|
||||
: S_ISREG($mode) ? 'file'
|
||||
: 'other';
|
||||
|
||||
# Create permissions string (simplified)
|
||||
my $perms = sprintf( "%04o", $mode & oct('07777') );
|
||||
|
||||
|
|
@ -341,8 +316,7 @@ sub _list_files {
|
|||
longname => sprintf( "%s %8d %s %s", $perms, $size, scalar( localtime($mtime) ), $file ),
|
||||
size => $size,
|
||||
perms => $perms,
|
||||
mtime => $mtime,
|
||||
type => $type,
|
||||
mtime => $mtime
|
||||
};
|
||||
}
|
||||
$dir_handle->close;
|
||||
|
|
@ -373,35 +347,36 @@ sub _rename_file {
|
|||
my ( $self, $old_name, $new_name ) = @_;
|
||||
my $operation = "rename";
|
||||
|
||||
my $directory = $self->_working_directory('download_directory');
|
||||
unless ( defined $directory ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
{ error => 'No download directory configured or set via change_directory()' }
|
||||
);
|
||||
}
|
||||
|
||||
my $old_path = File::Spec->catfile( $directory, $old_name );
|
||||
my $new_path = File::Spec->catfile( $directory, $new_name );
|
||||
my $directory = $self->{current_directory} || $self->download_directory || '.';
|
||||
my $old_path = File::Spec->catfile( $directory, $old_name );
|
||||
my $new_path = File::Spec->catfile( $directory, $new_name );
|
||||
|
||||
unless ( -f $old_path ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => "File not found: $old_path",
|
||||
path => $old_path
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => "File not found: $old_path",
|
||||
path => $old_path
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
unless ( move( $old_path, $new_path ) ) {
|
||||
return $self->_abort_operation(
|
||||
$operation,
|
||||
$self->add_message(
|
||||
{
|
||||
error => $!,
|
||||
path => "$old_path -> $new_path"
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => {
|
||||
error => $!,
|
||||
path => "$old_path -> $new_path"
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$self->add_message(
|
||||
|
|
@ -415,29 +390,6 @@ sub _rename_file {
|
|||
return 1;
|
||||
}
|
||||
|
||||
=head3 _abort_operation
|
||||
|
||||
return $self->_abort_operation( $operation, \%payload );
|
||||
|
||||
Records an error message for the named operation, persisting a status
|
||||
snapshot via the shared _record_error() (see Koha::File::Transport), and
|
||||
returns nothing, so callers can
|
||||
C<return $self-E<gt>_abort_operation($operation, \%payload)> on failure.
|
||||
|
||||
Unlike FTP/SFTP there is no live connection to abort here - this purely
|
||||
records the failure, but does so the same way as the other two backends so
|
||||
a Local transport's displayed status reflects real usage too.
|
||||
|
||||
=cut
|
||||
|
||||
sub _abort_operation {
|
||||
my ( $self, $operation, $payload ) = @_;
|
||||
|
||||
$self->_record_error( $operation, $payload );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
=head3 _is_connected
|
||||
|
||||
Internal method to check if transport is currently connected.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use File::Spec;
|
|||
use IO::File;
|
||||
use Net::SFTP::Foreign;
|
||||
use Try::Tiny;
|
||||
use Fcntl qw( S_ISDIR S_ISREG );
|
||||
use JSON qw( decode_json encode_json );
|
||||
|
||||
use base qw(Koha::File::Transport);
|
||||
|
||||
|
|
@ -137,24 +137,6 @@ sub _download_file {
|
|||
return 1;
|
||||
}
|
||||
|
||||
=head3 _current_directory
|
||||
|
||||
my $path = $self->_current_directory;
|
||||
|
||||
Internal method that returns the SFTP connection's current working directory.
|
||||
|
||||
Net::SFTP::Foreign's C<cwd()> is a pure read-only accessor - it reports the
|
||||
current remote directory without navigating - unlike FTP's C<cwd()> (see
|
||||
Koha::File::Transport::FTP::_current_directory).
|
||||
|
||||
=cut
|
||||
|
||||
sub _current_directory {
|
||||
my ($self) = @_;
|
||||
|
||||
return $self->{connection}->cwd;
|
||||
}
|
||||
|
||||
=head3 _change_directory
|
||||
|
||||
my $success = $server->_change_directory($directory);
|
||||
|
|
@ -190,16 +172,7 @@ sub _change_directory {
|
|||
|
||||
Internal method that performs the SFTP-specific file listing operation.
|
||||
Returns an array reference of hashrefs with file information.
|
||||
Each hashref contains: filename, longname, size, perms, mtime, type.
|
||||
|
||||
Net::SFTP::Foreign's ls() natively returns the size/perms/mtime nested inside
|
||||
a Net::SFTP::Foreign::Attributes object (the 'a' key). Those fields are
|
||||
flattened out here so the returned shape matches the FTP and Local transports.
|
||||
|
||||
Net::SFTP::Foreign::ls() does not filter '.' and '..' out of its
|
||||
SSH_FXP_READDIR response itself - whether those pseudo-entries appear is
|
||||
purely down to the remote server implementation. They are skipped here so
|
||||
behaviour is consistent with the FTP and Local transports.
|
||||
Each hashref contains: filename, longname, a (attributes).
|
||||
|
||||
=cut
|
||||
|
||||
|
|
@ -209,22 +182,6 @@ sub _list_files {
|
|||
|
||||
my $file_list = $self->{connection}->ls or return $self->_abort_operation($operation);
|
||||
|
||||
my @files = map {
|
||||
my $attrs = $_->{a};
|
||||
my $mode = $attrs ? $attrs->perm : undef;
|
||||
{
|
||||
filename => $_->{filename},
|
||||
longname => $_->{longname},
|
||||
size => $attrs ? $attrs->size : undef,
|
||||
perms => $attrs && defined $mode ? sprintf( "%04o", $mode & oct('07777') ) : undef,
|
||||
mtime => $attrs ? $attrs->mtime : undef,
|
||||
type => !defined $mode ? 'other'
|
||||
: S_ISDIR($mode) ? 'directory'
|
||||
: S_ISREG($mode) ? 'file'
|
||||
: 'other',
|
||||
}
|
||||
} grep { $_->{filename} !~ /^\.\.?$/ } @{$file_list};
|
||||
|
||||
$self->add_message(
|
||||
{
|
||||
message => $operation,
|
||||
|
|
@ -237,7 +194,7 @@ sub _list_files {
|
|||
}
|
||||
);
|
||||
|
||||
return \@files;
|
||||
return $file_list;
|
||||
}
|
||||
|
||||
=head3 _rename_file
|
||||
|
|
@ -381,10 +338,6 @@ sub _locate_key_file {
|
|||
|
||||
Helper method to abort the current operation and return.
|
||||
|
||||
Records the failure via the shared _record_error() (see
|
||||
Koha::File::Transport), which persists a status snapshot the same way for
|
||||
every transport type.
|
||||
|
||||
=cut
|
||||
|
||||
sub _abort_operation {
|
||||
|
|
@ -400,12 +353,24 @@ sub _abort_operation {
|
|||
error_raw => $stderr
|
||||
};
|
||||
|
||||
$self->_record_error( $operation, $payload );
|
||||
$self->add_message(
|
||||
{
|
||||
message => $operation,
|
||||
type => 'error',
|
||||
payload => $payload
|
||||
}
|
||||
);
|
||||
|
||||
if ( $self->{connection} ) {
|
||||
$self->{connection}->abort;
|
||||
}
|
||||
|
||||
my $status = {
|
||||
status => 'errors',
|
||||
operations => [ { code => $operation, status => 'error', detail => $payload } ]
|
||||
};
|
||||
$self->set( { status => encode_json($status) } )->store();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
48
Koha/Hold.pm
48
Koha/Hold.pm
|
|
@ -34,7 +34,7 @@ use Koha::Biblios;
|
|||
use Koha::Hold::CancellationRequests;
|
||||
use Koha::Items;
|
||||
use Koha::Libraries;
|
||||
use Koha::Library::Calendar;
|
||||
use Koha::Calendar;
|
||||
use Koha::Plugins;
|
||||
use Koha::ILL::ISO18626::Request;
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ sub age {
|
|||
my $age;
|
||||
|
||||
if ($use_calendar) {
|
||||
my $calendar = Koha::Library::Calendar->new( branchcode => $self->branchcode );
|
||||
my $calendar = Koha::Calendar->new( branchcode => $self->branchcode );
|
||||
$age = $calendar->days_between( dt_from_string( $self->reservedate ), $today );
|
||||
} else {
|
||||
$age = $today->delta_days( dt_from_string( $self->reservedate ) );
|
||||
|
|
@ -214,32 +214,14 @@ sub move_hold {
|
|||
$new_itemnumber = $item->itemnumber;
|
||||
$new_biblionumber = $item->biblionumber;
|
||||
|
||||
# need to make sure the item is being moved to a valid pickup location
|
||||
return { success => 0, error => 'invalidPickupBranch' } unless $self->_pickup_valid_for_item( $item, $patron );
|
||||
|
||||
$canReserve = C4::Reserves::CanItemBeReserved( $patron, $item, $self->branchcode, { ignore_hold_counts => 1 } );
|
||||
} elsif ( $args->{new_biblionumber} ) {
|
||||
$new_biblionumber = $args->{new_biblionumber};
|
||||
|
||||
# for record level holds we must check to make sure there is at least 1 item on the record that can fulfill the new hold
|
||||
my @items = Koha::Items->search( { biblionumber => $new_biblionumber } )->as_list;
|
||||
|
||||
return { success => 0, error => 'noItemsOnBib' } unless @items;
|
||||
|
||||
my $valid_pickup_item = 0;
|
||||
foreach my $candidate_item (@items) {
|
||||
if ( $self->_pickup_valid_for_item( $candidate_item, $patron ) ) {
|
||||
$valid_pickup_item = 1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
return { success => 0, error => 'invalidPickupBranch' } unless $valid_pickup_item;
|
||||
|
||||
$canReserve = C4::Reserves::CanBookBeReserved(
|
||||
$patron->borrowernumber, $new_biblionumber, $self->branchcode,
|
||||
{ ignore_hold_counts => 1 }
|
||||
);
|
||||
|
||||
} else {
|
||||
return { success => 0, error => 'Missing itemnumber or biblionumber' };
|
||||
}
|
||||
|
|
@ -428,7 +410,7 @@ sub set_waiting {
|
|||
branchcode => $self->branchcode,
|
||||
}
|
||||
);
|
||||
my $calendar = Koha::Library::Calendar->new( branchcode => $self->branchcode, days_mode => $daysmode );
|
||||
my $calendar = Koha::Calendar->new( branchcode => $self->branchcode, days_mode => $daysmode );
|
||||
|
||||
$new_expiration_date = $calendar->days_forward( dt_from_string( $self->waitingdate ), $max_pickup_delay );
|
||||
}
|
||||
|
|
@ -1715,30 +1697,6 @@ sub strings_map {
|
|||
return $strings;
|
||||
}
|
||||
|
||||
=head3 _pickup_valid_for_item
|
||||
|
||||
Helper method to determine if the pickup location for an item is valid when a hold is moved.
|
||||
Returns true if the pickup location is valid.
|
||||
|
||||
=cut
|
||||
|
||||
sub _pickup_valid_for_item {
|
||||
my ( $self, $item, $patron ) = @_;
|
||||
|
||||
my $branchitemrule = C4::Circulation::GetBranchItemRule(
|
||||
Koha::Policy::Holds->holds_control_library( $item, $patron ),
|
||||
$item->effective_itemtype
|
||||
);
|
||||
|
||||
my $policy = $branchitemrule->{hold_fulfillment_policy};
|
||||
|
||||
return 0
|
||||
if ( $policy eq 'homebranch' || $policy eq 'holdingbranch' )
|
||||
&& $self->branchcode ne $item->$policy;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
=head2 Internal methods
|
||||
|
||||
=head3 _type
|
||||
|
|
|
|||
|
|
@ -161,18 +161,11 @@ sub get_items_that_can_fill {
|
|||
collapse => 1,
|
||||
}
|
||||
)->get_column('itemnumber');
|
||||
my @pending_recalls = Koha::Recalls->search(
|
||||
{ 'status' => [ 'waiting', 'in_transit' ] },
|
||||
{
|
||||
columns => ['item_id'],
|
||||
collapse => 1,
|
||||
}
|
||||
)->get_column('item_id');
|
||||
|
||||
return Koha::Items->search(
|
||||
{
|
||||
-or => \@bibs_or_items,
|
||||
itemnumber => { -not_in => [ @branchtransfers, @waiting_holds, @pending_recalls ] },
|
||||
itemnumber => { -not_in => [ @branchtransfers, @waiting_holds ] },
|
||||
onloan => undef,
|
||||
notforloan => 0,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,8 @@ Return the I<Koha::ILL::Requests> for this batch
|
|||
|
||||
sub requests {
|
||||
my ($self) = @_;
|
||||
return Koha::ILL::Requests->_new_from_dbic( scalar $self->_result->requests );
|
||||
my $requests = $self->_result->requests;
|
||||
return Koha::ILL::Requests->_new_from_dbic($requests);
|
||||
}
|
||||
|
||||
=head3 create_and_log
|
||||
|
|
@ -107,7 +108,7 @@ sub create_and_log {
|
|||
|
||||
$logger->log_something(
|
||||
{
|
||||
modulename => 'ILL_BATCHES',
|
||||
modulename => 'ILL',
|
||||
actionname => 'batch_create',
|
||||
objectnumber => $self->id,
|
||||
infos => to_json( {} )
|
||||
|
|
@ -143,7 +144,7 @@ sub update_and_log {
|
|||
|
||||
$logger->log_something(
|
||||
{
|
||||
modulename => 'ILL_BATCHES',
|
||||
modulename => 'ILL',
|
||||
actionname => 'batch_update',
|
||||
objectnumber => $self->id,
|
||||
infos => to_json(
|
||||
|
|
@ -171,7 +172,7 @@ sub delete_and_log {
|
|||
|
||||
$logger->log_something(
|
||||
{
|
||||
modulename => 'ILL_BATCHES',
|
||||
modulename => 'ILL',
|
||||
actionname => 'batch_delete',
|
||||
objectnumber => $self->id,
|
||||
infos => to_json( {} )
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ sub create_and_log {
|
|||
|
||||
$logger->log_something(
|
||||
{
|
||||
modulename => 'ILL_BATCHES',
|
||||
modulename => 'ILL',
|
||||
actionname => 'batch_status_create',
|
||||
objectnumber => $self->id,
|
||||
infos => to_json( {} )
|
||||
|
|
@ -94,7 +94,7 @@ sub update_and_log {
|
|||
|
||||
$logger->log_something(
|
||||
{
|
||||
modulename => 'ILL_BATCHES',
|
||||
modulename => 'ILL',
|
||||
actionname => 'batch_status_update',
|
||||
objectnumber => $self->id,
|
||||
infos => to_json(
|
||||
|
|
@ -131,7 +131,7 @@ sub delete_and_log {
|
|||
|
||||
$logger->log_something(
|
||||
{
|
||||
modulename => 'ILL_BATCHES',
|
||||
modulename => 'ILL',
|
||||
actionname => 'batch_status_delete',
|
||||
objectnumber => $self->id,
|
||||
infos => to_json( {} )
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ use Koha::Items;
|
|||
use Koha::Libraries;
|
||||
use Koha::AdditionalContents;
|
||||
use Koha::Patron;
|
||||
use Koha::Statistics;
|
||||
|
||||
use C4::Circulation qw( CanBookBeIssued AddIssue );
|
||||
|
||||
|
|
@ -450,7 +449,7 @@ Returns the installed I<Koha::Plugin> corresponding to the given backend_id or u
|
|||
sub get_backend_plugin {
|
||||
my ( $self, $backend_id ) = @_;
|
||||
|
||||
if ( !$self->{_plugin}{$backend_id} ) {
|
||||
if ( !$self->{_plugin} ) {
|
||||
my $koha_plugins = Koha::Plugins->new();
|
||||
my @backend_plugins = $koha_plugins
|
||||
? Koha::Plugins->new()->GetPlugins(
|
||||
|
|
@ -462,11 +461,11 @@ sub get_backend_plugin {
|
|||
)
|
||||
: ();
|
||||
|
||||
$self->{_plugin}{$backend_id} = $backend_plugins[0]
|
||||
$self->{_plugin} = $backend_plugins[0]
|
||||
if scalar @backend_plugins;
|
||||
}
|
||||
|
||||
return $self->{_plugin}{$backend_id};
|
||||
return $self->{_plugin};
|
||||
}
|
||||
|
||||
=head3 load_backend
|
||||
|
|
@ -1726,12 +1725,6 @@ sub check_out {
|
|||
}
|
||||
|
||||
# We can allegedly check out, so make it so
|
||||
# Mark as ILL checkout in the logs to track system-initiated checkouts.
|
||||
# AddIssue signature is ( $patron, $barcode, $datedue, $cancelreserve,
|
||||
# $issuedate, $sipmode, $params ) - pad to position 7 so the params
|
||||
# hashref is not mistaken for $issuedate or $sipmode.
|
||||
push @issue_args, undef while @issue_args < 6;
|
||||
push @issue_args, { forced => ['ILL_CHECKOUT'] };
|
||||
my $issue = C4::Circulation::AddIssue(@issue_args);
|
||||
|
||||
if ($issue) {
|
||||
|
|
@ -2276,7 +2269,7 @@ Actions to be done after the request has been completed
|
|||
sub after_completed {
|
||||
my ($self) = @_;
|
||||
|
||||
Koha::Statistic->new(
|
||||
C4::Stats::UpdateStats(
|
||||
{
|
||||
borrowernumber => $self->borrowernumber // undef,
|
||||
branch => $self->branchcode,
|
||||
|
|
@ -2288,7 +2281,7 @@ sub after_completed {
|
|||
location => undef,
|
||||
type => 'illreq_comp',
|
||||
}
|
||||
)->store();
|
||||
);
|
||||
}
|
||||
|
||||
=head3 after_created
|
||||
|
|
@ -2302,7 +2295,7 @@ Actions to be done after the request has been fully created
|
|||
sub after_created {
|
||||
my ($self) = @_;
|
||||
|
||||
Koha::Statistic->new(
|
||||
C4::Stats::UpdateStats(
|
||||
{
|
||||
borrowernumber => $self->borrowernumber // undef,
|
||||
branch => $self->branchcode,
|
||||
|
|
@ -2314,7 +2307,7 @@ sub after_created {
|
|||
location => undef,
|
||||
type => 'illreq_created',
|
||||
}
|
||||
)->store();
|
||||
);
|
||||
}
|
||||
|
||||
=head3 requested_partners
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ sub replace {
|
|||
if ($biblio) {
|
||||
my $record = $biblio->metadata->record;
|
||||
$xmlrecord = $record->as_xml;
|
||||
my $context = { source => 'add_to_basket' };
|
||||
my $context = { source => 'batchimport' };
|
||||
if ($logged_in_patron) {
|
||||
$context->{categorycode} = $logged_in_patron->categorycode;
|
||||
$context->{userid} = $logged_in_patron->userid;
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ sub new_from_file {
|
|||
);
|
||||
}
|
||||
|
||||
if ( scalar @$errors ) {
|
||||
if ($errors) {
|
||||
Koha::Logger->get->warn( 'The following error(s) occurred during '
|
||||
. ( defined $format ? ( $format . " " ) : "" )
|
||||
. "record import:\nERROR: "
|
||||
|
|
|
|||
198
Koha/Item.pm
198
Koha/Item.pm
|
|
@ -20,13 +20,13 @@ package Koha::Item;
|
|||
use Modern::Perl;
|
||||
|
||||
use List::MoreUtils qw( any );
|
||||
use Try::Tiny qw( catch try );
|
||||
|
||||
use Koha::Database;
|
||||
use Koha::DateUtils qw( dt_from_string output_pref );
|
||||
|
||||
use C4::Context;
|
||||
use C4::Circulation qw( barcodedecode GetBranchItemRule );
|
||||
use Koha::Policy::Circulation;
|
||||
use C4::Reserves;
|
||||
use C4::ClassSource qw( GetClassSort );
|
||||
use C4::Log qw( logaction );
|
||||
|
|
@ -43,7 +43,6 @@ use Koha::Exceptions::Checkin;
|
|||
use Koha::Exceptions::Item::Bundle;
|
||||
use Koha::Exceptions::Item::Transfer;
|
||||
use Koha::Item::Attributes;
|
||||
use Koha::Item::Availability::Checkin;
|
||||
use Koha::Exceptions::Item::Bundle;
|
||||
use Koha::Item::Transfer::Limits;
|
||||
use Koha::Item::Transfers;
|
||||
|
|
@ -59,7 +58,6 @@ use Koha::StockRotationItem;
|
|||
use Koha::StockRotationRotas;
|
||||
use Koha::TrackedLinks;
|
||||
use Koha::Policy::Holds;
|
||||
use Koha::Statistics;
|
||||
|
||||
use base qw(Koha::Object);
|
||||
|
||||
|
|
@ -270,7 +268,7 @@ Record a statistics entry for this item (e.g. on store or delete).
|
|||
|
||||
sub _add_statistic {
|
||||
my ( $self, $type ) = @_;
|
||||
Koha::Statistic->new(
|
||||
C4::Stats::UpdateStats(
|
||||
{
|
||||
borrowernumber => undef,
|
||||
branch => C4::Context->userenv ? C4::Context->userenv->{branch} : undef,
|
||||
|
|
@ -281,7 +279,7 @@ sub _add_statistic {
|
|||
location => $self->location,
|
||||
type => $type,
|
||||
}
|
||||
)->store();
|
||||
);
|
||||
}
|
||||
|
||||
=head3 delete
|
||||
|
|
@ -631,17 +629,12 @@ sub bookings {
|
|||
|
||||
=head3 find_booking
|
||||
|
||||
my $booking = $item->find_booking(
|
||||
{ checkout_date => $now, due_date => $future_date, [ exclude_booking_id => $booking_id ] } );
|
||||
my $booking = $item->find_booking( { checkout_date => $now, due_date => $future_date } );
|
||||
|
||||
Find the first booking that would conflict with the passed checkout dates for this item. If a booking
|
||||
lead period is configured for the itemtype we will also take that into account here, counting bookings
|
||||
that fall in that lead period as conflicts too.
|
||||
|
||||
Optionally, you may pass an exclude_booking_id to leave a specific booking out of the checks; this is
|
||||
helpful when looking for bookings that would clash with an extension of the checkout that is already
|
||||
satisfying that booking.
|
||||
|
||||
FIXME: This can be simplified, it was originally intended to iterate all biblio level bookings
|
||||
to catch cases where this item may be the last available to satisfy a biblio level only booking.
|
||||
However, we dropped the biblio level functionality prior to push as bugs were found in it's
|
||||
|
|
@ -652,10 +645,9 @@ implementation.
|
|||
sub find_booking {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
my $start_date = $params->{checkout_date};
|
||||
my $end_date = $params->{due_date};
|
||||
my $exclude_booking_id = $params->{exclude_booking_id};
|
||||
my $biblio = $self->biblio;
|
||||
my $start_date = $params->{checkout_date};
|
||||
my $end_date = $params->{due_date};
|
||||
my $biblio = $self->biblio;
|
||||
|
||||
my $rule = Koha::CirculationRules->get_effective_rule(
|
||||
{
|
||||
|
|
@ -697,8 +689,7 @@ sub find_booking {
|
|||
}
|
||||
]
|
||||
},
|
||||
{ status => { '-not_in' => [ 'cancelled', 'completed' ] } },
|
||||
( $exclude_booking_id ? { booking_id => { '!=' => $exclude_booking_id } } : () ),
|
||||
{ status => { '-not_in' => [ 'cancelled', 'completed' ] } }
|
||||
]
|
||||
},
|
||||
{ order_by => { '-asc' => 'start_date' } }
|
||||
|
|
@ -759,14 +750,7 @@ sub check_booking {
|
|||
my $booking_id = $params->{booking_id};
|
||||
|
||||
if ( my $checkout = $self->checkout ) {
|
||||
|
||||
# A checkout linked to the booking under consideration must not block
|
||||
# that booking's own updates (i.e. extending an issued booking)
|
||||
my $is_own_checkout =
|
||||
defined($booking_id)
|
||||
&& defined( $checkout->booking_id )
|
||||
&& $checkout->booking_id == $booking_id;
|
||||
return 0 if !$is_own_checkout && $start_date <= dt_from_string( $checkout->date_due );
|
||||
return 0 if ( $start_date <= dt_from_string( $checkout->date_due ) );
|
||||
}
|
||||
|
||||
my $dtf = Koha::Database->new->schema->storage->datetime_parser;
|
||||
|
|
@ -807,91 +791,6 @@ sub check_booking {
|
|||
return $bookings_count ? 0 : 1;
|
||||
}
|
||||
|
||||
=head3 can_be_returned_at
|
||||
|
||||
my ($allowed, $message) = $item->can_be_returned_at(
|
||||
{
|
||||
library => $branchcode,
|
||||
to_library => $destination_branchcode, # optional
|
||||
}
|
||||
);
|
||||
|
||||
Checks whether this item can be returned at the given library, based on
|
||||
the AllowReturnToBranch system preference and branch transfer limits.
|
||||
|
||||
Returns a two-element list: ($allowed, $message).
|
||||
|
||||
When the return is not allowed, $message contains the branchcode of
|
||||
the library where the item should be returned instead.
|
||||
|
||||
=cut
|
||||
|
||||
sub can_be_returned_at {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
my $library = $params->{library};
|
||||
my $to_library = $params->{to_library};
|
||||
|
||||
my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
|
||||
|
||||
# Refuse check-in if it does not respect AllowReturnToBranch rules
|
||||
if ( $allowreturntobranch eq 'homebranch' && $library ne $self->homebranch ) {
|
||||
return ( 0, $self->homebranch );
|
||||
} elsif ( $allowreturntobranch eq 'holdingbranch' && $library ne $self->holdingbranch ) {
|
||||
return ( 0, $self->holdingbranch );
|
||||
} elsif ( $allowreturntobranch eq 'homeorholdingbranch'
|
||||
&& $library ne $self->homebranch
|
||||
&& $library ne $self->holdingbranch )
|
||||
{
|
||||
return ( 0, $self->homebranch ); # FIXME: choice of homebranch is arbitrary
|
||||
}
|
||||
|
||||
# Refuse check-in if item cannot be transferred to $to_library due to transfer limits
|
||||
if ( defined $to_library ) {
|
||||
my $from = Koha::Libraries->find($library);
|
||||
my $to = Koha::Libraries->find($to_library);
|
||||
if ( !$self->can_be_transferred( { from => $from, to => $to } ) ) {
|
||||
return ( 0, $to_library );
|
||||
}
|
||||
}
|
||||
|
||||
return ( 1, undef );
|
||||
}
|
||||
|
||||
=head3 checkin_availability
|
||||
|
||||
my $availability = $item->checkin_availability(
|
||||
{
|
||||
library => $branchcode,
|
||||
to_library => $destination_branchcode,
|
||||
}
|
||||
);
|
||||
|
||||
Returns check-in availability information for this item.
|
||||
|
||||
This is a convenience method that calls Koha::Item::Availability::Checkin->check().
|
||||
|
||||
Parameters:
|
||||
library => branchcode where the return takes place (required)
|
||||
to_library => branchcode where the item should be sent after return
|
||||
(optional, for transfer limit validation)
|
||||
|
||||
Returns a Koha::Result::Availability object with methods:
|
||||
available() # Boolean: true if check-in allowed
|
||||
needs_confirmation()# Boolean: true if confirmation required
|
||||
blockers() # Hashref of blocking conditions
|
||||
confirmations() # Hashref of conditions requiring confirmation
|
||||
warnings() # Hashref of warning messages
|
||||
context() # Hashref with checkout and patron objects
|
||||
|
||||
=cut
|
||||
|
||||
sub checkin_availability {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
return Koha::Item::Availability::Checkin->check( $self, $params );
|
||||
}
|
||||
|
||||
=head3 request_transfer
|
||||
|
||||
my $transfer = $item->request_transfer(
|
||||
|
|
@ -1778,7 +1677,7 @@ sub _status {
|
|||
if ( $self->first_hold ) {
|
||||
push @statuses, 'on_hold';
|
||||
}
|
||||
if ( C4::Context->preference('UseRecalls') ne "off" && $self->recall ) {
|
||||
if ( C4::Context->preference('UseRecalls') && $self->recall ) {
|
||||
push @statuses, 'recalled';
|
||||
}
|
||||
|
||||
|
|
@ -2515,38 +2414,48 @@ sub add_to_bundle {
|
|||
|
||||
$schema->txn_do(
|
||||
sub {
|
||||
Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $self->checkout;
|
||||
try {
|
||||
Koha::Exceptions::Item::Bundle::BundleIsCheckedOut->throw if $self->checkout;
|
||||
|
||||
my $checkout = $bundle_item->checkout;
|
||||
if ($checkout) {
|
||||
unless ( $options->{force_checkin} ) {
|
||||
Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
|
||||
my $checkout = $bundle_item->checkout;
|
||||
if ($checkout) {
|
||||
unless ( $options->{force_checkin} ) {
|
||||
Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
|
||||
}
|
||||
|
||||
my $branchcode = C4::Context->userenv->{'branch'};
|
||||
my ($success) = C4::Circulation::AddReturn( $bundle_item->barcode, $branchcode );
|
||||
|
||||
if ($success) {
|
||||
|
||||
# HoldsQueue doesn't seem to mind bundles, not sure if this is correct, but rebuild for now
|
||||
Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
|
||||
{ biblio_ids => [ $self->biblionumber ] } )
|
||||
if C4::Context->preference('RealTimeHoldsQueue');
|
||||
} else {
|
||||
Koha::Exceptions::Checkin::FailedCheckin->throw();
|
||||
}
|
||||
}
|
||||
|
||||
my $branchcode = C4::Context->userenv->{'branch'};
|
||||
my ($success) = C4::Circulation::AddReturn( $bundle_item->barcode, $branchcode );
|
||||
|
||||
if ($success) {
|
||||
|
||||
# HoldsQueue doesn't seem to mind bundles, not sure if this is correct, but rebuild for now
|
||||
Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
|
||||
{ biblio_ids => [ $self->biblionumber ] } )
|
||||
if C4::Context->preference('RealTimeHoldsQueue');
|
||||
} else {
|
||||
Koha::Exceptions::Checkin::FailedCheckin->throw();
|
||||
my $holds = $bundle_item->current_holds;
|
||||
if ( $holds->count ) {
|
||||
unless ( $options->{ignore_holds} ) {
|
||||
Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
my $holds = $bundle_item->current_holds;
|
||||
if ( $holds->count ) {
|
||||
unless ( $options->{ignore_holds} ) {
|
||||
Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
|
||||
}
|
||||
}
|
||||
$self->_result->add_to_item_bundles_hosts( { item => $bundle_item->itemnumber } );
|
||||
|
||||
$self->_result->add_to_item_bundles_hosts( { item => $bundle_item->itemnumber } );
|
||||
$bundle_item->notforloan($BundleNotLoanValue)->store();
|
||||
} catch {
|
||||
|
||||
$bundle_item->notforloan($BundleNotLoanValue)->store();
|
||||
# Translate raw DBIC exceptions (from add_to_item_bundles_hosts)
|
||||
$self->_result->result_source->schema->translate_exception($_)
|
||||
if ref($_) eq 'DBIx::Class::Exception';
|
||||
|
||||
# Propagate Koha exceptions as-is
|
||||
$_->rethrow();
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -2644,14 +2553,12 @@ sub recall {
|
|||
|
||||
Does item-level checks and returns if items can be recalled by this borrower
|
||||
|
||||
If hold_convert $param is included, this is a check to convert a hold to a recall, so we should not check for an existing hold.
|
||||
|
||||
=cut
|
||||
|
||||
sub can_be_recalled {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
return 0 if ( C4::Context->preference('UseRecalls') eq "off" );
|
||||
return 0 if !( C4::Context->preference('UseRecalls') );
|
||||
|
||||
# check if this item is not for loan, withdrawn or lost
|
||||
return 0 if ( $self->notforloan != 0 );
|
||||
|
|
@ -2665,7 +2572,7 @@ sub can_be_recalled {
|
|||
|
||||
my $branchcode = C4::Context->userenv->{'branch'};
|
||||
if ($patron) {
|
||||
$branchcode = Koha::Policy::Circulation->circ_control_library( $self, $patron );
|
||||
$branchcode = C4::Circulation::_GetCircControlBranch( $self, $patron );
|
||||
}
|
||||
|
||||
# Check the circulation rule for each relevant itemtype for this item
|
||||
|
|
@ -2707,12 +2614,9 @@ sub can_be_recalled {
|
|||
->count > 0 );
|
||||
|
||||
# check if this patron has already reserved this item
|
||||
unless ( $params->{hold_convert} ) {
|
||||
return 0
|
||||
if (
|
||||
Koha::Holds->search( { itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber } )
|
||||
->count > 0 );
|
||||
}
|
||||
return 0
|
||||
if ( Koha::Holds->search( { itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber } )
|
||||
->count > 0 );
|
||||
}
|
||||
|
||||
# check item availability
|
||||
|
|
@ -2751,7 +2655,7 @@ At this point the item has already been recalled. We are now at the checkin and
|
|||
sub can_be_waiting_recall {
|
||||
my ($self) = @_;
|
||||
|
||||
return 0 if ( C4::Context->preference('UseRecalls') eq "off" );
|
||||
return 0 if !( C4::Context->preference('UseRecalls') );
|
||||
|
||||
# check if this item is not for loan, withdrawn or lost
|
||||
return 0 if ( $self->notforloan != 0 );
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
package Koha::Item::Availability::Checkin;
|
||||
|
||||
# Copyright 2026 Koha Development Team
|
||||
#
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use C4::Context;
|
||||
use Koha::Result::Availability;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Item::Availability::Checkin - Check-in availability validation for items
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
my $availability = Koha::Item::Availability::Checkin->check( $item,
|
||||
{
|
||||
library => $branchcode,
|
||||
to_library => $destination_branchcode,
|
||||
}
|
||||
);
|
||||
|
||||
# Or via instance method
|
||||
my $availability = $item->checkin_availability(
|
||||
{
|
||||
library => $branchcode,
|
||||
to_library => $destination_branchcode,
|
||||
}
|
||||
);
|
||||
|
||||
# Collect all blockers (e.g. for API responses)
|
||||
my $availability = $item->checkin_availability(
|
||||
{
|
||||
library => $branchcode,
|
||||
to_library => $destination_branchcode,
|
||||
no_short_circuit => 1,
|
||||
}
|
||||
);
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This class provides validation logic for item check-in operations, extracting
|
||||
and centralizing checks from C4::Circulation::AddReturn.
|
||||
|
||||
The result categorizes conditions as:
|
||||
- blockers: Prevent check-in (e.g., BlockReturnOfWithdrawnItems enabled)
|
||||
- confirmations: Noteworthy conditions (e.g., item not checked out)
|
||||
- warnings: Informational (e.g., item is withdrawn but check-in allowed)
|
||||
|
||||
=head1 API
|
||||
|
||||
=head2 Class methods
|
||||
|
||||
=head3 check
|
||||
|
||||
my $availability = Koha::Item::Availability::Checkin->check( $item,
|
||||
{
|
||||
library => $branchcode,
|
||||
to_library => $destination_branchcode,
|
||||
no_short_circuit => 1,
|
||||
}
|
||||
);
|
||||
|
||||
Validates check-in availability for an item.
|
||||
|
||||
Parameters:
|
||||
$item - Koha::Item object (required)
|
||||
$params - hashref with:
|
||||
library => branchcode where the return takes place (required)
|
||||
to_library => branchcode where the item should be sent after return
|
||||
(optional, for transfer limit validation)
|
||||
no_short_circuit => boolean, if true all checks are performed and all
|
||||
blockers collected. Default: false (short-circuit
|
||||
on first blocker). This follows the same pattern
|
||||
as Koha::Patron->can_place_holds.
|
||||
|
||||
Returns a Koha::Result::Availability object with:
|
||||
blockers => {} # Conditions that prevent check-in
|
||||
confirmations => {} # Conditions requiring confirmation
|
||||
warnings => {} # Informational messages
|
||||
context => {
|
||||
checkout => $checkout # Koha::Checkout object if checked out
|
||||
patron => $patron # Koha::Patron object if item is checked out
|
||||
}
|
||||
|
||||
=cut
|
||||
|
||||
sub check {
|
||||
my ( $class, $item, $params ) = @_;
|
||||
|
||||
my $library = $params->{library};
|
||||
my $to_library = $params->{to_library};
|
||||
my $no_short_circuit = $params->{no_short_circuit} // 0;
|
||||
|
||||
my $result = Koha::Result::Availability->new();
|
||||
|
||||
# Always check checkout status first so context is available to callers
|
||||
# even when blockers prevent the check-in
|
||||
my $checkout = $item->checkout;
|
||||
if ($checkout) {
|
||||
$result->set_context( checkout => $checkout );
|
||||
$result->set_context( patron => $checkout->patron );
|
||||
} else {
|
||||
|
||||
# Item not checked out
|
||||
$result->add_confirmation( NotIssued => $item->barcode );
|
||||
}
|
||||
|
||||
# Check if item is withdrawn
|
||||
if ( $item->withdrawn ) {
|
||||
if ( C4::Context->preference("BlockReturnOfWithdrawnItems") ) {
|
||||
$result->add_blocker( BlockedWithdrawn => 1 );
|
||||
return $result unless $no_short_circuit;
|
||||
} else {
|
||||
$result->add_warning( withdrawn => 1 );
|
||||
}
|
||||
}
|
||||
|
||||
# Check AllowReturnToBranch policy and branch transfer limits
|
||||
my ( $returnallowed, $message ) =
|
||||
$item->can_be_returned_at( { library => $library, to_library => $to_library } );
|
||||
unless ($returnallowed) {
|
||||
$result->add_blocker(
|
||||
Wrongbranch => {
|
||||
Wrongbranch => $library,
|
||||
Rightbranch => $message
|
||||
}
|
||||
);
|
||||
return $result unless $no_short_circuit;
|
||||
}
|
||||
|
||||
# Check if item is lost and blocked
|
||||
if ( $item->itemlost && C4::Context->preference("BlockReturnOfLostItems") ) {
|
||||
$result->add_blocker( BlockedLost => 1 );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Koha Development Team <https://koha-community.org/>
|
||||
|
||||
=cut
|
||||
|
||||
1;
|
||||
|
|
@ -77,7 +77,7 @@ Returns the associated item for this transfer.
|
|||
|
||||
sub item {
|
||||
my ($self) = @_;
|
||||
my $item_rs = $self->_result->item;
|
||||
my $item_rs = $self->_result->itemnumber;
|
||||
return Koha::Item->_new_from_dbic($item_rs);
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ Returns the associated from_library for this transfer.
|
|||
|
||||
sub from_library {
|
||||
my ($self) = @_;
|
||||
my $from_library_rs = $self->_result->from_library;
|
||||
my $from_library_rs = $self->_result->frombranch;
|
||||
return Koha::Library->_new_from_dbic($from_library_rs);
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ Returns the associated to_library for this transfer.
|
|||
|
||||
sub to_library {
|
||||
my ($self) = @_;
|
||||
my $to_library_rs = $self->_result->to_library;
|
||||
my $to_library_rs = $self->_result->tobranch;
|
||||
return Koha::Library->_new_from_dbic($to_library_rs);
|
||||
}
|
||||
|
||||
|
|
@ -216,28 +216,7 @@ sub strings_map {
|
|||
};
|
||||
}
|
||||
|
||||
=head3 to_api_mapping
|
||||
|
||||
This method returns the mapping for representing a Koha::Item::Transfer object on the API.
|
||||
|
||||
=cut
|
||||
|
||||
sub to_api_mapping {
|
||||
return {
|
||||
branchtransfer_id => 'transfer_id',
|
||||
itemnumber => 'item_id',
|
||||
daterequested => 'requested_date',
|
||||
datesent => 'sent_date',
|
||||
datearrived => 'arrived_date',
|
||||
datecancelled => 'cancellation_date',
|
||||
frombranch => 'from_library_id',
|
||||
tobranch => 'to_library_id',
|
||||
};
|
||||
}
|
||||
|
||||
=head3 _type
|
||||
|
||||
Returns the DBIC resultset class name backing this object.
|
||||
=head3 type
|
||||
|
||||
=cut
|
||||
|
||||
|
|
|
|||
106
Koha/Items.pm
106
Koha/Items.pm
|
|
@ -32,7 +32,6 @@ use Koha::SearchEngine::Indexer;
|
|||
use Koha::Item::Attributes;
|
||||
use Koha::Item;
|
||||
use Koha::CirculationRules;
|
||||
use Koha::Regex::Replacement;
|
||||
|
||||
use base qw(Koha::Objects);
|
||||
|
||||
|
|
@ -312,9 +311,9 @@ sub filter_by_has_recalls {
|
|||
|
||||
$params //= {};
|
||||
my $recalls = Koha::Recalls->search(
|
||||
{ %$params, 'me.item_id' => [ $self->get_column('itemnumber') ], 'me.item_level' => 1, },
|
||||
{ %$params, 'me.itemnumber' => [ $self->get_column('itemnumber') ], 'me.item_level' => 1, },
|
||||
{
|
||||
columns => ['item_id'],
|
||||
columns => ['itemnumber'],
|
||||
distinct => 1
|
||||
}
|
||||
)->_resultset->as_query;
|
||||
|
|
@ -600,8 +599,7 @@ sub batch_update {
|
|||
|
||||
if (@modified_itemnumbers) {
|
||||
my @biblionumbers =
|
||||
uniq(
|
||||
Koha::Items->search( { itemnumber => { -in => \@modified_itemnumbers } } )->get_column('biblionumber') );
|
||||
uniq( Koha::Items->search( { itemnumber => \@modified_itemnumbers } )->get_column('biblionumber') );
|
||||
|
||||
if (@biblionumbers) {
|
||||
my $indexer = Koha::SearchEngine::Indexer->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
|
||||
|
|
@ -635,24 +633,22 @@ sub apply_regex {
|
|||
my $modifiers = $params->{modifiers} || q{};
|
||||
my $value = $params->{value};
|
||||
|
||||
$replace =~ s/"/\\"/g; # Protection from embedded code
|
||||
$replace = '"' . $replace . '"'; # Put in a string for /ee
|
||||
my @available_modifiers = qw( i g );
|
||||
my $retained_modifiers = q||;
|
||||
for my $modifier ( split //, $modifiers ) {
|
||||
$retained_modifiers .= $modifier
|
||||
if grep { /$modifier/ } @available_modifiers;
|
||||
}
|
||||
|
||||
# The replacement is expanded as DATA (capture-group backreferences and the
|
||||
# \n \r \t escapes) and never evaluated as code, so @{...}/${...}
|
||||
# interpolation blocks are inert text, not Perl.
|
||||
if ( $retained_modifiers =~ m/^(ig|gi)$/ ) {
|
||||
$value =~ s/$search/Koha::Regex::Replacement::expand_template( $replace, [@{^CAPTURE}], {%+} )/ige;
|
||||
$value =~ s/$search/$replace/igee;
|
||||
} elsif ( $retained_modifiers eq 'i' ) {
|
||||
$value =~ s/$search/Koha::Regex::Replacement::expand_template( $replace, [@{^CAPTURE}], {%+} )/ie;
|
||||
$value =~ s/$search/$replace/iee;
|
||||
} elsif ( $retained_modifiers eq 'g' ) {
|
||||
$value =~ s/$search/Koha::Regex::Replacement::expand_template( $replace, [@{^CAPTURE}], {%+} )/ge;
|
||||
$value =~ s/$search/$replace/gee;
|
||||
} else {
|
||||
$value =~ s/$search/Koha::Regex::Replacement::expand_template( $replace, [@{^CAPTURE}], {%+} )/e;
|
||||
$value =~ s/$search/$replace/ee;
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
|
@ -666,76 +662,9 @@ Filters items based on the specified status.
|
|||
|
||||
=cut
|
||||
|
||||
sub _extract_custom_attrs {
|
||||
my ($params) = @_;
|
||||
my %custom_attrs;
|
||||
my $clean_params = _process_params( $params, \%custom_attrs );
|
||||
return ( \%custom_attrs, $clean_params );
|
||||
}
|
||||
|
||||
sub _process_params {
|
||||
my ( $params, $custom_attrs ) = @_;
|
||||
|
||||
return $params unless ref $params;
|
||||
|
||||
if ( ref $params eq 'HASH' ) {
|
||||
my %clean;
|
||||
for my $key ( keys %$params ) {
|
||||
if ( $key =~ /^_(.+)/ ) {
|
||||
$custom_attrs->{$key} = $params->{$key};
|
||||
} else {
|
||||
$clean{$key} = _process_params( $params->{$key}, $custom_attrs );
|
||||
}
|
||||
}
|
||||
return \%clean;
|
||||
} elsif ( ref $params eq 'ARRAY' ) {
|
||||
return [ map { _process_params( $_, $custom_attrs ) } @$params ];
|
||||
} else {
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
|
||||
=head3 _validate_params
|
||||
|
||||
Verify is the input provided with _status is valid.
|
||||
|
||||
Since we have to tweak the search for researches involving the _status parameter, if the search involves a _status, we need to check that the provided hashref is compatible with the use case.
|
||||
An hashref is compatible if it is not nested or if the lonely level of nesting is under a "-and" parameter
|
||||
|
||||
=cut
|
||||
|
||||
sub _validate_params {
|
||||
my ( $clean_params, $custom_attrs ) = @_;
|
||||
|
||||
return 1 unless exists $custom_attrs->{_status};
|
||||
|
||||
if ( ref($clean_params) eq 'HASH' ) {
|
||||
if ( grep { $_ eq '-and' } keys %$clean_params ) {
|
||||
my $and_hashref = $clean_params->{'-and'};
|
||||
if ( ref($and_hashref) eq 'HASH' && !grep { ref( $and_hashref->{$_} ) eq 'HASH' } keys %$and_hashref ) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 1 unless grep { ref( $clean_params->{$_} ) eq 'HASH' } keys %$clean_params;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub search {
|
||||
my ( $self, $params, $attributes ) = @_;
|
||||
|
||||
my ( $custom_attrs, $clean_params ) = _extract_custom_attrs($params);
|
||||
|
||||
unless ( _validate_params( $clean_params, $custom_attrs ) ) {
|
||||
Koha::Exceptions::BadParameter->throw(
|
||||
error => "Filtering on status is only implemented for -and search",
|
||||
);
|
||||
}
|
||||
|
||||
$params = $clean_params;
|
||||
my $status = $custom_attrs->{_status};
|
||||
|
||||
my $status = ( $params && ref($params) eq 'HASH' ) ? delete $params->{_status} : undef;
|
||||
if ($status) {
|
||||
if ( $status eq 'checked_out' ) {
|
||||
$self = $self->filter_by_checked_out( { onsite_checkout => 0 } );
|
||||
|
|
@ -749,32 +678,17 @@ sub search {
|
|||
if ( $status eq 'lost' ) {
|
||||
$self = $self->search( { itemlost => { '!=' => 0 } } );
|
||||
}
|
||||
if ( $status =~ '^lost-(.*)' ) {
|
||||
$self = $self->search( { itemlost => $1 } );
|
||||
}
|
||||
if ( $status eq 'withdrawn' ) {
|
||||
$self = $self->search( { withdrawn => { '!=' => 0 } } );
|
||||
}
|
||||
if ( $status =~ '^withdrawn-(.*)' ) {
|
||||
$self = $self->search( { withdrawn => $1 } );
|
||||
}
|
||||
if ( $status eq 'damaged' ) {
|
||||
$self = $self->search( { damaged => { '!=' => 0 } } );
|
||||
}
|
||||
if ( $status =~ '^damaged-(.*)' ) {
|
||||
$self = $self->search( { damaged => $1 } );
|
||||
}
|
||||
if ( $status eq 'not_for_loan' ) {
|
||||
my @item_types_notforloan =
|
||||
Koha::ItemTypes->search( { notforloan => { '!=' => 0 } } )->get_column('itemtype');
|
||||
$self = $self->search( [ { notforloan => { '!=' => 0 } }, { 'me.itype' => \@item_types_notforloan } ] );
|
||||
}
|
||||
if ( $status =~ '^not_for_loan-(.*)' ) {
|
||||
my $av_not_for_loan = $1;
|
||||
my @item_types_notforloan =
|
||||
Koha::ItemTypes->search( { notforloan => $av_not_for_loan } )->get_column('itemtype');
|
||||
$self = $self->search( [ { notforloan => $av_not_for_loan }, { 'me.itype' => \@item_types_notforloan } ] );
|
||||
}
|
||||
if ( $status eq 'on_hold' ) {
|
||||
$self = $self->filter_by_has_holds;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
package Koha::Library::Calendar::Exception;
|
||||
|
||||
# Copyright 2026 Theke Solutions
|
||||
#
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use Koha::Caches;
|
||||
use Koha::Database;
|
||||
use Koha::Exceptions;
|
||||
|
||||
use base qw(Koha::Object);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Library::Calendar::Exception - Koha closure exception (open override) Object class
|
||||
|
||||
=head1 API
|
||||
|
||||
=head2 Class methods
|
||||
|
||||
=head3 store
|
||||
|
||||
Overloaded store method that clears the holidays cache on insert.
|
||||
|
||||
Only flushes on insert (new exception), not on title/description updates,
|
||||
because the holidays cache only stores dates and their open/closed status.
|
||||
An update that changes C<date> in place would desync the cache with no
|
||||
warning, so that is rejected outright - callers must delete and re-add
|
||||
instead.
|
||||
|
||||
=cut
|
||||
|
||||
sub store {
|
||||
my ($self) = @_;
|
||||
|
||||
Koha::Exceptions::BadParameter->throw(
|
||||
error => 'Cannot change date on an existing closure exception; delete and re-add instead',
|
||||
parameter => 'date'
|
||||
) if $self->in_storage && $self->_result->is_column_changed('date');
|
||||
|
||||
my $flush = !$self->in_storage;
|
||||
|
||||
$self = $self->SUPER::store;
|
||||
|
||||
if ($flush) {
|
||||
Koha::Caches->get_instance()->clear_from_cache( $self->library_id . '_holidays' );
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
=head3 delete
|
||||
|
||||
Overloaded delete method that clears the holidays cache.
|
||||
|
||||
=cut
|
||||
|
||||
sub delete {
|
||||
my ($self) = @_;
|
||||
my $library_id = $self->library_id;
|
||||
$self->SUPER::delete;
|
||||
Koha::Caches->get_instance()->clear_from_cache( $library_id . '_holidays' );
|
||||
return $self;
|
||||
}
|
||||
|
||||
=head2 Internal methods
|
||||
|
||||
=head3 _type
|
||||
|
||||
=cut
|
||||
|
||||
sub _type {
|
||||
return 'LibraryClosureException';
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package Koha::Library::Calendar::RepeatingClosures;
|
||||
|
||||
# Copyright 2026 Theke Solutions
|
||||
#
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use Koha::Database;
|
||||
|
||||
use Koha::Library::Calendar::RepeatingClosure;
|
||||
|
||||
use base qw(Koha::Objects);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Library::Calendar::RepeatingClosures - Koha annual repeating closure Object set class
|
||||
|
||||
=head1 API
|
||||
|
||||
=head2 Class methods
|
||||
|
||||
=head3 _type
|
||||
|
||||
=cut
|
||||
|
||||
sub _type {
|
||||
return 'LibraryRepeatingClosure';
|
||||
}
|
||||
|
||||
=head3 object_class
|
||||
|
||||
=cut
|
||||
|
||||
sub object_class {
|
||||
return 'Koha::Library::Calendar::RepeatingClosure';
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
package Koha::Library::Calendar::SingleClosure;
|
||||
|
||||
# Copyright 2026 Theke Solutions
|
||||
#
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use Koha::Caches;
|
||||
use Koha::Database;
|
||||
use Koha::Exceptions;
|
||||
|
||||
use base qw(Koha::Object);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Library::Calendar::SingleClosure - Koha single-date closure Object class
|
||||
|
||||
=head1 API
|
||||
|
||||
=head2 Class methods
|
||||
|
||||
=head3 store
|
||||
|
||||
Overloaded store method that clears the holidays cache on insert.
|
||||
|
||||
Only flushes on insert (new closure), not on title/description updates,
|
||||
because the holidays cache only stores dates and their open/closed status.
|
||||
An update that changes C<date> in place would desync the cache with no
|
||||
warning, so that is rejected outright - callers must delete and re-add
|
||||
instead.
|
||||
|
||||
=cut
|
||||
|
||||
sub store {
|
||||
my ($self) = @_;
|
||||
|
||||
Koha::Exceptions::BadParameter->throw(
|
||||
error => 'Cannot change date on an existing single closure; delete and re-add instead',
|
||||
parameter => 'date'
|
||||
) if $self->in_storage && $self->_result->is_column_changed('date');
|
||||
|
||||
my $flush = !$self->in_storage;
|
||||
|
||||
$self = $self->SUPER::store;
|
||||
|
||||
if ($flush) {
|
||||
Koha::Caches->get_instance()->clear_from_cache( $self->library_id . '_holidays' );
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
=head3 delete
|
||||
|
||||
Overloaded delete method that clears the holidays cache.
|
||||
|
||||
=cut
|
||||
|
||||
sub delete {
|
||||
my ($self) = @_;
|
||||
my $library_id = $self->library_id;
|
||||
$self->SUPER::delete;
|
||||
Koha::Caches->get_instance()->clear_from_cache( $library_id . '_holidays' );
|
||||
return $self;
|
||||
}
|
||||
|
||||
=head2 Internal methods
|
||||
|
||||
=head3 _type
|
||||
|
||||
=cut
|
||||
|
||||
sub _type {
|
||||
return 'LibrarySingleClosure';
|
||||
}
|
||||
|
||||
1;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue