From 592a12061d76b72fd029012fdd12d83deae3a694 Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Wed, 13 Nov 2024 16:55:25 +0100 Subject: [PATCH] Bug 15248: Use record matching rules to find duplicate while cataloging This patch allows to configure how Koha searches for duplicates when cataloging manually by using a record matching rule. For each framework we can now set a record matching rule to use when searching for duplicates. This allow to use different matching rules while in acquisition for instance (ACQ framework). If a matching rule is selected, we also have the ability to forbid the creation of duplicate: if a duplicate is found and this option is enabled, the usual option to create a duplicate will be hidden. If no matching rule is selected, then the default matching mechanism is used. Test plan: 1. Create a record matching rule matching on ISBN Threshold: 100 Record type: biblio Match point: Search index: "isbn" Score: 100 Tag: "020" (or "010" for UNIMARC) Subfields: "a" Offset: 0 Length: 0 Normalization rule: Remove spaces No match checks 2. Edit the default MARC framework and select the new matching rule and enable "Forbid duplicate creation" 3. Create a biblio with an ISBN 4. Make sure your new biblio is indexed in Zebra or ES 5. Try to create a biblio with the same ISBN. You should see a warning about the duplicate with no option to create the duplicate biblio 6. In the acquisitions module, in a basket add a new order from an external source (requires a configured Z39.50/SRU server) 7. Choose a record with an ISBN 8. Once the order is created, try to create a new order from an external source and choose the same record. You should see a warning about the duplicate. 9. Edit the record matching rule and disable "Forbid duplicate creation" 10. Redo step 5. Now there should be an option to create the duplicate record. 11. Redo step 8. Now there should be an option to create the duplicate record. 12. Run the following test scripts: - t/db_dependent/Search.t - t/db_dependent/Matcher.t Sponsored-by: SCD Lyon2 Signed-off-by: Andrew Fuerste Henry Signed-off-by: Phillip Berg --- C4/Matcher.pm | 40 +++--- C4/Search.pm | 43 ++++++ Koha/BiblioFramework.pm | 17 +++ Koha/BiblioFrameworkMarcMatcher.pm | 43 ++++++ Koha/BiblioFrameworkMarcMatchers.pm | 49 +++++++ Koha/REST/V1/Biblios.pm | 11 +- acqui/neworderempty.pl | 55 ++++---- admin/biblio_framework.pl | 77 +++++++---- cataloguing/addbiblio.pl | 12 +- .../data/mysql/atomicupdate/bug-15248.pl | 29 ++++ installer/data/mysql/kohastructure.sql | 17 +++ .../modules/acqui/neworderempty_duplicate.tt | 30 +++-- .../prog/en/modules/admin/biblio_framework.tt | 38 +++++- .../prog/en/modules/admin/matching-rules.tt | 2 + .../prog/en/modules/cataloguing/addbiblio.tt | 126 +++++++++++------- t/db_dependent/Matcher.t | 6 +- t/db_dependent/Search.t | 75 ++++++++++- 17 files changed, 521 insertions(+), 149 deletions(-) create mode 100644 Koha/BiblioFrameworkMarcMatcher.pm create mode 100644 Koha/BiblioFrameworkMarcMatchers.pm create mode 100644 installer/data/mysql/atomicupdate/bug-15248.pl diff --git a/C4/Matcher.pm b/C4/Matcher.pm index 0aaaf818d9b..41d2c6512a9 100644 --- a/C4/Matcher.pm +++ b/C4/Matcher.pm @@ -19,6 +19,7 @@ package C4::Matcher; use Modern::Perl; +use Koha::Database; use Koha::SearchEngine; use Koha::SearchEngine::Search; use Koha::SearchEngine::QueryBuilder; @@ -70,7 +71,7 @@ C4::Matcher - find MARC records matching another one =head2 GetMatcherList - my @matchers = C4::Matcher::GetMatcherList(); + my @matchers = C4::Matcher::GetMatcherList($filters); Returns an array of hashrefs list all matchers present in the database. Each hashref includes: @@ -78,19 +79,32 @@ present in the database. Each hashref includes: * matcher_id * code * description + * record_type + +C<$filters> is an optional hashref parameter that allows to filter the result. Useful keys are: + +=over + +=item * C + +=back + +See L for more info + +=head3 Examples + + @matchers = C4::Matcher::GetMatcherList(); + @matchers = C4::Matcher::GetMatcherList({ record_type => 'biblio' }); + @matchers = C4::Matcher::GetMatcherList({ record_type => 'authority' }); =cut sub GetMatcherList { - my $dbh = C4::Context->dbh; + my ($filters) = @_; - my $sth = $dbh->prepare_cached("SELECT matcher_id, code, description FROM marc_matchers ORDER BY matcher_id"); - $sth->execute(); - my @results = (); - while ( my $row = $sth->fetchrow_hashref ) { - push @results, $row; - } - return @results; + my $rs = Koha::Database->schema->resultset('MarcMatcher'); + + return $rs->search($filters // {}, { result_class => 'DBIx::Class::ResultClass::HashRefInflator' })->all; } =head2 GetMatcherId @@ -171,12 +185,8 @@ sub fetch { $sth->finish(); return unless defined $row; - my $self = {}; - $self->{'id'} = $row->{'matcher_id'}; - $self->{'record_type'} = $row->{'record_type'}; - $self->{'code'} = $row->{'code'}; - $self->{'description'} = $row->{'description'}; - $self->{'threshold'} = int( $row->{'threshold'} ); + my $self = { %$row }; + $self->{id} = delete $self->{matcher_id}; bless $self, $class; # matchpoints diff --git a/C4/Search.pm b/C4/Search.pm index 12ba64f60fe..4cb9e121daa 100644 --- a/C4/Search.pm +++ b/C4/Search.pm @@ -28,7 +28,9 @@ use XML::Simple; use C4::XSLT qw( XSLTParse4Display ); use C4::Reserves qw( GetReserveStatus ); use C4::Charset qw( SetUTF8Flag ); +use C4::Matcher; use Koha::AuthorisedValues; +use Koha::BiblioFrameworkMarcMatchers; use Koha::ItemTypes; use Koha::Libraries; use Koha::Logger; @@ -146,6 +148,47 @@ sub FindDuplicate { return @results; } +sub FindDuplicateWithMatchingRules { + my ($record, $frameworkcode) = @_; + + $frameworkcode //= ''; + + my $biblio_framework_marc_matcher = Koha::BiblioFrameworkMarcMatchers->find($frameworkcode); + if ($biblio_framework_marc_matcher) { + my $matcher = C4::Matcher->fetch($biblio_framework_marc_matcher->marc_matcher_id); + if ($matcher) { + my @duplicates; + my $max_matches = 1; + my @matches = $matcher->get_matches($record, $max_matches); + foreach my $match (@matches) { + my $biblio = Koha::Biblios->find($match->{record_id}); + if ($biblio) { + push @duplicates, { + biblionumber => $biblio->biblionumber, + title => $biblio->title, + forbid_duplicate_creation => $biblio_framework_marc_matcher->forbid_duplicate_creation, + }; + } + } + + return @duplicates; + } + } + + # If no matcher can be used, default to FindDuplicate + my @results = FindDuplicate($record); + my @duplicates; + while ((my $biblionumber = shift @results) && (my $title = shift @results)) { + push @duplicates, { + biblionumber => $biblionumber, + title => $title, + forbid_duplicate_creation => 0, + } + } + + return @duplicates; +} + =head2 SimpleSearch ( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers], [%options] ); diff --git a/Koha/BiblioFramework.pm b/Koha/BiblioFramework.pm index 02d08c98325..bc661234cb3 100644 --- a/Koha/BiblioFramework.pm +++ b/Koha/BiblioFramework.pm @@ -31,6 +31,23 @@ Koha::BiblioFramework - Koha BiblioFramework Object class =cut +=head3 delete + +See L + +=cut + +sub delete { + my ($self) = @_; + + my $biblio_framework_marc_matcher = Koha::BiblioFrameworkMarcMatchers->find($self->frameworkcode); + if ($biblio_framework_marc_matcher) { + $biblio_framework_marc_matcher->delete(); + } + + $self->SUPER::delete(); +} + =head3 type =cut diff --git a/Koha/BiblioFrameworkMarcMatcher.pm b/Koha/BiblioFrameworkMarcMatcher.pm new file mode 100644 index 00000000000..c02c09a61f9 --- /dev/null +++ b/Koha/BiblioFrameworkMarcMatcher.pm @@ -0,0 +1,43 @@ +package Koha::BiblioFrameworkMarcMatcher; + +# 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 . + +use Modern::Perl; + + +use Koha::Database; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::BiblioFrameworkMarcMatcher + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub _type { + return 'BiblioFrameworkMarcMatcher'; +} + +1; diff --git a/Koha/BiblioFrameworkMarcMatchers.pm b/Koha/BiblioFrameworkMarcMatchers.pm new file mode 100644 index 00000000000..32bef344b27 --- /dev/null +++ b/Koha/BiblioFrameworkMarcMatchers.pm @@ -0,0 +1,49 @@ +package Koha::BiblioFrameworkMarcMatchers; + +# 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 . + +use Modern::Perl; + + +use Koha::Database; + +use Koha::BiblioFrameworkMarcMatcher; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::BiblioFrameworkMarcMatchers + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub _type { + return 'BiblioFrameworkMarcMatcher'; +} + +sub object_class { + return 'Koha::BiblioFrameworkMarcMatcher'; +} + +1; diff --git a/Koha/REST/V1/Biblios.pm b/Koha/REST/V1/Biblios.pm index 94bca50e83b..3bbb6387a55 100644 --- a/Koha/REST/V1/Biblios.pm +++ b/Koha/REST/V1/Biblios.pm @@ -23,7 +23,7 @@ use Koha::Biblios; use Koha::DateUtils; use Koha::Ratings; use C4::Biblio qw( DelBiblio AddBiblio ModBiblio ); -use C4::Search qw( FindDuplicate ); +use C4::Search; use C4::Auth qw( haspermission ); use C4::Barcodes::ValueBuilder; @@ -667,15 +667,14 @@ sub add { my $confirm_not_duplicate = $headers->header('x-confirm-not-duplicate'); - if ( !$confirm_not_duplicate ) { - my ( $duplicatebiblionumber, $duplicatetitle ) = FindDuplicate($record); - + my ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($record, $frameworkcode); + if ( $duplicate && ( $duplicate->{forbid_duplicate_creation} || !$confirm_not_duplicate ) ) { return $c->render( status => 400, openapi => { - error => "Duplicate biblio $duplicatebiblionumber", + error => "Duplicate biblio $duplicate->{biblionumber}", } - ) if $duplicatebiblionumber; + ); } my ($biblio_id) = C4::Biblio::AddBiblio( $record, $frameworkcode, { record_source_id => $record_source_id } ); diff --git a/acqui/neworderempty.pl b/acqui/neworderempty.pl index c4db7f4044b..25b0ad6edba 100755 --- a/acqui/neworderempty.pl +++ b/acqui/neworderempty.pl @@ -83,7 +83,7 @@ use C4::Biblio qw( ); use C4::Output qw( output_and_exit output_html_with_http_headers ); use C4::Members; -use C4::Search qw( FindDuplicate ); +use C4::Search; #needed for z3950 import: use C4::ImportBatch qw( SetImportRecordStatus SetMatchedBiblionumber GetImportRecordMarc ); @@ -172,15 +172,31 @@ if ( $ordernumber eq '' and defined $breedingid ) { $marcrecord->delete_field($item); } - my $duplicatetitle; - - #look for duplicates - ( $biblionumber, $duplicatetitle ) = FindDuplicate($marcrecord); - if ( $biblionumber && $op ne 'cud-use_external_source' ) { - + # look for duplicates + my ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($marcrecord, $frameworkcode); + if ( $duplicate && $op ne 'cud-use_external_source' ) { #if duplicate record found and user did not decide yet, first warn user #and let them choose between using a new record or an existing record - Load_Duplicate($duplicatetitle); + #(if the matching rule does not forbid it) + my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "acqui/neworderempty_duplicate.tt", + query => $input, + type => "intranet", + flagsrequired => { acquisition => 'order_manage' }, + } + ); + + my $marcflavour = uc C4::Context->preference("marcflavour"); + $template->param( + basketno => $basketno, + booksellerid => $basket->{'booksellerid'}, + breedingid => $breedingid, + duplicate => $duplicate, + $marcflavour => 1 + ); + + output_html_with_http_headers $input, $cookie, $template->output; exit; } @@ -608,26 +624,3 @@ sub MARCfindbreeding { } return -1; } - -sub Load_Duplicate { - my ($duplicatetitle) = @_; - ( $template, $loggedinuser, $cookie ) = get_template_and_user( - { - template_name => "acqui/neworderempty_duplicate.tt", - query => $input, - type => "intranet", - flagsrequired => { acquisition => 'order_manage' }, - } - ); - - $template->param( - biblionumber => $biblionumber, - basketno => $basketno, - booksellerid => $basket->{'booksellerid'}, - breedingid => $breedingid, - duplicatetitle => $duplicatetitle, - ( uc( C4::Context->preference("marcflavour") ) ) => 1 - ); - - output_html_with_http_headers $input, $cookie, $template->output; -} diff --git a/admin/biblio_framework.pl b/admin/biblio_framework.pl index ecad5c2bb2b..eb7709c8b37 100755 --- a/admin/biblio_framework.pl +++ b/admin/biblio_framework.pl @@ -23,13 +23,17 @@ use CGI qw ( -utf8 ); use C4::Context; use C4::Auth qw( get_template_and_user ); use C4::Output qw( output_html_with_http_headers ); +use C4::Matcher; use Koha::Biblios; use Koha::BiblioFramework; use Koha::BiblioFrameworks; +use Koha::BiblioFrameworkMarcMatcher; +use Koha::BiblioFrameworkMarcMatchers; use Koha::Caches; +use Koha::I18N; my $input = CGI->new; -my $frameworkcode = $input->param('frameworkcode') || q||; +my $frameworkcode = $input->param('frameworkcode'); my $op = $input->param('op') || q|list|; my $cache = Koha::Caches->get_instance(); my @messages; @@ -46,38 +50,59 @@ my ( $template, $borrowernumber, $cookie ) = get_template_and_user( my $dbh = C4::Context->dbh; if ( $op eq 'add_form' ) { my $framework; - if ($frameworkcode) { - $framework = Koha::BiblioFrameworks->find($frameworkcode); + my $biblio_framework_marc_matcher; + if (defined $frameworkcode) { + if ($frameworkcode ne '') { + $framework = Koha::BiblioFrameworks->find($frameworkcode); + } else { + $framework = Koha::BiblioFramework->new({ frameworkcode => '', frameworktext => __('Default framework') }) + } + + $biblio_framework_marc_matcher = Koha::BiblioFrameworkMarcMatchers->find($frameworkcode); } $template->param( framework => $framework ); + $template->param( biblio_framework_marc_matcher => $biblio_framework_marc_matcher ); + + my @matchers = C4::Matcher::GetMatcherList(); + $template->param('marc_matchers' => \@matchers); } elsif ( $op eq 'cud-add_validate' ) { - my $frameworkcode = $input->param('frameworkcode'); - my $frameworktext = $input->param('frameworktext'); - my $is_a_modif = $input->param('is_a_modif'); - - if ($is_a_modif) { - my $framework = Koha::BiblioFrameworks->find($frameworkcode); - $framework->frameworktext($frameworktext); - eval { $framework->store; }; - if ($@) { - push @messages, { type => 'error', code => 'error_on_update' }; - } else { - push @messages, { type => 'message', code => 'success_on_update' }; - } + my $frameworkcode = $input->param('frameworkcode'); + my $frameworktext = $input->param('frameworktext'); + my $marc_matcher_id = $input->param('marc_matcher_id'); + my $forbid_duplicate_creation = $input->param('forbid_duplicate_creation') // 0; + my $is_a_modif = $input->param('is_a_modif'); + + my $framework; + if ( $is_a_modif && $frameworkcode ne '' ) { + $framework = Koha::BiblioFrameworks->find($frameworkcode); } else { - my $framework = Koha::BiblioFramework->new( - { - frameworkcode => $frameworkcode, - frameworktext => $frameworktext, + $framework = Koha::BiblioFramework->new( { frameworkcode => $frameworkcode } ); + } + + $framework->frameworktext($frameworktext); + eval { + $framework->store() unless $framework->frameworkcode eq ''; + + my $biblio_framework_marc_matcher = Koha::BiblioFrameworkMarcMatchers->find($frameworkcode); + if ($marc_matcher_id) { + unless ($biblio_framework_marc_matcher) { + $biblio_framework_marc_matcher = + Koha::BiblioFrameworkMarcMatcher->new( { frameworkcode => $frameworkcode } ); } - ); - eval { $framework->store; }; - if ($@) { - push @messages, { type => 'error', code => 'error_on_insert' }; - } else { - push @messages, { type => 'message', code => 'success_on_insert' }; + $biblio_framework_marc_matcher->marc_matcher_id($marc_matcher_id); + $biblio_framework_marc_matcher->forbid_duplicate_creation($forbid_duplicate_creation); + $biblio_framework_marc_matcher->store(); + } elsif ($biblio_framework_marc_matcher) { + $biblio_framework_marc_matcher->delete(); } + }; + + if ($@) { + push @messages, { type => 'error', code => $is_a_modif ? 'error_on_update' : 'error_on_insert' }; + } else { + push @messages, { type => 'message', code => $is_a_modif ? 'success_on_update' : 'success_on_insert' }; } + $cache->clear_from_cache("MarcStructure-0-$frameworkcode"); $cache->clear_from_cache("MarcStructure-1-$frameworkcode"); $cache->clear_from_cache("MarcSubfieldStructure-$frameworkcode"); diff --git a/cataloguing/addbiblio.pl b/cataloguing/addbiblio.pl index 46f28d19dfb..453c21b484b 100755 --- a/cataloguing/addbiblio.pl +++ b/cataloguing/addbiblio.pl @@ -37,7 +37,7 @@ use C4::Biblio qw( TransformHtmlToMarc ApplyMarcOverlayRules ); -use C4::Search qw( FindDuplicate enabled_staff_search_views ); +use C4::Search qw( enabled_staff_search_views ); use C4::Auth qw( get_template_and_user haspermission ); use C4::Context; use MARC::Record; @@ -694,14 +694,14 @@ if ( $op eq "cud-addbiblio" ) { $record = TransformHtmlToMarc( $input, 1 ); # check for a duplicate - my ( $duplicatebiblionumber, $duplicatetitle ); + my $duplicate; if ( !$is_a_modif ) { - ( $duplicatebiblionumber, $duplicatetitle ) = FindDuplicate($record); + ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($record, $frameworkcode); } my $confirm_not_duplicate = $input->param('confirm_not_duplicate'); # it is not a duplicate (determined either by Koha itself or by user checking it's not a duplicate) - if ( !$duplicatebiblionumber or $confirm_not_duplicate ) { + if ( !$duplicate or ( !$duplicate->{forbid_duplicate_creation} and $confirm_not_duplicate ) ) { my $oldbibitemnum; if ($is_a_modif) { ModBiblio( @@ -796,9 +796,7 @@ if ( $op eq "cud-addbiblio" ) { $template->param( biblionumber => $biblionumber, biblioitemnumber => $biblioitemnumber, - duplicatebiblionumber => $duplicatebiblionumber, - duplicatebibid => $duplicatebiblionumber, - duplicatetitle => $duplicatetitle, + duplicate => $duplicate, ); } diff --git a/installer/data/mysql/atomicupdate/bug-15248.pl b/installer/data/mysql/atomicupdate/bug-15248.pl new file mode 100644 index 00000000000..3daa0e394e4 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug-15248.pl @@ -0,0 +1,29 @@ +use Modern::Perl; + +return { + bug_number => "15248", + description => "Add biblio_framework_marc_matcher table", + up => sub { + my ($args) = @_; + my ( $dbh, $out ) = @$args{qw(dbh out)}; + + unless (TableExists('biblio_framework_marc_matcher')) { + $dbh->do( + q{ + CREATE TABLE biblio_framework_marc_matcher ( + frameworkcode VARCHAR(4) NOT NULL COMMENT 'MARC framework code', + marc_matcher_id INT(11) NOT NULL COMMENT 'MARC matcher id', + forbid_duplicate_creation TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Do not show an option to create a duplicate record when a duplicate is found', + PRIMARY KEY (frameworkcode), + CONSTRAINT biblio_framework_marc_matcher_ibfk_1 + FOREIGN KEY (marc_matcher_id) REFERENCES marc_matchers (matcher_id) + ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + } + ); + say $out "Created new table biblio_framework_marc_matcher"; + } else { + say $out "Table biblio_framework_marc_matcher already exists"; + } + }, +}; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index e391dc68f5d..bd6674bb3f2 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -1131,6 +1131,23 @@ CREATE TABLE `biblio_framework` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `biblio_framework_marc_matcher` +-- + +DROP TABLE IF EXISTS `biblio_framework_marc_matcher`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `biblio_framework_marc_matcher` ( + `frameworkcode` varchar(4) NOT NULL COMMENT 'MARC framework code', + `marc_matcher_id` int(11) NOT NULL COMMENT 'MARC matcher id', + `forbid_duplicate_creation` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Do not show an option to create a duplicate record when a duplicate is found', + PRIMARY KEY (`frameworkcode`), + KEY `biblio_framework_marc_matcher_ibfk_1` (`marc_matcher_id`), + CONSTRAINT `biblio_framework_marc_matcher_ibfk_1` FOREIGN KEY (`marc_matcher_id`) REFERENCES `marc_matchers` (`matcher_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `biblio_metadata` -- diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty_duplicate.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty_duplicate.tt index 78cb23489db..a3af870a765 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty_duplicate.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty_duplicate.tt @@ -35,7 +35,7 @@

Duplicate warning

You selected a record from an external source that matches an existing record in your catalog: - [% duplicatetitle | html %]

[% duplicate.title | html %]

@@ -64,20 +64,22 @@ -
-
-
- [% INCLUDE 'csrf-token.inc' %] -

Create new record

-

Create a new record by importing the external (duplicate) record.

- - - - - -
+ [% UNLESS duplicate.forbid_duplicate_creation %] +
+
+
+ [% INCLUDE 'csrf-token.inc' %] +

Create new record

+

Create a new record by importing the external (duplicate) record.

+ + + + + +
+
-
+ [% END %]
[% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/biblio_framework.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/biblio_framework.tt index b4a85fe0217..b8f16e3dd82 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/biblio_framework.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/biblio_framework.tt @@ -131,10 +131,39 @@ [% END %]
  • - - - Required + [% IF framework && frameworkcode == '' %] + + [% framework.frameworktext | html %] + [% ELSE %] + + + Required + [% END %]
  • + + [% IF marc_matchers.size > 0 %] +
  • + + +
  • + [% END %] + +
  • + + +
    + [% t('If the selected matching rule finds a duplicate and this option is enabled, it will not be possible to create a duplicate. This has no effect if no matching rule is selected.') | html %] +
    +
  • @@ -191,6 +220,9 @@
  • MARC structure
  • +
  • Edit
  • # Code Description + Record type Actions [% FOREACH available_matching_rule IN available_matching_rules %] @@ -597,6 +598,7 @@ [% available_matching_rule.matcher_id | html %] [% available_matching_rule.code | html %] [% available_matching_rule.description | html %] + [% available_matching_rule.record_type | html %] Edit - [% IF ( duplicatebiblionumber ) %] + [% IF ( duplicate ) %]
    -

    Duplicate record suspected

    -

    Is this a duplicate of - [% duplicatetitle | html %] - ?

    - [% IF ( CAN_user_editcatalogue_edit_items ) %] -
    - [% IF ( circborrowernumber ) # It is possible we have come from fast cataloging - include the fields %] - - - - - - [% END %] - - -
    - [% ELSIF ( circborrowernumber ) # Coming from fast cataloging %] -
    - - -
    + [% IF duplicate.forbid_duplicate_creation %] +

    Duplicate record detected

    +

    This is a duplicate of + [% duplicate.title | html %] +

    + [% IF ( CAN_user_editcatalogue_edit_items ) %] +
    + [% IF ( circborrowernumber ) # It is possible we have come from fast cataloging - include the fields %] + + + + + + [% END %] + + +
    + [% ELSIF ( circborrowernumber ) # Coming from fast cataloging %] +
    + + +
    + [% ELSE %] +
    + + +
    + [% END %] [% ELSE %] -
    - - -
    - [% END %] -
    - [% IF ( CAN_user_editcatalogue_edit_items || CAN_user_editcatalogue_fast_cataloging ) %] - [% IF ( circborrowernumber ) # It is possible we have come from fast cataloging - include the fields %] - - - - - - [% END %] - +

    Duplicate record suspected

    +

    Is this a duplicate of + [% duplicate.title | html %] + ?

    + [% IF ( CAN_user_editcatalogue_edit_items ) %] + + [% IF ( circborrowernumber ) # It is possible we have come from fast cataloging - include the fields %] + + + + + + [% END %] + + +
    + [% ELSIF ( circborrowernumber ) # Coming from fast cataloging %] +
    + + +
    [% ELSE %] - +
    + + +
    [% END %] - +
    + [% IF ( CAN_user_editcatalogue_edit_items || CAN_user_editcatalogue_fast_cataloging ) %] + [% IF ( circborrowernumber ) # It is possible we have come from fast cataloging - include the fields %] + + + + + + [% END %] + + [% ELSE %] + + [% END %] +
    + [% END %]
    [% END # /IF duplicatebiblionumber %] diff --git a/t/db_dependent/Matcher.t b/t/db_dependent/Matcher.t index 00840843d13..c9587668891 100755 --- a/t/db_dependent/Matcher.t +++ b/t/db_dependent/Matcher.t @@ -32,7 +32,7 @@ my $builder = t::lib::TestBuilder->new; $schema->storage->txn_begin; subtest 'GetMatcherList' => sub { - plan tests => 9; + plan tests => 11; $schema->resultset('MarcMatcher')->delete_all; my $matcher1 = $builder->build( @@ -70,6 +70,10 @@ subtest 'GetMatcherList' => sub { $testmatcher->description('match on ISSN'); is( $testmatcher->description(), 'match on ISSN', 'testing code accessor' ); + + @matchers = C4::Matcher::GetMatcherList({ record_type => 'blue' }); + is(scalar @matchers, 1, 'Filtering works'); + is($matchers[0]->{record_type}, 'blue', 'Filtering on record type works'); }; subtest '_get_match_keys() tests' => sub { diff --git a/t/db_dependent/Search.t b/t/db_dependent/Search.t index 8ebcd4e2b2a..6d64beeef34 100755 --- a/t/db_dependent/Search.t +++ b/t/db_dependent/Search.t @@ -20,6 +20,7 @@ use Modern::Perl; use utf8; use C4::AuthoritiesMarc qw( SearchAuthorities ); +use C4::Matcher; use C4::XSLT; require C4::Context; @@ -27,13 +28,14 @@ require C4::Context; use open ':std', ':encoding(utf8)'; use Test::NoWarnings; -use Test::More tests => 5; +use Test::More tests => 6; use Test::MockModule; use Test::Warn; use t::lib::Mocks; use t::lib::Mocks::Zebra; use Koha::Caches; +use Koha::Database; use MARC::Record; use File::Spec; @@ -42,6 +44,7 @@ use File::Find; use File::Temp qw/ tempdir /; use File::Path; +use DateTime; # Fall back to make sure that the Zebra process # and files get cleaned up @@ -1369,6 +1372,76 @@ subtest 'FindDuplicate' => sub { }; +subtest 'FindDuplicateWithMatchingRules' => sub { + plan tests => 3; + + my $marcflavour = 'MARC21'; + my $mock_zebra = t::lib::Mocks::Zebra->new({marcflavour => $marcflavour}); + push @cleanup, $mock_zebra; + + mock_GetMarcSubfieldStructure($marcflavour); + + my $sourcedir = dirname(__FILE__) . "/data"; + $mock_zebra->load_records( + sprintf( "%s/%s/zebraexport/biblio", $sourcedir, lc($marcflavour) ), + 'iso2709', 'biblios', 1 ); + $mock_zebra->launch_zebra; + t::lib::Mocks::mock_preference('SearchEngine', 'Zebra' ); + + my $schema = Koha::Database->schema; + + # Unlike with FindDuplicate, we need the biblio to exist + my $biblio = Koha::Biblios->find(51); + unless ($biblio) { + $schema->resultset('Biblio')->create( + { + biblionumber => 51, + frameworkcode => '', + title => 'Administração da produção /', + datecreated => DateTime->now->ymd, + } + ); + } + + $schema->txn_begin; + + $schema->resultset('MarcMatcher')->delete; + + my $record = MARC::Record->new; + $record->add_fields( + [ '020', ' ', ' ', a => '9788522421718' ], + [ '245', '0', '0', a => 'Administração da produção /' ] + ); + my ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($record); + is($duplicate && $duplicate->{biblionumber}, 51, 'Without any matcher, same result than FindDuplicate'); + + # Make sure the matcher is used by configuring it badly first, it should return no duplicate + my $matcher = C4::Matcher->new('biblio', 1000); + $matcher->add_matchpoint('issn', 1000, [ { tag => '020', subfields => 'a', offset => 0 } ]); + $matcher->store(); + + my $biblio_framework_marc_matcher = Koha::BiblioFrameworkMarcMatcher->new( + { + frameworkcode => '', + marc_matcher_id => $matcher->{id}, + } + ); + $biblio_framework_marc_matcher->store(); + + ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($record); + ok(!defined $duplicate, 'Matcher is used and returns no duplicates'); + + # Now fix the matcher and make sure it returns a duplicate + $matcher->{matchpoints} = []; + $matcher->add_matchpoint('isbn', 1000, [ { tag => '020', subfields => 'a', offset => 0 } ]); + $matcher->store(); + + ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($record); + is($duplicate && $duplicate->{biblionumber}, 51, 'Matcher is used and returns a duplicate'); + + $schema->txn_rollback; +}; + # Make sure that following tests are not using our config settings Koha::Caches->get_instance('config')->flush_all; -- 2.39.5