Compare commits

...

1172 Commits

Author SHA1 Message Date
Alex Carver [Acerock7] 103f60a4ff
Bug 41253: Add new permission to superlibrarian test
Adds items_modification_by_age to the superlibrarian subtest in t/Koha/Auth/Permissions.t

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-10-01 15:04:44 +00:00
Tomás Cohen Arazi d1f8d5bbd2
Bug 42719: (QA follow-up) Adjust tests
This patch makes the tests reflect the design decision made on the bug report.
The author for the original 'No CGISESSID then error' patch signed off on this
so I'm confident this is the right thing to do.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-10-01 13:32:51 +00:00
Lucas Gass 9f50aa9862
Bug 42667: (follow-up) Fix POD coverage
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-10-01 13:32:50 +00:00
Jonathan Druart 541cbd84a5
Bug 42685: Fix Cypress test
AssertionError: Timed out retrying after 10000ms: Expected to find content: 'Requesting Agencies' within the element: <div.sidebar_menu> but never did.
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-30 09:14:54 +00:00
Jonathan Druart 36606ea9d6
Bug 41674: Fix review issues - remove unused import, fix POD synopsis, remove unused variable
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-30 09:14:52 +00:00
Jonathan Druart 8526f2ac3b
Bug 41674: Use LinkPref for CurbsidePickup
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-30 09:14:51 +00:00
Jonathan Druart a746db644c
Bug 41674: Add a test
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-30 09:14:50 +00:00
Owen Leonard e7aee90141
Bug 41674: (follow-up) Proof of concept: branches.pl
This patch implements the new template plugin on Administration ->
Libraries.

- When viewed as a user with permission to manage system preferences,
  the system preference names under "Reply-To", "Return-Path", and "MARC
  organization code" should be linked to system preferences, and the
  link should take you to the correct preference search.
- When viewed as a user without permission the system preference names
  should not be linked.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-30 09:14:49 +00:00
Owen Leonard 797c1b352e
Bug 41674: Add template plugin for linking to system preferences based on user permission
This patch adds a new template plugin, LinkPref, for displaying system
preference names as links depending on the logged-in user's permission.

Syntax:

  [% "SYSTEM_PREFERENCE_NAME" | html | $LinkPref %]

For a user with 'CAN_user_parameters_manage_sysprefs' permission it
outputs:

  <a class="link_preference" href="admin/preferences.pl?op=search&amp;ok=Search&amp;searchfield=SYSTEM_PREFERENCE_NAME">SYSTEM_PREFERENCE_NAME</a>

For a user without permission it outputs:

  <span class="link_preference">SYSTEM_PREFERENCE_NAME</span>

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-30 09:14:45 +00:00
Martin Renvoize 106fabbf9a
Bug 43669: Make t::lib::Selenium wait for the server to be ready
This patch adds a wait_for_server_ready check to t::lib::Selenium, so
Selenium-based tests poll the server until it responds successfully
before driving it, instead of assuming it is already up.

This guards against a race we saw on Jenkins CI:
t/db_dependent/selenium/00-onboarding.t and
t/db_dependent/selenium/01-installation.t both failed with 'no such
element: //div[@class="alert alert-success"]' right after the DB
connection check step, immediately after run_tests.pl's
get_commands_to_reset_db() restarted Apache/Plack. That helper
restarts services and moves straight to running the tests with no
readiness wait, so the very first requests can hit workers that are
still warming up. The test then fails on a missing page element even
though there is no bug in the installer flow itself.

A companion issue has been filed against koha-misc4dev to add a
readiness wait to run_tests.pl itself:
https://gitlab.com/koha-community/koha-misc4dev/-/work_items/108

Test plan:
1. Apply the patch
2. Run t/db_dependent/selenium/01-installation.t (or 00-onboarding.t)
   against a fresh empty database with KOHA_TESTING=1, as usual
3. Confirm it still passes normally
4. To exercise the new wait: drop/recreate the database, then run
   "sudo service apache2 restart" and "sudo service koha-common
   restart", then immediately run the selenium test with no delay.
   Confirm it either passes (server came up in time) or fails fast
   with a clear "Cannot wait more for the server to be ready" message
   rather than a confusing missing-element error deep in the install
   wizard

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 16:03:56 +00:00
nela a86b7a063d
Bug 43650: incremental EAN-13 barcodes produce unexpected sequence
When incremental EAN-13 barcodes are selected for autoBarcode and new item
records are created an unexpected sequence of barcode numbers is produced
instead of a sequence where the barcode is incremented by 1 between each item.
This patch fixes that by adding quote marks in the inline JS, thus treating the
barcode as a string rather than an integer :+)

Test plan:
1. have records imported from somewhere but with no items
2. autoBarcode set to incremental EAN-13 barcodes
3. create a number of new items
4. observe that the item barcodes don't follow the expected sequence
5. apply patch
6. delete item records
7. repeat step 3
8. observe that the item barcodes follow the expected sequence

Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 16:03:52 +00:00
Martin Renvoize e1e08a7f5d
Bug 40901: (QA follow-up) Keep daemon output in the traditional log files under systemd
Under koha-sysv the daemon(1) wrapper captures each daemon's stdout and
stderr to /var/log/koha/INSTANCE/NAME-output.log and NAME-error.log.
The systemd units sent that output to the journal instead, so
zebra-*.log, indexer-*.log, es-indexer-*.log, worker-*.log and
sip-error.log silently stopped being written on migrated hosts, and
log locations differed between the two init packages.

Use StandardOutput=append: and StandardError=append: on those six
units so the files are identical whichever package supervises the
daemons. The shipped logrotate stanza uses copytruncate, which is safe
with append-mode descriptors. The journal still records systemd's own
messages about each unit, which is what matters when one fails to
start, and README.Debian shows the drop-in for sites that prefer the
journal. Plack and the Z39.50 responder already wrote their own files.

The README, NEWS.Debian entry and koha-common(8) text that described
the journal-only behaviour are updated accordingly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:39 +00:00
Martin Renvoize 0aa04d9ddc
Bug 40901: (QA follow-up) Document the two init packages, the new options and the logging change
- koha-systemd.README.Debian was written for the first prototype: it
  named koha-worker-long@.service (now koha-worker-long_tasks@), said
  SIP needs the sip.enabled flag file (no longer read on systemd hosts),
  described coexisting with koha-common and a manual migration that the
  postinst now performs. Rewrite it to describe the actual units, how
  the koha-* helpers map onto them, where each configuration value is
  read from, how to relax the sandboxing with a drop-in, and the
  migration.
- Logging is the one visible behavioural change and was undocumented:
  with koha-sysv the daemon(1) wrapper captured stdout/stderr to
  /var/log/koha/INSTANCE/NAME-output.log and NAME-error.log; with
  koha-systemd that output is in the journal, so zebra-*, indexer-*,
  es-indexer-*, worker-* and sip-error.log are no longer written, while
  everything Koha writes itself (Starman's plack.log/plack-error.log,
  the log4perl files, z3950.log, the Apache vhost logs) is unchanged.
  Spell that out in README.Debian, in a new koha-systemd NEWS.Debian
  entry shown by apt-listchanges on upgrade, and in a "Service
  management" section of the koha-common(8) man page.
- Add --enable/--disable to the koha-zebra, koha-worker, koha-indexer
  and koha-es-indexer man pages.
- z3950_responder.pl's synopsis listed --config-dir twice and had lost
  -c, which is still a YAZ pass-through option; put -c back.
- t/00-valid-systemd-units.t: give it the usual shebang, and ignore
  every "Command ... is not executable" line from systemd-analyze rather
  than only the ones for scripts in this repository. Whether starman or
  /usr/share/koha/bin/... is installed on the host running the test says
  nothing about the unit files, and the old filter made the test fail on
  any development machine without the Koha packages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:38 +00:00
Martin Renvoize ffe8b55b57
Bug 40901: (QA follow-up) Make koha-disable/koha-enable symmetric and fix koha-remove/koha-create
koha-disable had gained "koha-sip --disable" and an unconditional
"koha-plack --disable". On both backends those are not just systemd
bookkeeping: on SysV the former deletes the sip.enabled flag and the
latter comments the Plack includes out of the Apache vhost (which
koha-disable then restarts Apache to apply). koha-enable was unchanged,
so "koha-disable x && koha-enable x" left the instance on CGI with SIP
off. Go back to stopping the daemons only (the new Plack and ES indexer
stops are kept), and under systemd stop and disable the per-instance
koha@<inst>.target so the instance stays down across reboots while the
individual units keep their enablement. koha-enable re-enables and
starts that target, so an enabled instance comes back exactly as it
was.

koha-remove nested every --disable inside "if is_*_running", so a unit
that was enabled but stopped kept its symlink in
koha@<inst>.target.wants/ after the instance was gone, which is the
very thing the block below it says it prevents. Disable unconditionally.

koha-create aborted after the database and vhost existed if a
systemd-only --enable failed, because the helpers run under set -e;
guard them like the systemctl calls already were. It also only started
koha.target, which is a no-op when the target is already active, so
the new instance's units were not started; start koha@<name>.target
itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:36 +00:00
Martin Renvoize 0bdb94bfbb
Bug 40901: (QA follow-up) Harden the service abstraction in koha-functions.sh
- koha_init_backend relied on "systemctl list-unit-files" exiting
  non-zero when nothing matches. That behaviour is version dependent,
  and on a systemd host running koha-sysv a false positive would send
  every koha-* helper to units that do not exist. Test for the
  koha-plack@.service unit file instead: unambiguous, and no systemctl
  fork per call. The _KOHA_INIT_BACKEND cache is dropped; every caller
  ran the function in a $(...) subshell so it never took effect anyway.
- The _sysv_worker and _sysv_z3950 code moved here from bash-only
  scripts brought "((error_count++))" and "[[ ! $VAR ]]" into a
  #!/bin/sh library that koha-disable, koha-enable, koha-remove,
  koha-list and the init script source under dash. Use POSIX
  equivalents.
- _sysv_restart_zebra and _sysv_restart_sip lost the explicit
  "return 0" of the original quiet not-running branch, so a start
  failure now aborted the caller's whole instance loop under set -e
  (including the init script's restart across all instances). Restore
  it.
- systemctl status is run with --no-pager from koha_service_ctl and
  koha-systemd-ctl so the helpers never block on a pager.
- koha-plack --enable/--disable ran systemctl before editing the Apache
  configuration, so a systemctl failure under set -e left the vhost
  untouched. Do the systemd half after the Apache edits and warn
  instead of aborting.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:35 +00:00
Martin Renvoize 606fc6bcd6
Bug 40901: (QA follow-up) Fix the koha-systemd migration, upgrade and removal paths
First install (migration from the SysV layout):

- The postinst ran daemon-reload before stopping koha-common.service, so
  the name already resolved to the new native unit and the stop was a
  no-op; the daemon(1)-supervised starman/zebrasrv/workers survived and
  kept plack.sock and the Zebra sockets. The init script itself is gone
  by then (koha-common's rm_conffile) and the koha-* helpers already
  dispatch to systemd, so neither can be used either. Kill the legacy
  koha-common.service cgroup (the generated unit has KillMode=process,
  a plain stop leaves the supervisors alive), sweep remaining pid files,
  and wait for them to exit, all before daemon-reload.
- Nothing ever started the new units, and koha-common.service was never
  enabled, so a migrated host came back with every instance stopped and
  "systemctl restart koha-common" doing nothing. Enable both koha.target
  and koha-common.service and start them through deb-systemd-invoke.
- koha-plack@ was enabled for every instance including CGI-only ones,
  and koha-indexer@ regardless of USE_INDEXER_DAEMON. Use
  "koha-list --enabled --plack" and /etc/default/koha-common like the
  init script did.
- Drop the "systemctl disable koha-common.service" that targeted our
  own compat unit.

Upgrade: deb-systemd-invoke takes unit names, not glob patterns, so
enumerate the active koha-*@*.service units before try-restarting them.

Removal: the prerm only did a daemon-reload, leaving every daemon
running with its unit file deleted, so swapping to koha-sysv started a
second copy of each. Stop koha-common.service and koha.target on
remove; a new postrm removes the enablement symlinks on purge and
reloads the manager.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:34 +00:00
Martin Renvoize 1a81cf9bc7
Bug 40901: (QA follow-up) Polish the per-component units
- Order services Before= their instance target instead of After=. With
  After=, koha@<inst>.target (and therefore koha.target) reported active
  before a single daemon had started, so its state meant nothing to
  anything ordered after Koha. koha@.target is likewise Before=koha.target.
- Add SyslogIdentifier=%i-koha-<service> to every unit so journal lines
  can be filtered per instance and component (the template this series
  removed already did this) and Documentation= pointing at each helper's
  man page.
- koha-plack@: stop with SIGQUIT and a 30s timeout. Starman treats QUIT
  as graceful shutdown and TERM as immediate; the SysV path used
  QUIT/30/KILL/5, so the default SIGTERM cut in-flight requests on every
  restart. Drop the redundant --user/--group starman flags, User=/Group=
  already apply.
- koha-worker@, koha-worker-long_tasks@: KillMode=mixed with a 120s
  timeout so the parent is signalled but a running forked job gets a
  chance to finish before it is killed.
- koha-zebra@: remove every *socket file in the run directory rather
  than the two default names, so sites with customised <listen> entries
  get the same stale-socket protection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:32 +00:00
Martin Renvoize 5562b26bb0
Bug 40901: (QA follow-up) Fix Z39.50 option expansion and honour the indexer defaults
Two problems in how the units consume koha-generate-env's output:

1. koha-z3950@.service passed ${Z3950_ADDITIONAL_OPTS}. In systemd unit
   files "${VAR}" always expands to exactly one argument, and to a single
   empty argument when the variable is empty; only unbraced "$VAR" is
   word-split. So a configured value like "--add-item-status k -t 5"
   reached z3950_responder.pl as one unknown option, and the default
   empty value reached it as a "" listener address. Verified with
   systemd-run: ${X}="--a --b" gives ["--a --b"], ${Y}="" gives [""],
   $X gives ["--a"] ["--b"]. Use the unbraced form.

2. koha-indexer@.service hardcoded rebuild_zebra.pl -daemon -sleep,
   ignoring ALTERNATE_INDEXER_DAEMON and INDEXER_PARAMS from
   /etc/default/koha-common that the SysV path honours. koha-generate-env
   now resolves INDEXER_DAEMON and INDEXER_PARAMS exactly like
   koha-indexer and the unit runs them through /usr/bin/env, so any
   configured indexer works.

koha-generate-env is also tightened up: numeric values are validated
and fall back to their defaults, every value is forced onto one line
without quote characters so the EnvironmentFile always parses, the
debug_mode test matches is_debug_mode, Z3950_ADDITIONAL_OPTS is read
from /etc/default/koha-common first (as before this patchset), the
Z39.50 config dir uses the same config.xml test as is_z3950_enabled,
the env dir is created with the instance's ownership if it does not
exist yet, and unknown services get an empty env file instead of a
chmod failure. The responder is invoked through /usr/bin/perl like the
other Perl daemons.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:31 +00:00
Martin Renvoize 3fb9421561
Bug 40901: (QA follow-up) Make koha-common.service a dependency-free shim
The compat unit declared BindsTo=koha.target and After=koha.target while
its ExecStop ran "systemctl stop koha.target". After= orders this unit's
stop before the target's, so the ExecStop waited on a job that could not
run until the ExecStop itself finished. Every "systemctl stop koha-common"
and every "systemctl restart koha-common" hung for TimeoutStopSec (90s
by default) and left the unit in failed state. An async --no-block stop
does not help either: the queued target stop replaces the start job the
restart transaction needs, and everything ends up stopped.

Drop the dependencies. The unit is now a plain oneshot with
RemainAfterExit that starts koha.target in ExecStart and stops it in
ExecStop, which is all the old "service koha-common" UX requires.
Verified with a replica set of user units: start, stop and restart of
the shim each complete in well under a second, restart gives every
service a new PID, and managing koha.target directly is unaffected.
The one cosmetic consequence is that the shim stays "active (exited)"
if koha.target is stopped behind its back; README.Debian says to look
at koha.target for real status.

Also sets SyslogIdentifier so its journal lines are identifiable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:30 +00:00
Martin Renvoize 529df17b42
Bug 40901: (QA follow-up) Install units to /lib/systemd/system and tidy control
The old koha-common package shipped /lib/systemd/system/koha-common.service
while koha-systemd installs to /usr/lib/systemd/system. On a merged-/usr
Debian 12 host these are the same file through the /lib symlink, but
dpkg compares path strings, so Replaces does not apply and removing the
old package's copy can delete the file koha-systemd just unpacked
(the DEP-17 aliasing problem). Keep the units under lib/systemd/system,
the path the previous package used, so this is an ordinary takeover.

While here:
- koha-core gains Recommends: koha-systemd | koha-sysv. Installing it
  alone previously gave helpers that fell back to the daemon(1) wrapper
  that no longer gets installed.
- drop a stray comment from the Source stanza of control.in
- stop appending Perl dependencies to koha-common.substvars: the
  metapackage no longer uses ${koha:Depends}

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:28 +00:00
Martin Renvoize 2992ea058b
Bug 40901: (QA follow-up) Migrate debconf answers from koha-common to koha-core
The debconf questions moved from the koha-common/* to the koha-core/*
namespace, but nothing carried the existing answers across, so a site
that had opted out of automatic translation updates was asked again
and defaulted back to "yes". koha-core.config now seeds the new
question, including its "seen" flag, from the old one when it exists.

debian/koha-common.templates was left behind after koha-common.config
was deleted; the metapackage registered three questions that no config
script ever asks, so remove it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:26 +00:00
Martin Renvoize 67d9b903d6
Bug 40901: (QA follow-up) Restore the RabbitMQ and memcached steps in koha-common.postinst
The monolithic koha-common.postinst ended by enabling the rabbitmq_stomp
plugin, restarting RabbitMQ and restarting memcached (Bug 35242). None
of that survived the split: koha-core.postinst never had it and the new
koha-common.postinst was an empty stub. A fresh "apt install koha-common"
therefore left the STOMP plugin disabled, and upgrades stopped flushing
memcached, which is exactly the stale-cache class of bug 35242 fixed.

koha-common is the package that depends on rabbitmq-server and
memcached, so the steps belong in its postinst. The copy in
koha-full.postinst is dropped now that koha-full depends on
koha-common.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:25 +00:00
Martin Renvoize 9485938333
Bug 40901: (QA follow-up) Keep /etc conffiles under their koha-common names
The cron, logrotate and defaults files were renamed from koha-common.*
to koha-core.*, so debhelper installed them as /etc/cron.d/koha-core,
/etc/logrotate.d/koha-core and /etc/default/koha-core. dpkg never
deletes a conffile that a package stops shipping, so after an upgrade
both the old and the new files exist side by side:

- every cron job (process_message_queue.pl every 15 minutes,
  overdue_notices.pl, fines.pl, backups...) runs twice
- logrotate complains about duplicate log entries every night
- the admin's /etc/default/koha-common (USE_INDEXER_DAEMON,
  INDEXER_PARAMS, ES_* tuning) is replaced by a symlink to a fresh
  copy of the packaged defaults, silently losing every customisation

Keep the on-disk names instead: koha-core now ships the files as
debian/koha-core.koha-common.* and debian/rules passes
--name=koha-common to dh_installinit, dh_installcron and
dh_installlogrotate. Because koha-core Replaces the old koha-common,
dpkg hands each conffile over with the admin's content intact and no
duplicates are left behind. The defaults file content is byte-identical
to the old koha-common.default so nobody gets a spurious conffile
prompt, and koha-generate-env reads the single canonical name again.

The experimental pre-split koha-core (used by koha-full) had its own
/etc/default/koha-core and /etc/init.d/koha-core; a new
koha-core.maintscript migrates the former with mv_conffile and removes
the latter, with the matching rc.d links dropped in the postinst.
/etc/koha/koha-worker@.service, whose template was deleted, is removed
from upgrading systems too. koha-core.preinst gains the #DEBHELPER#
token the maintscript machinery needs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:24 +00:00
Martin Renvoize 760641c550
Bug 40901: (QA follow-up) Install the SysV init script through dh_installinit
debhelper treats debian/<package>.init as that package's init script, so
koha-sysv would have shipped /etc/init.d/koha-sysv (with generated
update-rc.d and invoke-rc.d handling) as well as the .install-copied
/etc/init.d/koha-common (with none). That meant two identical init
scripts, two generated units on systemd hosts, and on a fresh
non-systemd install the "update-rc.d koha-common disable" call in the
postinst aborting with "no runlevel symlinks to modify" because
nothing had ever created them.

Rename the script to debian/koha-sysv.koha-common.init and run
dh_installinit --name=koha-common for koha-sysv. debhelper now installs
it as /etc/init.d/koha-common, registers the rc.d links, starts it on
install, restarts it on upgrade, stops it on removal and removes the
links on purge, which is exactly what the old koha-common package did.
The hand-written prerm/postrm and the Bug 18250 disable/enable dance
(a one-off link reshuffle from 2017 that is a no-op under
dependency-based boot) are dropped in favour of the generated code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:22 +00:00
Martin Renvoize b8b064bf40
Bug 40901: (QA follow-up) Fix Replaces/Breaks version bound for the package split
The split lands in the 26.06 development cycle (Koha.pm is at
26.06.00.xxx), but the Replaces/Breaks on the old monolithic
koha-common were written as (<< 25.11). Released 25.11.x and 26.05.x
koha-common packages therefore don't match, and upgrading from them
aborts with dpkg's "trying to overwrite '/usr/sbin/koha-list', which
is also in package koha-common".

Use (<< 26.06~) everywhere, including the rm_conffile version in
koha-common.maintscript, so every pre-split release is covered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:21 +00:00
Kyle M Hall 699a7a3c0c
Bug 40901: (QA follow-up) Use /run instead of legacy /var/run in unit files
systemd 257 warns that these units point PIDFile at the legacy
/var/run directory. This patch updates the remaining /var/run/koha
paths to /run/koha to match the EnvironmentFile lines; /var/run is a
symlink to /run on Debian, so nothing changes at runtime.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:19 +00:00
Kyle M Hall 29e31a2257
Bug 40901: (QA follow-up) Make the systemd unit validation test run in KTD
The test failed in KTD instead of skipping, and the systemd-analyze
option it relied on doesn't exist in any systemd QA will meet, so the
unit files were never actually checked anywhere. Load Test::NoWarnings
after the plan, use the SYSTEMD_UNIT_PATH environment variable
instead, and ignore noise from helper scripts the packages haven't
installed yet.

Test Plan:
1) Apply this patch
2) prove t/00-valid-systemd-units.t
3) Note all units are now verified instead of the file failing!

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:18 +00:00
Kyle M Hall 7169ea9e2d
Bug 40901: (QA follow-up) Move ES indexer re-discovery into koha-systemd-ctl
koha-common.service had an unexplained one-line shell loop in
ExecStartPre. It's now a documented sync-es-indexers command in
koha-systemd-ctl that does the same thing: notice instances that
switched to Elasticsearch and start their indexers, like the SysV init
script did on every start.

https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=40901#c132
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:17 +00:00
Kyle M Hall a46f1aa4ca
Bug 40901: (QA follow-up) Don't stop all Koha services on every koha-systemd upgrade
The postinst stopped and disabled koha-common.service on every
configure, which after migration means every upgrade of this package
shuts down every Koha instance. Now it only happens on first install,
where it exists to stop the old SysV-started services. The hand-rolled
deletion of the old init script is replaced with dpkg's standard
rm_conffile mechanism.

https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=40901#c130
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:15 +00:00
Kyle M Hall 67ec61a60f
Bug 40901: (QA follow-up) Add Replaces/Breaks on old koha-common to koha-systemd
The old koha-common package shipped the same koha-common.service file
that koha-systemd now ships. Declaring Replaces/Breaks lets upgrades
hand the file over cleanly, matching what koha-sysv and koha-core
already declare.

https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=40901#c129
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:14 +00:00
Kyle M Hall de946eac7f
Bug 40901: (QA follow-up) Remove redundant unit files ( koha-sysv.service, koha-worker@.service )
Nothing ever used either of these files. On systemd hosts koha-sysv
already gets a generated koha-common.service, so shipping its own unit
meant two units driving the same init script. The koha-worker@.service
sample in /etc/koha was a copy-by-hand template that koha-systemd's
real worker unit replaces.

https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=40901#c129
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:13 +00:00
Kyle M Hall 653d6092b5
Bug 40901: (QA follow-up) Make koha-full depend on koha-common so it gets an init system
After the package split, installing koha-full got you Koha with no way
to start its services. Depending on koha-common brings in an init
system and the other services, leaving only the database server to
add.

https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=40901#c128
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:11 +00:00
Martin Renvoize 71f9a68679
Bug 40901: (QA follow-up) Fix systemd unit test for Test::NoWarnings and older systemd
Add Test::NoWarnings and probe for --unit-path support, skipping on
systemd versions that don't recognise the flag (e.g. KTD's Debian image).

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:10 +00:00
Martin Renvoize 445b955b78
Bug 40901: (QA follow-up) Fix koha-disable --disable idempotency under set -e
koha-plack, koha-zebra, koha-indexer and koha-es-indexer all return
exit code 1 when asked to disable a service that is already disabled.
koha-disable has set -e, so this propagated as a fatal error when
koha-create called koha-disable on a freshly-created instance (where
none of the per-service configs are enabled yet).

Apply the same || true guard that koha-worker already had to the four
unconditional --disable calls. The desired postcondition (service
disabled) is achieved either way; the exit code from the sub-script
is not meaningful here.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:08 +00:00
Martin Renvoize 6b54ee976b
Bug 40901: Add systemd unit file validation test
Adds t/00-valid-systemd-units.t which runs systemd-analyze verify
against every unit file in debian/systemd/. Skips gracefully if
systemd-analyze is not installed (requires the systemd package).

To enable this test in KTD-based CI, add 'systemd' to the KTD
container's package list. systemd-analyze verify does not require
systemd to be running as PID 1 — it is pure static analysis and
works correctly in a standard Docker container.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:07 +00:00
Martin Renvoize 3eb65c1041
Bug 40901: (QA follow-up) Drop redundant ExecStart systemctl call in koha-common.service
koha-common.service already declares BindsTo=koha.target and
After=koha.target.  BindsTo= implies Requires=, so systemd will start
koha.target automatically when this compat unit starts and will wait for
it to be active before proceeding.  The explicit

    ExecStart=/bin/systemctl start koha.target

was therefore redundant, and calling systemctl from within a service body
is an anti-pattern (it makes an extra D-Bus round-trip back to PID 1 and
can deadlock in edge cases during early boot).

Replace with /bin/true so the oneshot succeeds immediately after the
ExecStartPre ES-indexer discovery step.  ExecStop is kept as-is because
there is no declarative equivalent for "stopping this unit should stop
koha.target" — BindsTo= only propagates stops in the other direction.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:05 +00:00
Martin Renvoize 868947cb55
Bug 40901: (QA follow-up) Quote path/option values in koha-generate-env env files
systemd EnvironmentFile parsing treats unquoted values as ending at the
first whitespace.  PLACK_PSGIFILE and Z3950_CONFIGDIR are paths that
could in principle contain spaces; Z3950_ADDITIONAL_OPTS routinely holds
multiple flags separated by spaces (e.g. "--port 9999 --verbose").

Wrap these three values in double-quotes in the generated env file so
systemd reads the full value even when it contains spaces.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:04 +00:00
Martin Renvoize 6d3b026d91
Bug 40901: (QA follow-up) Fix koha-sysv.postinst configure guard and double-enable
Two problems in the original koha-sysv.postinst:

1. #DEBHELPER# was placed at the top (line 5) before any case statement,
   so whatever dh_installsystemd expands it to ran for ALL postinst
   invocations (abort-upgrade, abort-remove, triggered…), not just
   configure.

2. The custom block manually called `deb-systemd-helper enable
   koha-common.service` on systemd hosts.  Because koha-sysv also ships
   koha-sysv.service (which dh_installsystemd enables via #DEBHELPER#),
   both koha-sysv.service and the sysv-generator-created koha-common.service
   could end up enabled, resulting in a double-start of the init script
   at boot.

Fix both by wrapping the custom code in `case "$1" in configure)`, moving
manual enable: dh_installsystemd already enables and starts
koha-sysv.service correctly.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:03 +00:00
Martin Renvoize 53ebfcb8c5
Bug 40901: (QA follow-up) koha-disable must disable units even when stopped
On systemd, koha-disable gated both --stop and --disable behind
is_*_running checks (systemctl is-active).  A service that was enabled
but not currently active (e.g. stopped for maintenance) was therefore
never disabled, leaving its unit enabled and able to restart on the next
boot — so the instance was never truly disabled.

Separate the concerns: keep the is_*_running guard around --stop (so we
don't try to stop an already-stopped service), but call --disable
unconditionally for plack, zebra, indexer, and es-indexer.  The disable
call is idempotent on both systemd and SysV, so calling it on an already-
disabled unit is safe.

SIP was already handled correctly (using is_sip_enabled).  Worker units
already used || true so were unaffected.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:01 +00:00
Martin Renvoize 3644a8407f
Bug 40901: (QA follow-up) Fix stale /etc/default/koha-common reference in koha-generate-env
koha-generate-env sourced /etc/default/koha-common to read
INDEXER_TIMEOUT, but the defaults file was renamed to
/etc/default/koha-core in this patchset.  On fresh installations no
/etc/default/koha-common exists, so INDEXER_TIMEOUT was always silently
fixed at the built-in default of 5, ignoring any admin customisation.

Read /etc/default/koha-core first; fall back to /etc/default/koha-common
so that systems upgrading from the pre-split packaging still pick up
their existing settings.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:02:00 +00:00
Kyle M Hall b5000b8869
Bug 40901: (follow-up) Systemd hardening
I think the following systemd security measures are safe for Koha:

ProtectSystem=full
Sets /usr & /boot to read-only

ProtectHome=yes
Hides /home, /root and /run/user
NOTE: The hiding of /run/user through me off, but google says it's only for
interactive logins only. AFAIK the only thing that does that is
koha-shell which isn't a systemd thing so it is unaffected

PrivateTmp=yes
Private /tmp & /var/tmp
NOTE: AFACT nothing in Koha shares files via /tmp which feels like it
would be a bad thing to do anyway

NoNewPrivileges=yes
setuid binaries can't re-elevate
NOTE: This feels like the most important one!

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:59 +00:00
Kyle M Hall 05228589ed
Bug 40901: (follow-up) Drop redundant StopWhenUnneeded on koha@.target
PartOf=koha.target already cascades stop to the per-instance target
so StopWhenUnneeded is not needed and adds more complication that could
trip us up later.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:58 +00:00
Kyle M Hall 88926c5032
Bug 40901: (follow-up) Tighten RestartSec on SIP, Z3950, es-indexer
30 seconds between restart attempts is a very long time especially for
AIP. With StartLimitIntervalSec at 60, and RestartSec at 5 we get 6
retries and what is hopefully a less visible downtime window for
the tcp services. For es-indexer it's not so big a deal but could be
an issue for cataloguers.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:57 +00:00
Kyle M Hall a84dbc3d29
Bug 40901: (follow-up) Order per-component units after koha-create-dirs
Pair each Requires=koha-create-dirs@%i.service with a matching
After=, and drop the now redundant Before= list from
koha-create-dirs@.service.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:56 +00:00
Kyle M Hall a6226588f6
Bug 40901: (follow-up) Cap restart attempts on per-component units
Add StartLimitIntervalSec=60 add StartLimitBurst=3 so a broken-at-
startup daemon stops looping at after three attempts.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:55 +00:00
Kyle M Hall 0f2f88371d
Bug 40901: (follow-up) Re-discover ES indexers on koha-common start
The SysV init script runs `koha-list --enabled --elasticsearch` at
every start. ExecStartPre on koha-common.service restores that.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:53 +00:00
Kyle M Hall 1425743c10
Bug 40901: (follow-up) koha-remove leaves koha@<inst>.target enabled
Disable koha@<inst>.target and koha-create-dirs@<inst>.service so
their koha.target.wants/ symlinks don't outlive the instance.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:52 +00:00
Kyle M Hall d0d193ea7e
Bug 40901: (follow-up) Clean up stale Zebra Unix sockets on startup
zebrasrv removes its Unix sockets on clean exit but leaves them behind on
SIGKILL, OOM, or sysv to systemd swap, causing 'Address already in use' on
the next start. ExecStartPre rm makes the unit auto-fix itself.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:51 +00:00
Kyle M Hall 5e37c3e412
Bug 40901: (follow-up) Allow seamless swap between koha-systemd and koha-sysv
koha-systemd and koha-sysv have always declared mutual Conflicts
so you can't install both, but they don't declare Replaces which
would make it so installing one autmatically removes the other

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:50 +00:00
Kyle M Hall 8023b38202
Bug 40901: (follow-up) koha-create enables koha@<inst>.target on systemd
Without this, freshly-created instances on a koha-systemd host do
not auto-start at next boot.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:49 +00:00
Kyle M Hall 9ce9a07460
Bug 40901: (follow-up) sip.enabled is an anti-pattern for systemd
Remove the use of sip.enabled for systemd, keep for sysv

No other service using a .enabled file is controlled via systemd or sysv

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:48 +00:00
Kyle M Hall 8dccb4cc37
Bug 40901: (follow-up) Use --config-dir for z3950_responder.pl
Documentation is wrong, -c doesn't work for z3950_responder.pl

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:47 +00:00
Kyle M Hall c82e55a5a5
Bug 40901: (follow-up) Fail loudly when koha-sip is enabled without flag file
The koha-sip@.service unit gates startup on
/var/lib/koha/<inst>/sip.enabled, the per-instance "I want SIP on"
flag file. `koha-sip --enable` creates that flag in the same call
that runs `systemctl enable`; running a bare `systemctl enable
koha-sip@<inst>.service` only flips the systemd half, so the unit
ends up enabled-but-refusing-to-start.

With ConditionPathExists the failure mode is silent and the unit
ends up "enabled" at the systemd level but "Active: inactive"
(dead) with "Condition: start condition failed" and the journal
logs aren't a big help figuring out the problem.

This patch replaces the silent skip with a loud failure with a helpful message:
ERROR: SIP not enabled for instance <inst>. Use: koha-sip --enable <inst>

SIPconfig.xml is gated by AssertPathExists if it's missing the unit fails.

Either failure path lands the unit in failed state, surfaced by 'systemctl --failed'

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:45 +00:00
Tomás Cohen Arazi d67d319b89
Bug 40901: (follow-up) Preserve 'service koha-common' UX with systemd
Adds PartOf=koha.target to koha@.target so that stopping or
restarting the global target propagates to all instance targets
and their services.

Ships a koha-common.service oneshot unit that delegates to
koha.target, so 'systemctl restart koha-common' works as the
drop-in replacement for the old 'service koha-common restart'.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:44 +00:00
Tomás Cohen Arazi bf8529fbd4
Bug 40901: (follow-up) Read service config from koha-conf.xml in systemd units
The systemd units were missing most of the parameters that the SysV
init functions read from koha-conf.xml. This caused wrong worker
counts, missing log files, ignored batch sizes, and other config
being silently dropped.

This patch adds koha-generate-env, a helper script that reads
koha-conf.xml and writes an EnvironmentFile for each service.
Each unit runs it as ExecStartPre and then uses the variables.

Changes per unit:
- koha-plack@: workers, max-requests, access/error logs,
  environment (development/deployment), per-instance psgi, -M FindBin
- koha-zebra@: loglevels, max_record_size, timestamp format
- koha-es-indexer@: batch_size
- koha-indexer@: INDEXER_TIMEOUT from /etc/default/koha-common
- koha-z3950@: config dir fallback to global, additional options

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:42 +00:00
Tomás Cohen Arazi d8b3d46634
Bug 40901: (follow-up) Fix parallel execution of SysV and systemd services
When upgrading from the old koha-common (pre-split) to the new
koha-core + koha-systemd packages, the SysV init script
/etc/init.d/koha-common is left behind as an orphan. The
systemd-sysv-generator auto-creates koha-common.service from it,
which runs in parallel with the native systemd units, causing
double-starts of all Koha services.

Changes:
- koha-systemd.postinst now stops, disables, and removes the
  orphaned SysV init script when koha-sysv is not installed
- koha-sysv gets Replaces/Breaks on koha-common (<< 25.11) so
  dpkg can cleanly take over the init script file

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:41 +00:00
Tomás Cohen Arazi 6763a3048b
Bug 40901: (follow-up) Enable systemd units for existing instances on install
On first install of koha-systemd (migration from SysV), enable
units for all existing Koha instances. Core services (plack, zebra,
workers) are always enabled. SIP, Z39.50, and indexers are enabled
conditionally based on instance configuration.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:40 +00:00
Tomás Cohen Arazi af44d814af
Bug 40901: (follow-up) Add Replaces/Breaks for smooth upgrade from koha-common
koha-core takes over files previously owned by koha-common
(e.g. /etc/default/koha-common). Without Replaces/Breaks, dpkg
refuses to overwrite them during upgrade.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:38 +00:00
Tomás Cohen Arazi bde94861c4
Bug 40901: (follow-up) Regenerate debian/control from control.in
Run debian/update-control to include koha-systemd and koha-sysv
package stanzas and refresh Perl autodependencies.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:37 +00:00
Tomás Cohen Arazi 439be05aef
Bug 40901: (follow-up) Extract koha-create-dirs into a oneshot unit
Replace per-service ExecStartPre=+koha-create-dirs (running as root
on every start of every service) with a single oneshot unit per
instance. Services declare Requires= on it, and the oneshot's Before=
ensures directories exist before any service starts.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:35 +00:00
Tomás Cohen Arazi 90825d3292
Bug 40901: (follow-up) Fix incomplete systemd wiring in helpers
- koha-z3950-responder: add systemd enable/disable to enable_z3950/disable_z3950
- koha-disable: add plack and es-indexer stop/disable
- koha-remove: add worker --disable after --stop
- koha-systemd-ctl: fix service name worker-long to worker-long_tasks

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:34 +00:00
Tomás Cohen Arazi 630b1c377d
Bug 40901: (follow-up) Fix critical bugs in systemd integration
- koha-sip: disable_sip() was calling 'enable' instead of 'disable'
- koha-indexer: enable/disable dispatch was calling start/stop functions
- koha-systemd.postinst: upgrade try-restart was outside the case block

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:33 +00:00
Jake Bateman 142ab7320f
Bug 40901: Add systemd try-restart to koha-systemd postinst
In order for apt upgrades to trigger a service restart for all koha-systemd subservices and all instances, this adds a wildcard try-restart for those in addition to the daemon-reload. Only services which are already present and running will be restarted at postinst time

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:32 +00:00
Jake Deery d998990d77
Bug 40901: (testing follow-up) Add koha-create-dirs to units
This patch adds an elevated koha-create-dirs to the systemd units, so
that when the units (re)start, the directories are definitely present.
We found that PID files were missing and couldn't be created without
this patch.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:30 +00:00
Jake Deery 3073a347cc
Bug 40901: (testing follow-up) Added enable / disable to more helpers
This patch adds more enable / disable flags to the koha daemon helper
scripts. This is essential as systemd does not enable units by default,
especially when using instantiation variables.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:29 +00:00
Jake Deery d28ded7538
Bug 40901: (testing follow-up) Move koha-worker-long@ unit to koha-worker-long_tasks@
We found a bug when running koha-create. The systemd unit name didn't
match expected, so we've moved the unit to match the koha functions
computed value.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:28 +00:00
Jake Deery 0af4e80aa0
Bug 40901: (testing follow-up) Moved legacy code to koha-sysv.postinst
This patch moves the update-rc.d lines from koha-core to koha-sysv, so
it only fires if systemd is not in use

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:27 +00:00
Jake Deery 80e6ddaa90
Bug 40901: (testing follow-up) Remove / rename koha-common to koha-core
This patch removes or renames some of the debian files from koha-common
to koha-core, to avoid file clash during install

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:25 +00:00
Tomás Cohen Arazi a0f36621ad
Bug 40901: Add support for 'systemctl' in `koha-*` scripts
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:24 +00:00
Tomás Cohen Arazi be89db235c
Bug 40901: Restructure packages - koha-common becomes metapackage
This patch restructures the Koha packaging to support both systemd and
SysV init systems through a clean separation of concerns.

Package hierarchy:
- koha-core: Core application files (no init system)
- koha-systemd: Native systemd units (Provides: koha-init)
- koha-sysv: SysV init scripts (Provides: koha-init, Conflicts: koha-systemd)
- koha-common: Metapackage (Depends: koha-core, koha-systemd | koha-sysv)

Changes:
- koha-core: Removed 'daemon' dependency, updated description to clarify
  it provides core files without init system
- koha-systemd: Now depends on koha-core instead of koha-common, provides
  koha-init virtual package
- koha-sysv: Now depends on koha-core instead of koha-common, provides
  koha-init virtual package
- koha-common: Converted to metapackage that depends on koha-core and
  prefers koha-systemd on systemd-based systems
- Removed koha-common.install (files now installed by koha-core)
- Simplified koha-common.postinst (metapackage has minimal logic)

Benefits:
- Clean separation between core application and init system
- Users can explicitly choose their init system
- KTD can use koha-sysv without systemd
- Modern systems get native systemd by default
- Conflicts prevent both init systems being active simultaneously

Test plan:
1. Build packages
2. Install koha-common on a systemd system, verify koha-systemd is pulled in
3. Try installing both koha-systemd and koha-sysv, verify conflict
4. Install koha-core + koha-sysv explicitly, verify it works

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:23 +00:00
Tomás Cohen Arazi 34ad12360f
Bug 40901: Add koha-sysv package for SysV init support
This patch introduces the koha-sysv package, which provides traditional
SysV init.d scripts for managing Koha instances. All services (Plack,
Zebra, SIP, Z3950, workers, indexers) are managed through a single init
script using the daemon wrapper.

The package includes:
- /etc/init.d/koha-common (SysV init script)
- /lib/systemd/system/koha-common.service (systemd wrapper for init.d)

This package is intended for legacy systems, containers without systemd
(like KTD), or environments where SysV init is preferred.

The package provides 'koha-init' and conflicts with koha-systemd to
ensure only one init system is active at a time.

Test plan:
1. Build and install the package
2. Start services with /etc/init.d/koha-common start
3. Verify all services start correctly
4. Check that koha-systemd cannot be installed alongside

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:22 +00:00
Tomás Cohen Arazi dc8cced6be
Bug 40901: Add koha-systemd package with native systemd units
This patch introduces the koha-systemd package, which provides native
systemd service units for managing Koha instances. Each service type
(Plack, Zebra, SIP, Z3950, workers, indexers) is managed as a separate
systemd unit using instance templates.

The package includes:
- 8 service templates (plack, zebra, sip, z3950, worker, worker-long,
  indexer, es-indexer)
- 2 target units (koha@.target for per-instance, koha.target for global)
- Helper script (koha-systemd-ctl) for managing services

All service units include:
- KOHA_HOME environment variable pointing to /usr/share/koha
- WorkingDirectory set to /var/lib/koha/%i
- Proper user/group permissions (%i-koha)

This allows granular control over individual services per instance and
leverages systemd features like dependency management, resource limits,
and journald logging.

Test plan:
1. Build and install the package
2. Enable services for an instance
3. Verify all services start correctly
4. Check journalctl logs show no errors

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-29 09:01:21 +00:00
Pedro Amorim 02e0b4dc9e
Bug 42490: DBRev 26.06.00.030
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:36:08 +00:00
Andrew Fuerste Henry 20223a3e16
Bug 42490: (QA follow-up) Reword syspref description and options
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:29:59 +00:00
Martin Renvoize d8c245b505
Bug 42490: (QA follow-up) Address QA review comments
- Remove leftover skeleton boilerplate comments in the atomicupdate
  ("Do you stuffs here", "# sysprefs")
- Fix typo in atomicupdate description ("preferene" -> "preference")
- Update POD on MapItemsToHoldRequests to mention the new
  LocalHoldsPriorityScope gate
- Explicitly mock LocalHoldsPriorityScope in the existing Local Holds
  Priority subtests that exercise CreateQueue/MapItemsToHoldRequests,
  instead of relying on it falling through to the real DB default

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:29:58 +00:00
Andrew Fuerste Henry fa3aacffd8
Bug 42490: Update unit tests
Prove t/db_dependent/HoldsQueue.t

Sponsored-by: The Main Library Alliance <https://www.mainlib.org/>
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:29:57 +00:00
Andrew Fuerste Henry 662de2a955
Bug 42490: Implement LocalHoldsPriorityScope
To test:
1 - Set LocalHoldsPriority to "Don't give", set HoldsQueuePrioritizeBranch to "home library"
2 - have a bib with two items: both items available, Item 1 home and holdingbranch Centerville, Item 2 home and holdingbranch Fairfield
3 - place a hold (Hold 1) on the bib for a patron from Centerville , for pickup at Fairfield
4 - place a hold (Hold 2) on the bib for a patron from Fairfield, for pickup at Centerville

Establish behavior without LocalHoldsPriority
5 - build holds queue
6 - confirm the queue targets the FPL item for Hold 1 and the CPL item for Hold 2
7 - check both items out to a third patron (not one of your patrons with holds)
8 - check in Item 2, confirm it tries to fill Hold 1, click Ignore
9 - check in Item 1, confirm it also tries to fill Holds 1, click Ignore
10 - reset item locations: UPDATE items SET holdingbranch=homebranch;

Establish behavior with LocalHoldsPriority
11 - Set LocalHoldsPriority to "Give library", LocalHoldsPriorityPatronControl to "home library", LocalHoldsPriorityItemControl to "home library"
12 - build holds queue
13 - confirm the queue targets the CPL item for Hold 1 and the FPL item for Hold 2
14 - check both items out to a third patron (not one of your patrons with holds)
15 - build holds queue, confirm it has no targets
16 - check in Item 2, confirm it tries to fill Hold 2, click Ignore
17 - check in Item 1, confirm it tries to fill Holds 1, click Ignore
18 - reset item locations: UPDATE items SET holdingbranch=homebranch;

Apply patch, updatedatabase, restart_all
19 - confirm new system preference LocalHoldsPriorityScope, set to "checking items in and building the holds queue"
20 - repeat steps 11 - 18, no change in behavior
21 - set LocalHoldsPriorityScope to "checking items in"
22 - build holds queue
23 - confirm the queue targets the FPL item for Hold 1 and the CPL item for Hold 2
24 - check both items out to a third patron (not one of your patrons with holds)
25 - build holds queue, confirm it has no targets
26 - check in Item 2, confirm it tries to fill Hold 2
27 - check in Item 1, confirm it tries to fill Holds 1

Sponsored-by: The Main Library Alliance <https://www.mainlib.org/>
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:29:56 +00:00
Andrew Fuerste Henry 5434465a35
Bug 42490: Add system preference LocalHoldsPriorityScope
Sponsored-by: The Main Library Alliance <https://www.mainlib.org/>
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:29:54 +00:00
Slava Shishkin 08333dc996
Bug 42505: (follow-up) Generate hold slip after confirming hold
The previous follow-up passed the checked-in itemnumber to the slip, but
the slip was still rendered before ModReserveAffect() confirmed the
hold. Fields updated during confirmation, such as waitingdate and
expirationdate, were therefore missing or stale on the first print.

Open a blank named popup during the user gesture, then submit the
confirmation form with print_slip set. The response page reuses that
popup after the hold has been updated.

This preserves the popup blocker fix and allows ReserveSlip() to use
the persisted hold and item data without passing individual fields as
workarounds.

To test:
1. Set HoldsAutoFill to "Don't".
2. Add the following to the HOLD_SLIP notice:
   Waiting since: [% hold.waitingdate | $KohaDates %]
   Barcode: <<items.barcode>>
3. Place a title-level "Next available" hold with pickup at the
   check-in library.
4. Check in a matching item.
5. Click "Print slip and confirm".
6. Confirm the popup is not blocked.
7. Confirm the first slip contains the item's barcode and today's
   waiting date.
8. Reload the slip and confirm the values are unchanged.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:29:53 +00:00
Martin Renvoize 3ce02836d0
Bug 42505: Open hold transfer slip in user gesture to bypass popup blocker
When returning an item required to fulfil a hold at another branch with
HoldsAutoFill set to "Don't", the librarian is shown a "Hold found"
modal and chooses "Print slip, transfer, and confirm". The previous
implementation set a hidden 'print_slip' input to 1, submitted the
form, and relied on $(document).ready on the response page to call
Dopop() / window.open() to open the slip. Because that window.open()
is no longer associated with a user gesture (the gesture chain is
broken by the form submission and page navigation), modern browsers
block it as an unsolicited popup. The hold is confirmed but the slip
never appears, and the librarian may not realise it was attempted.

This patch opens the slip synchronously inside the .print click
handler instead, while the user gesture is still active, so the
browser allows the window.open() through. The form is then submitted
as before to confirm the hold. The reserve_id is taken from the
hidden input already present in the modal form; hold-transfer-slip.pl
needs only the reserve_id and does not require the hold to have been
moved to "Waiting" state first, so the slip content is correct.

To test:
1. Set syspref HoldsAutoFill = "Don't".
2. Place a hold for a patron whose pickup branch is different from
   your logged-in branch.
3. Check in the item at your logged-in branch.
4. The "Hold found" modal appears.
5. Click "Print slip, transfer, and confirm".
6. Confirm the hold transfer slip opens in a new window and is not
   blocked by the browser's popup blocker.
7. Confirm the hold is still correctly confirmed and the item placed
   in transit.
8. Repeat with HoldsAutoFill = "Don't" and a same-branch hold,
   clicking "Print slip and confirm" instead, and confirm the slip
   opens for that path too.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:29:52 +00:00
Pedro Amorim f1955d23f0
Bug 41253: DBRev 26.06.00.029
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:29:51 +00:00
Alex Carver [Acerock7] 03a817759c
Bug 41253: Fix Cataloging Side Nav
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:29:50 +00:00
Jonathan Druart 1d0e6f0930
Bug 41253: Grant permission only when the permission is created
In case it got backported, we don't want to grant permissions again,
only when the db rev is run for the first time.

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:29:48 +00:00
Alex Carver [Acerock7] 9403a95104
Bug 41253: Item Modification by Age should have its own permission
Adds a new permission to control access to the Item Modification by Age tool in the Cataloging Module.

Previously access to this tool was controlled by the items_batchmod permission. This can be an issue for consortiums
where staff members should be able to batch change items that belong to their library, but who shouldn't be able
to define item aging rules. Staff with automatic item aging tool access can use authorized values to change items
that belong to other libraries.

This patch introduces a dedicated permission (items_modification_by_age) to restrict access to the tool to users
with this permission.

- new permission definition via atomicupdate
- removal of dependency on items_batchmod for tool access
- updated permission UI display
- updated cataloguing-home button visibility
- updated flagsrequired check in tool script

Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-28 15:29:45 +00:00
Martin Renvoize 17cd7f3517
Bug 31925: Document that thesaurus-aware linking depends on index freshness
QA (comment 58/60) suggested LinkerConsiderThesaurus only works with
Elasticsearch, having observed duplicate authorities being created when
testing with Zebra. Reproduced this directly: with AutoCreateAuthorities
and LinkerConsiderThesaurus on, saving two bibs with the same new FAST
heading back-to-back (no pause) creates a duplicate authority under
Zebra, but waiting for Zebra's indexing daemon to catch up (a few
seconds) between saves lets the second heading link to the first-created
authority correctly, as designed.

The same duplicate-on-rapid-succession behaviour also reproduces with
LinkerConsiderThesaurus off and a plain LCSH heading - it's a pre-existing
race between AutoCreateAuthorities and Zebra's asynchronous indexing
daemon, not something specific to thesaurus-aware matching or this bug.
Elasticsearch indexes near-synchronously within the same request, so the
same race window is far less likely to be hit there, which is almost
certainly why this only showed up in manual Zebra testing.

This documents that dependency on the LinkerConsiderThesaurus system
preference description, rather than gating the feature to Elasticsearch
only, since the underlying logic is search-engine agnostic and works
correctly on Zebra given normal indexing latency.

Sponsored-by: Heythrop Library <https://heythroplibrary.co.uk/>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:51 +00:00
Martin Renvoize b0c502407d
Bug 31925: Only code auto-created authorities with the source thesaurus when LinkerConsiderThesaurus is on
Comments 43-51 identified a further problem with unconditionally coding
a newly auto-created authority's 008/11 (and 040$f) from the source bib
heading's thesaurus: when LinkerConsiderThesaurus is off (the default),
the linker matches new headings to existing authorities by text alone,
regardless of thesaurus. So a bib heading from one thesaurus (e.g. FAST)
can auto-create an authority coded for that thesaurus, and a later bib
heading with the *same* text but no thesaurus of its own (or a different
one) can then link to that same authority purely on the text match.
Saving that authority afterwards propagates its 008/11 and 040$f coding
back onto every bib heading linked to it - silently adding indicators and
subfields the cataloguer never set, on installs that never opted into
thesaurus-aware behaviour.

Before this bug, AutoCreateAuthorities always created LCSH-coded records,
so that propagation was always onto the coding most sites already expect.
This bug's earlier patches changed that default for every site regardless
of whether they use LinkerConsiderThesaurus, which is what comments 43/45
(reproduced by Pedro Amorim) demonstrated as disruptive.

Sites that haven't turned on LinkerConsiderThesaurus don't benefit from
thesaurus-aware authority creation anyway, since their linker never
searches by thesaurus - C4::Heading::_search and C4::Linker::Default
already gate thesaurus-consideration on this same preference for
matching. This change gates authority-creation coding on it too, so the
behaviour introduced by this bug only applies to installs that already
opted into thesaurus-aware linking.

Test plan:
1. prove t/db_dependent/Biblio.t
   Note the new case 7 in the "codes new authorities with the source
   thesaurus" subtest: with LinkerConsiderThesaurus off, a FAST heading
   no longer codes the new authority as FAST, it keeps the
   MARCAuthorityControlField008 syspref default instead.
2. Librarian-facing regression check: with LinkerConsiderThesaurus off
   (the default), AutoLinkBiblios and AutoCreateAuthorities on, catalogue
   a 650 with ind2=7 $2=fast and save. The new authority's 008/11 should
   be whatever MARCAuthorityControlField008 says (LCSH 'a' by default),
   not 'z'.
3. Confirm the LinkerConsiderThesaurus=on cases from the earlier patches
   are unaffected: with it on, a FAST heading still codes 008/11='z' with
   040$f=fast, and a MeSH heading still codes 008/11='c'.

Sponsored-by: Heythrop Library <https://heythroplibrary.co.uk/>
Signed-off-by: Phil Ringnalda <phil@chetcolibrary.org>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:50 +00:00
Martin Renvoize c15b42c457
Bug 31925: Don't override 008/11 when the heading's thesaurus wasn't identified
QA (comments 23-30) flagged a regression in the previous patches: the
guard around coding a new authority's 008/11 (and 040$f) from the source
heading's thesaurus was `if ( $heading->{thesaurus} )`, but
C4::Heading::MARC21::_get_subject_thesaurus() never returns a false
value - a heading with a blank/unrecognised indicator 2 (by far the
commonest real-world case) comes back as the placeholder string
"notdefined", and indicator 2 = 4 comes back as "notspecified". Both are
truthy, so the override always fired, unconditionally coding every new
authority's 008/11 as 'z' or '|' instead of respecting the site's
MARCAuthorityControlField008 default (which defaults to 'a'/LCSH) - a
disruptive behaviour change for the majority of headings, not just the
non-LCSH ones this bug is meant to fix.

Adds Koha::Authority::$MARC21_UNDEFINED_THESAURUS_VALUES, the subset of
the existing thesaurus table's keys ('notdefined', 'notspecified') that
mean "no thesaurus could be identified" rather than a genuine one, and
uses it in LinkBibHeadingsToAuthorities()'s guard so those two values now
fall through to the site's own syspref default, unchanged, exactly as
before this bug's patches.

Test plan:
1. prove t/db_dependent/Biblio.t
   Note the two new cases in the "codes new authorities with the source
   thesaurus" subtest: a blank-indicator-2 heading now keeps 008/11 at
   the syspref default ('a') instead of being forced to 'z', and an
   indicator-2=4 heading respects a customised syspref default instead
   of being forced to '|'.
2. Librarian-facing regression check: with AutoLinkBiblios and
   AutoCreateAuthorities on, catalogue a 650 with a blank/space
   indicator 2 (e.g. `650 __ $a Some topic`, no $2) and save. The new
   authority's 008 position 11 should read whatever
   MARCAuthorityControlField008 says (LCSH 'a' by default), not 'z'.
3. Confirm the non-LCSH cases from the earlier patches are unaffected:
   a FAST heading (ind2=7, $2=fast) still codes 008/11='z' with
   040$f=fast, and a MeSH heading (ind2=2) still codes 008/11='c'.

Sponsored-by: Heythrop Library <https://heythroplibrary.co.uk/>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:49 +00:00
Martin Renvoize 65dac0fb94
Bug 31925: AutoCreateAuthorities must preserve subject heading thesaurus
When AutoCreateAuthorities creates a new authority for a bib subject
heading that didn't match an existing one, it never set 008 or 040 on the
new MARC::Record, so C4::AuthoritiesMarc::AddAuthority()'s own defaulting
always ran and hardcoded 008/11 to 'a' (LCSH), regardless of the source
heading's real thesaurus. A FAST, MeSH, or other non-LCSH heading (MARC
indicator 2 = 7 with subfield $2, or indicator 2 = 1-6) therefore always
got mis-coded as LCSH on creation. Because the authority was permanently
mis-coded, it could never be found again by thesaurus-aware matching
(LinkerConsiderThesaurus), so every later import of the same non-LCSH
heading created yet another duplicate authority instead of linking to the
one already created (see bug 42694 for a real-world report of this).

This calls Koha::Authority->default_marc21_008($thesaurus) to build the
new authority's 008 - correctly coding position 11 for the source
heading's actual thesaurus - and, for headings identified only by a raw
$2 code, adds a 040$f before calling AddAuthority(), whose existing "only
default if the field isn't already there" guards then leave our
correctly-coded fields alone. No change to AddAuthority() itself is
needed for this. This is MARC21-only (matches the existing marcflavour
branch already in this code path) and ships as an unconditional bug fix,
with no new system preference: it only changes how a *new* authority is
coded going forward and does not touch or merge any existing authority
records.

Test plan (librarian-facing):
1. Turn on the AutoLinkBiblios and AutoCreateAuthorities system
   preferences (Administration -> System preferences -> Cataloging).
2. Catalog a new bibliographic record, or edit an existing one, and add a
   650 field coded for a non-LCSH thesaurus, e.g.:
     650 _7 $a Miracles $2 fast
   (2nd indicator = 7, subfield $2 = fast; a FAST heading with no
   existing matching authority in your catalogue).
3. Save the record. Koha auto-creates a new "Miracles" authority record,
   since none existed yet to link to.
4. In the Authorities module, open the newly created "Miracles" record
   for editing and switch to MARC view (Edit -> Edit record, or the "..."
   menu -> Edit as new).
5. Click into the 008 field to open its editor plugin.
   Before this fix: the "11- Subject heading system/thesaurus" dropdown
   is set to "a- Library of Congress Subject Headings", and field 040 has
   no $f subfield.
   After this fix: that dropdown is now set to "z- Other", and field 040
   has a $f subfield containing "fast" — correctly recording the real
   source vocabulary instead of silently mis-labelling it as LCSH.
6. As a regression check, repeat steps 2-5 with a plain LCSH heading
   instead, e.g.:
     650 __0 $a Feminism
   (2nd indicator = 0, no $2). Confirm the resulting authority's 008 still
   shows "a- Library of Congress Subject Headings" as before — this fix
   must not change behaviour for ordinary LCSH headings.
7. Optional, Elasticsearch installations only: with LinkerConsiderThesaurus
   also turned on, re-import or re-save a bib with the same FAST heading
   from step 2 a second time. It should now link to the authority created
   in step 3 instead of creating a second duplicate — this is the actual
   duplicate-authority problem reported in bug 42694.

Sponsored-by: Heythrop Library <https://heythroplibrary.co.uk/>
Signed-off-by: Clemens Gresser <cgresser@jesuit.org.uk>
Signed-off-by: Phil Ringnalda <phil@chetcolibrary.org>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:48 +00:00
Martin Renvoize 9186341ddd
Bug 31925: Make QueryBuilder reuse the shared thesaurus/008 lookup
Koha::SearchEngine::Elasticsearch::QueryBuilder kept its own copy of the
thesaurus-name to 008/11-code table used when building "thesaurus" search
queries against authority records. The previous commit moved that same
table to Koha::Authority as a shared lookup. This commit deletes
QueryBuilder's duplicate and points $thesaurus_to_value at
$Koha::Authority::MARC21_THESAURUS_TO_CONTROL_FIELD_008_11 instead, so
there is only one place to update if the code list ever changes.

Pure refactor: $thesaurus_to_value keeps its existing name and contents,
and build_authorities_query_compat() is unchanged, so query behaviour is
identical before and after.

Sponsored-by: Heythrop Library <https://heythroplibrary.co.uk/>
Signed-off-by: Clemens Gresser <cgresser@jesuit.org.uk>
Signed-off-by: Phil Ringnalda <phil@chetcolibrary.org>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:47 +00:00
Martin Renvoize 4bfb9abc68
Bug 31925: Add thesaurus/008 lookup and default MARC21 008 builder to Koha::Authority
AutoCreateAuthorities builds a brand-new authority record whenever a bib
subject heading (6XX) has no match, but always lets the record fall back
to C4::AuthoritiesMarc::AddAuthority()'s default 008/040, which hardcodes
008 position 11 ("Subject heading system/thesaurus") to 'a' (LCSH) no
matter what thesaurus the source heading actually uses. This is the first
step towards fixing that.

Adds two things to Koha::Authority:

- $MARC21_THESAURUS_TO_CONTROL_FIELD_008_11: a single-source-of-truth
  lookup mapping a thesaurus name (as already 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, per the LOC ad008
  code list. This table previously only existed once, for a different
  purpose, in Koha::SearchEngine::Elasticsearch::QueryBuilder (used for
  building thesaurus search queries) - it's promoted here so both that
  and the authority-creation fix that follows can read from one place
  instead of two copies that can silently drift apart.

- default_marc21_008($thesaurus): builds the default 34-character body of
  a new MARC21 authority's 008 field, honouring the
  MARCAuthorityControlField008 system preference. When $thesaurus is
  given, position 11 is set from the table above instead of whatever the
  default/syspref value carries, falling back to 'z' (Other) for a
  thesaurus with no dedicated code.

AddAuthority() is refactored to build its own (thesaurus-less, LCSH
default) 008 via this new method too, replacing its previous inline copy
of the same defaulting logic - pure refactor, no behaviour change.
Nothing thesaurus-aware calls it yet.

Sponsored-by: Heythrop Library <https://heythroplibrary.co.uk/>
Signed-off-by: Clemens Gresser <cgresser@jesuit.org.uk>
Signed-off-by: Phil Ringnalda <phil@chetcolibrary.org>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:46 +00:00
Tomás Cohen Arazi 9eaeae69e5
Bug 42719: Handle missing CGISESSID in OAuth login (IdP-initiated flow)
When a user initiates login from the IdP (e.g. Okta app tile), they
hit the OAuth endpoint without an existing session cookie. The code
crashed calling ->value on an undefined cookie object.

This patch:
- Removes the premature 'No user session found' guard that blocked
  IdP-initiated flows entirely
- Creates a session on-the-fly for the initial request when no
  cookie exists (IdP-initiated flow)
- On callback, redirects with wrong_csrf_token error instead of
  crashing when the cookie is missing

CSRF protection is preserved: the state token is bound to the
session created in the initial request, and the browser returns
the cookie on callback for validation.

Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:45 +00:00
Tomás Cohen Arazi fda4343019
Bug 42719: Add regression tests
Add tests for the OAuth/OIDC login endpoint covering:
- IdP-initiated flow (no CGISESSID cookie on initial request)
- Callback without CGISESSID cookie (session lost)
- Normal SP-initiated flow (cookie present)

Tests create a real identity provider in the database and assert:
- Initial request without cookie redirects to the IdP authorize URL
- A CGISESSID cookie is set for the new session
- Callback without cookie returns wrong_csrf_token error (not 500)
- Normal flow with cookie redirects to IdP as expected

Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:44 +00:00
Jonathan Druart 2db06f9510
Bug 43561: Redirect to local use sysprefs instead of 'not found'
If a user search for a local use syspref, the name will appear in the
autocomplete but when choosen "not found" will be return.  This patch
suggests to redirect to the "local use" syspref view if we found a local
use syspref that match the search term.

Test plan:
- in admin, type "OPACdid" into the system preference search
- see "OPACdidyoumean" is offered as a search term
- click OPACdidyoumean
=> Without this patch you get an error that no such syspref exists
=> With this patch you are redirected to the local use syspref view

Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: John Vinke <jvinke@austinseminary.edu>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:43 +00:00
Martin Renvoize f8601c1b0f
Bug 43288: Fix change amount missing from printed fee receipts
pay.tt, paycollect.tt and boraccount.tt all open printfeercpt.pl with
a change_given= query parameter after a payment, but printfeercpt.pl
only ever reads a 'change' param into its GetPreparedLetter substitute
hash - matching the ACCOUNT_CREDIT/CREDIT_* default notice templates,
which use [% change | $Price %], not [% change_given | $Price %].

As a result the change amount is silently blank on every printed fee
receipt that shows it, regardless of FinePaymentAutoPopup.

Rather than touch printfeercpt.pl or the notice content (both already
agree on 'change', matching a similar migration Koha did once before
in installer/data/mysql/db_revs/220600072.pl going the same direction),
fix the three query strings that build the printfeercpt.pl URL to
send change= instead of change_given=. The change_given variable name
is kept everywhere else (form fields, JS locals, Perl params) since
this is purely about the query key at the printfeercpt.pl boundary.

Test plan:
1) Set FinePaymentAutoPopup to 'show'
2) Take a payment for a patron via members/pay.pl or members/paycollect.pl
   that generates change (fine amount not evenly covered by cash tendered)
3) Note the auto-popped-up receipt shows the change amount
   (before this patch, the Change line was always blank)
4) Repeat via the boraccount.pl > printfeercpt.pl print receipt route

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:41 +00:00
Jonathan Druart 7bba6e855a
Bug 42697: Add Cypress tests
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:40 +00:00
Jonathan Druart ab1a20b3c8
Bug 42697: Revert "Bug 39011: Apply the pre-process to column filters"
This reverts commit 4fcff9db9e.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:39 +00:00
Lawrence O'Regan-Lloyd c893f47bb3
Bug 41445: Fix cleanup after single hold is suspended
This fixes information displayed in the holds table for a patron after a single hold is suspended (in the "Check out" and "Details" sections for a patron record in the staff interface).

After suspending a hold, no holds are selected in the holds table, the select_hold_all checkbox is unchecked, and the holds select badge counter is 0.

Test plan:

0. Apply patch
1. Go to a patron with 2 or more active holds
2. Navigate to Details -> Holds tab
3. Click Suspend on a single hold and confirm
4. Verify the select-all checkbox remains unchecked
5. Verify the selected holds count is set to 0
6. Repeat steps 3-5 from the Check out -> Holds tab

Assisted-by: Opus 5 (Anthropic)
Sponsored-by: CLAMS
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-25 12:08:35 +00:00
Nick Clemens 1b408f7bb2
Bug 36549: (QA follow-up) Restore sleep
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-24 15:48:32 +00:00
Nick Clemens 6b7db3cfb4
Bug 36549: Use Parallel::ForkManager in es_indexer_daemon
This patch copies the implementation from the general background
worked into the es_indexer daemon

Note: There is a 'sleep(10)' in the general indexer and I copied it here,
but I don't know why we do it?

To test:
1 - Apply the patch
2 - sudo koha-es-indexer --restart kohadev
3 - perl misc/maintenance/touch_all_biblios.pl
4 - Wait a bit
5 - Check the background jobs in the admin interface
6 - Confirm the jobs finish and records are searchable
7 - Test with and without rabbitmq enabled:
    . Stop rabbitmq service: sudo -s service rabbitmq-server stop
    . Repeat steps 2 to 5
8 - Sign off

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Aleisha Amohia <aleisha@catalyst.net.nz>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-24 15:48:31 +00:00
Lawrence O'Regan-Lloyd 7a5c6b6066
Bug 43513: Add .mailmap entries for Lawrence O'Regan-Lloyd
This patch adds two entries mapping both addresses I commit from to a single
identity, so the generated files pick up the right name the next time
they are regenerated.

Test plan:
1. Apply patch
2. Run: git check-mailmap "lawrenceol-clams <loreganlloyd@clamsnet.org>"
=> SUCCESS: returns Lawrence O'Regan-Lloyd <loreganlloyd@clamsnet.org>
3. Run: git check-mailmap "lawrenceol <hello@lawrenceol.dev>"
=> SUCCESS: returns Lawrence O'Regan-Lloyd <loreganlloyd@clamsnet.org>
4. Run: git shortlog -sne | grep -i "regan-lloyd"
=> SUCCESS: a single entry, not two

Assisted-By: Claude Opus 5 (Anthropic)
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-24 15:48:29 +00:00
Casey Conlin fb98e7c809
Bug 43591: Bring back my libraries filter.
This is a patch to bring back a small UI component that was dropped on the previously QA’ed and merged work in bug 16631.
Before applying patch, confirm the filter option for Show reports is not available on the Saved Reports page
1. Pull the latest from Main
2. Enable  LimitReportsByLibrary under Reports in System Prefernces
3. Visit the Save reports page
4. There should be NO Show reports with selectbox in the Filter pane on the left side
5. Implement the patch
6. Reload Save reports page
7. In the Filter pane on the left side, there should be a Show reports select box with options for Show my library reports and Show all reports options

Confirm only permissioned users see this option and reports are filtered
8. Create a staff user with the all reports permissions including Manage Report Limits (manage_report_limits). Home library should be Centerville.
9. Create a staff user with all reports permission but NOT Manage Report Limits (manage_report_limits). Home library should be Centerville.
10. Create a report, “No limit”  with no limit.
11. Create a report, “Centerville limit” limited to Centerville.
12. Create a report “Fairfield limit” limited to Fairfield.
13. Log in as the permissioned user.
14. Confirm the filter is available and that it filters reports so that only the No limit and Centerville limit reports show when My library reports only is selected.
15.Select All reports and all 3 reports should be in the list along with the Fairfield report .
16. Log in as the unpermissioned user.
17. The filter option for my / all reports should not be in the leftside pane.
18. Only the Centerville limit and No limit reports should be visible

Signed-off-by: Anna Stalter <staltera@hamiltoneastpl.org>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-24 15:48:28 +00:00
Owen Leonard b09bc3c948
Bug 43519: Direct links to article request form still work when the feature is disabled
This patch adds a check for the ArticleRequests system preference in
opac-request-article.pl so that the page will return a 404 error if the
preference is disabled.

To test, apply the patch and restart services.

- If necessary, enable the ArticleRequests system preference and
  configure a circulation rule to allow article requests.
- Locate a bibliographic record in the OPAC which will accept an article
  request. Note the biblionumber of the record you select.
  - Click "Request article" and complete the process. It should finish
    successfully.
- Disable the ArticleRequests system preference.
- Return to the OPAC and navigate directly to the page for requesting an
  article:
  http://localhost:8080/cgi-bin/koha/opac-request-article.pl?biblionumber=X
  ...where X is the biblionumber you noted before.
  - This should trigger a 404 error.

Sponsored-by: Athens County Public Libraries

Signed-off-by: Bastien CHAUVET <bastien.chauvet@inlibro.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-24 15:48:25 +00:00
Owen Leonard 04314c4fbc
Bug 38948: Modal error on some pages: Can't access property "backdrop"
This patch adds some checks to cookieconsent.js to avoid errors in the
console: If we try to access 'document.getElementById("cookieConsentModal")'
when the element doesn't exist there will be an error:

'Uncaught TypeError: can't access property "backdrop", this._config is
undefined'

The error can be seen in the browser console on the Circulation home
page and the Tools home page when the CookieConsent preference is
enabled.

To test apply the patch and clear your browser cache if necessary.

- Enable the CoookieConsent system preference.
- Go to the Circulation home page. There should be no error in the
  console.
- Clear your browser's local storage to forget your consent.
- Go to the staff client home page. The cookie consent message should
  appear.

Sponsored-by: Athens County Public Libraries
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-24 11:48:44 +00:00
Paul Derscheid 9e0b6c3914
Bug 42013: Fix missing confirmation dialog for "Remove all reserves"
- The confirmDelete dialog for removing all reserves from a course
  was bound as a click handler on the #rm_items form element, which
  is never directly clicked by the user
- The #rm_items_button click handler called .submit() on the form
  without checking confirmDelete first, bypassing the confirmation
- Move the confirmDelete check into the #rm_items_button click
  handler and remove the dead #rm_items click handler

Test plan:
- Enable UseCourseReserves system preference (should be enabled in ktd)
- Create a course (or use the existing one) and add at least one reserve
  to it
- On the course details page, click "Remove all reserves"
- Observe the form submits immediately without confirmation
- Apply patch
- Repeat the steps above
- Verify a confirmation dialog now appears
- Click cancel and verify the reserves are not removed
- Click "Remove all reserves" again, confirm, and verify
  the reserves are removed

Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-24 11:48:43 +00:00
Lucas Gass cd3574bdda
Bug 43383: Use checkbox status of :checked when doing cartsubmit or listsubmit
To test:
1. Do a catalog search that brings up multiple results
2. Check the checkbox of in the left column for the first two results
3. Click on the title of the 3rd result to take you to the details page for that item
4. Use the borwser's back button to return to the search results
5. Notice the first 2 titles are still checked.
6. Now check the 3rd result and click 'Add to list'
7. Only the 3rd title is added, even though all 3 are checked.
8. Try steps 1 - 7 again but with 'Add to cart', same behavior.
9. Try setps 1 - 7 again but with 'Place hold', all 3 items are selected.
10. APPLY PATCH, clear browser cache and run through all the steps again. This time your selction should be rememebered.

Bonus points for trying in as many browsers as possible.

Signed-off-by: Bastien CHAUVET <bastien.chauvet@inlibro.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-24 11:48:41 +00:00
Martin Renvoize 3b24981b89
Bug 42608: (follow-up) Adapt tests to Bug 42310 calendar schema
Bug 42310 landed on main after this patch's tests were written. It
renamed Koha::Calendar to Koha::Library::Calendar and normalized the
legacy repeatable_holidays/special_holidays tables into
library_weekly_closures/library_single_closures (plus repeating
closures and exceptions), managed via Koha::Library::Calendar::*
DBIC-backed classes.

The new subtests added for this bug still referenced the old class
name and the dropped DBIC sources (RepeatableHoliday, SpecialHoliday),
so t/db_dependent/Calendar.t and t/db_dependent/Hold.t failed after
rebasing onto current main. Update both to build closures via
Koha::Library::Calendar::WeeklyClosures/SingleClosures and to
instantiate Koha::Library::Calendar, matching the convention already
used elsewhere in these files since the Bug 42310 follow-up.

No production code changes; test-only fix.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-24 11:48:40 +00:00
Martin Renvoize 58d41bfedf
Bug 42608: Stop hold pickup expiration from week-jumping over closures
Koha::Hold::set_waiting calls Koha::Calendar::days_forward to derive the
holds-shelf expiration date when ExcludeHolidaysFromMaxPickUpDelay is on.
days_forward iterated next_open_days($dt, 1) ReservesMaxPickUpDelay times,
and next_open_days consults get_push_amt for every closed day it
encounters. Under useDaysMode=Dayweek, get_push_amt returns 7 for any
closure that is not a permanently closed weekday -- so an intermediate
Bank Holiday Monday inside the pickup window pushed the cursor a whole
week forward instead of one day, inflating the expiration by seven days.

Dayweek is a loan-due-date concept: it exists to keep a Friday-due loan
due on a Friday across closures. A patron does not pick a weekday for a
hold-shelf collection; staff set the expiration when they trap the item,
and the only thing that should drive it is how many open days the patron
should have to come and collect. days_forward is now decoupled from
useDaysMode and always counts forward one open day at a time, ignoring
closures without consuming them from the count.

next_open_days, prev_open_days, get_push_amt and addDays are unchanged --
loan due-date math keeps the existing Dayweek behaviour.

Test plan:
1. Inside KTD, run the updated unit tests:

     prove t/db_dependent/Calendar.t
     prove t/db_dependent/Hold.t

   Confirm both pass, including the new Bug 42608 sub-tests. These cover
   the full matrix of starting weekdays and useDaysMode values; the
   manual steps below are a single confirmatory pass through the UI.

2. Manual reproduction (UI only -- no database or CLI access needed):
   - Set sysprefs: useDaysMode=Dayweek, ReservesMaxPickUpDelay=6,
     ExcludeHolidaysFromMaxPickUpDelay=ON, ExpireReservesOnHolidays=ON.
   - Administration > Calendar: on a test branch, add one single
     (one-time) holiday -- not weekly/repeating -- on any weekday within
     the next 6 days that isn't already closed (e.g. the day after
     tomorrow, as long as it isn't a Sunday or other closed day). This
     one-off closure inside the pickup window is what triggers the bug.
   - Place a hold on an available item, then check the item in at that
     branch and confirm/trap it. This stamps waitingdate with today's
     date automatically -- no manual date entry required.
   - Check the hold's expiration date (Holds awaiting pickup list, or
     the patron's holds tab):
       Without the fix: expiration lands a full 7 days later than it
       should (today + 6 + 7), from the week-jump.
       With the fix: expiration is today + 6 open days, only skipping
       the one closed day.

3. Verify the existing Calendar mode behaviour is unchanged:
   - Switch useDaysMode to Calendar and repeat step 2 -- the same
     expected expiration date should apply.

4. Verify loan due-date behaviour is unchanged:
   - With useDaysMode=Dayweek, check out an item whose loan period would
     place its due date on the same closed day used in step 2. Confirm
     the due date still rolls forward by a whole week (the original
     Dayweek intent), proving this patch did not regress
     addDays/addDuration.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-24 11:48:37 +00:00
Owen Leonard 9c3d3306e3
Bug 43237: (follow-up) Adding [% PROCESS 'i18n.inc' %] to two templates which lack it
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 16:07:52 +00:00
Owen Leonard 3d0b88df00
Bug 43237: Add 'Expand' column header to all OPAC responsive DataTables
This patch updates several templates in the OPAC so that when a
responsive DataTable is partially collapsed, the column with the "+"
buttons has "Expand" in the column header.

To test, apply the patch and test tables on the affected pages,
narrowing the page enough to confirm that when the table collapses, the
last column has "Expand" in the header.:

- Authorities search results
- The details of a course reserve (with titles on reserve)
- The holdings table on the bibliographic detail page
- Search history
- Most popular

As a logged-in user:
- User summary page (all tabs)
- Curbside pickups (with at least one curbside pickup scheduled)
- Checkout history
- Suggestions
- Tags

In self-checkout:
- The checkouts, holds, and charges tabs.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 16:07:51 +00:00
Pedro Amorim d486b1c68b
Bug 43336: DBRev 26.06.00.028
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 16:07:50 +00:00
Magnus Enger 6d160313a2
Bug 43336: Remove support for LibrisSpellcheck
The API that this feature was based on was removed by the Swedish
National Library, so we can remove the code and sysprefs from Koha.

Before testing:
- Run this SQL in the database and verify the syspref is empty:
  SELECT value FROM systempreferences WHERE variable = 'OPACdidyoumean';
- Go to Administration > Did you mean? and verify there is a box for
  LibrisSpellcheck
- Move the boxes around and click on "Save configuration"
- Run the SQL again and verify the contents of the syspref are now
  something like this:
  [{ "name": "AuthorityFile"}, { "name": "LibrisSpellcheck"}, { "name": "ExplodedTerms"}]
- Go to Administration > System preferences and search for "libris"
- Verify the two sysprefs LibrisKey and LibrisURL show up

To test:
- Apply this patch and make sure the DB update is run. In KTD you can
  do:
  $ sudo koha-upgrade-schema kohadev
  The output from this should be like this:
	Removed system preference 'LibrisKey'
	Removed system preference 'LibrisURL'
	'LibrisSpellcheck' was removed from the OPACdidyoumean syspref
- Run koha-upgrade-schema again. The output should be:
	'LibrisKey' syspref already removed
	'LibrisURL' syspref already removed
	'OPACdidyoumean' syspref does not contain a setting for 'LibrisSpellcheck'
- Run the same SQL as before and verify that LibrisSpellcheck is now
  gone:
  [{"name":"AuthorityFile"},{"name":"ExplodedTerms"}]
- Go to Administration > Did you mean? and verify there is no box
  for LibrisSpellcheck
- Move the boxes around and click on "Save configuration"
- Run the SQL again and verify that there is still no mention of
  LibrisSpellcheck
- Go to Administration > System preferences and search for "libris"
- Verify the two sysprefs LibrisKey and LibrisURL do NOT show up
- Empty the OPACdidyoumean syspref by running this SQL:
  UPDATE systempreferences SET value = '' WHERE variable = 'OPACdidyoumean';
- Run koha-upgrade-schema again. The output should be:
	'LibrisKey' syspref already removed
	'LibrisURL' syspref already removed
	'OPACdidyoumean' is not set

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 16:07:48 +00:00
Jonathan Druart 632017e022
Bug 43267: Avoid flashing the wrong number of visible records
Wait before DT has finished the init to display the number of visible
records, otherwise there is a flash (display the count of the full set,
then the number of records displayed (20));

Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 16:07:47 +00:00
Jonathan Druart 83f17afef6
Bug 43267: Disable "Batch operations" menu when there's nothing to send
So we don't send an empty set to the tool

Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 16:07:46 +00:00
Jonathan Druart 26914173fd
Bug 43267: Reports - only send visible records to batch operations from DT view
When opening report results in the new DataTables view, batch
operations should only send visible records. Same as the normal view.

Test plan:
1. Create and run a report that will display more resulsts than fit on
   one page
2. Click "Open in DataTables".
3. Note "Batch operations with X visible records" shows the default
   page size (=20), and open the "Batch operations" dropdown to
   send results to e.g. batch item modification or batch record
   modification.
4. Confirm only the values on the currently visible page are sent, not
   the entire set.
5. Change the DataTable's page, or filter/search within it, and repeat
=> Confirm the count and the sent values update to the number of visible records
6. Confirm the standard (non-DataTables) report view is unaffected.

Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 16:07:45 +00:00
Chloé Zermatten 18aa5d1f4c
Bug 10190: (QA follow-up): fixes phase six
1. add missing <tr> wrapper element
2. amend casing in cypress test
3. change method name so it is codespell-friendly
4. remove deprecated test from TestBuilder.t
   (used Overduerules which do not exist anymore.
    rely on 'Tests with composite FK in userpermission'
    for FK test coverage instead -> no replacement
    introduced.)
5. 'Bug 10190: Fix patron_categories permission format'
   changed the permission for GET requests to patron_categories
   to catalogue = 1, matching the Koha precedent for such
   endpoints. However, patron_categories still tested the
   endpoint with the old permission assigned. Amend this to
   complete the work started in that commit.
6. remove redundant delete statement in permission test:
   if the test fails, this means the db update has not
   run, and should. It would be incorrect for the db
   to still include the permission at the point where the
   test runs.

Test plan:

1. run `npm run js:build` (or watch the js:watch output)
2. run t/cypress/integration/Admin/CirculationTriggers_spec.ts
3. run prove xt/author/codespell.t
4. run prove t/db_dependent/TestBuilder.t
5. run prove t/db_dependent/api/v1/patron_categories.t
6. run prove t/Koha/Auth/Permissions.t

All should run with no warnings or errors.

Assisted-by: Claude Opus 5 (Anthropic)
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 16:07:44 +00:00
Chloé Zermatten c96618f064
Bug 10190: (QA follow-up): fixes phase five
1. apply html filter to letters so notice names are escaped (must
    be done in attribute so the JSON format of the letters can
    be parsed).
2. fix a correctness issue where the frontend was pinning the
   logged in library instead of the logged in user's home library,
   which would have led to 403s (correct) when saving rules. Since
   the library dropdown is deliberately disabled unless a user has
   the manage_circ_rules_from_any_libraries permission, the user
   would then have needed to use 'Set library' to match their home
   library to fix this. Prevent the issue by setting the library
   to the user's home library.
   Also rename the variable to user_library_id.
3. list_rules correctly references /api/v1/circulation_rules/kinds
4. make untranslatable strings translatable
5. swap svg for font-awesome icon.
6. avoid css duplication:
   - remove dead / unused scoped css (leftover from an early component
     extraction)
   - import vue.css and only leave the minimal override required.
7. improve set_rules test coverage: add tests for manage_circ_rules
   / manage_circ_triggers separation

Test plan:

1) Administration > Circulation triggers: notice the 'Letter' dropdowns
   list circulation notices as "name (code)" and offer 'No letter'.
   (no regression)
2) Give a staff user 'catalogue' + 'manage_circ_triggers' only, with
   home library A. Log in as them, 'Set library' to B, and open
   Administration > Circulation triggers. Notice the view is pinned to
   A, and that adding and editing a trigger saves without a 403.
   (behaviour is now correct)
3) GET /api/v1/circulation_rules?rules=nosuchrule - notice the error
   points at /api/v1/circulation_rules/kinds.
   (behaviour is now correct)
4) Run 'gulp po:update --lang fr-FR'. Notice 'Add new trigger
   {triggerNumber}', 'No letter' and 'any' now appear in
   misc/translator/po/fr-FR-messages-js.po
   (behaviour is now correct)
5) Edit a trigger. Notice the delay's reset button is an 'x' inside the
   input, left of the chevrons, and clears the delay; the letter's
   reset button does the same for the notice.
   (no regression)
6) Notice no visual regression across the list, add, edit, reset and
   delete screens - select widths, chevrons and modals unchanged.

Also run t/db_dependent/api/v1/circulation_rules.t

Assisted-by: Claude Opus 5 (Anthropic)
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 16:07:39 +00:00
Martin Renvoize 167622c08d
Bug 40934: (follow-up) Fix false positive on --code/--exclude-code mutual exclusivity
The mutual-exclusivity check ran before the empty-element cleanup added
for bug 37075, so a bare -c (no value) left an empty string in
@letter_code, which the check saw as truthy and rejected even though
-c had effectively not been used. Move the cleanup above the check.

Also add the same mutual-exclusivity guard directly in
C4::Letters::SendQueuedMessages, since letter_code and
exclude_letter_code build the same search key and passing both would
otherwise let one silently clobber the other in the hashref passed to
->search - only the CLI script enforced this until now, so any other
caller could hit the silent-clobber bug.

Adds regression coverage: unit tests for the new exception in
SendQueuedMessages, and a script-level test reproducing the RM's
reported false positive.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:49 +00:00
Nick Clemens 2a652a8d12
Bug 40934: (QA follow-up) Don't delete the queue
We already delete the queue at the start of all tests and the tests
here are checking using ID, there is no reason to delete the queue again

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:48 +00:00
Martin Renvoize e6908ef0a7
Bug 40934: (follow-up) --code and --exclude-code should be mutually exclusive
Both options were being accepted but only the last one in the command line
was being applied. Now they are mutually exclusive like --category and
--skip-category in longoverdue.pl.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:46 +00:00
Martin Renvoize 3430793f07
Bug 40934: Add --exclude-code option to process_message_queue.pl
Some libraries want to send digest messages once per day (e.g., at 20:05)
but send all other messages every hour. Currently, this requires listing
almost all letter codes (~85) in the hourly cron job, which is error-prone
and makes it easy to forget notifications at upgrades.

This patch adds a new --exclude-code (-x) option to process_message_queue.pl
that allows excluding specific letter codes from processing, making it much
easier to configure digest scheduling.

Changes:
- Added --exclude-code option to process_message_queue.pl (repeatable)
- Modified C4::Letters::SendQueuedMessages to handle exclude_letter_code parameter
- Supports both scalar and array reference formats

Example usage:
  # Send all messages except digests every hour
  00 */1 * * * process_message_queue.pl --exclude-code DUEDGST \
    --exclude-code PREDUEDGST --exclude-code HOLDDGST \
    --exclude-code AUTO_RENEWALS_DGST

  # Send all messages including digests once per day
  05 20 * * * process_message_queue.pl

Test plan:
1. Apply both patches
2. Run: ktd --shell --run 'prove t/db_dependent/Letters.t'
3. Verify all tests pass (should show 105 tests passing)
4. Create test messages in message_queue with different letter codes:
   - Insert messages with codes: DUEDGST, PREDUEDGST, ACQ_NOTIF, HOLD
5. Test single exclusion:
   misc/cronjobs/process_message_queue.pl --exclude-code DUEDGST
   - Verify DUEDGST messages remain pending
   - Verify other messages are processed
6. Test multiple exclusions:
   misc/cronjobs/process_message_queue.pl --exclude-code DUEDGST \
     --exclude-code PREDUEDGST
   - Verify both digest messages remain pending
   - Verify non-digest messages are processed
7. Test combined with --code option (should work together):
   misc/cronjobs/process_message_queue.pl --code ACQ_NOTIF \
     --exclude-code DUEDGST
   - Verify only ACQ_NOTIF messages processed (DUEDGST excluded anyway)
8. Test help output:
   misc/cronjobs/process_message_queue.pl --help
   - Verify --exclude-code option is documented

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Marion Durand <marion.durand@biblibre.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:45 +00:00
Martin Renvoize 2bf60ebf21
Bug 40934: Add tests for exclude_letter_code in process_message_queue.pl
This patch adds unit tests for the new exclude_letter_code functionality
in SendQueuedMessages, which will allow excluding specific letter codes
when processing the message queue.

Test coverage includes:
- Excluding a single letter code (array format)
- Excluding multiple letter codes
- Excluding with scalar parameter format

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Marion Durand <marion.durand@biblibre.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:44 +00:00
Slava Shishkin 0dfcddf383
Bug 43296: (follow-up) Fix perltidy
This follow-up fixes the formatting issue reported by xt/perltidy.t.

Test plan:
1. Run prove xt/perltidy.t
2. Confirm it passes.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:43 +00:00
Pedro Amorim 18469d46c4
Bug 42667: DBRev 26.06.00.027
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:42 +00:00
Lucas Gass 5694e63a8c
Bug 42667: (follow-up) Fix edit button inside of preview SQL modal
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:40 +00:00
Lucas Gass 3d7b5c5a95
Bug 42667: (follow-up) Update dropdown so users can only choose to edit own report
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:39 +00:00
Martin Renvoize 51d1fe9498
Bug 42667: (QA follow-up) Test edit permission on ownerless reports
Add two test cases covering reports with no borrowernumber (NULL owner):
a patron without edit_all_reports should be denied, one with the
permission should succeed. The POD documents this case but it was
not previously exercised.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:38 +00:00
Martin Renvoize 5da8ba1ce9
Bug 42667: (QA follow-up) Fix null guard and exception description
- Add null check around Koha::Patrons->find() in guided_reports.pl
  before calling ->has_permission, matching the defensive pattern
  already used in Koha::Report::check_edit_permission
- Add missing description to Koha::Exceptions::Report base class
  for consistency with its sub-exceptions

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:37 +00:00
Kyle M Hall 5b5c875df9
Bug 42667: Enforce permission in Koha::Report
Prevents possible template bugs from allowing editing
of reports by others without the proper permissions.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:36 +00:00
Kyle M Hall 1da4dcec00
Bug 42667: Hide edit controls as needed
Test plan:
1) Apply this patch set
2) Run updatedatabase.pl
3) Restart all the things!
4) Create 2 users with create_reports and not edit_all_reports
5) Create reports with each
6) Note that each cannot edit the others reports
7) Create a user with create_reports and edit_all_reports
8) Note the this user can edit reports from the previous two users

Edit button placements to look for:
* Reports table row
* Actions menu
* Toolbar

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:34 +00:00
Kyle M Hall df282a5f89
Bug 42667: Require edit_all_reports to edit other librarian's reports
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:33 +00:00
Kyle M Hall d2fa6156fd
Bug 42667: Add new edit_all_reports permission
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:56:29 +00:00
Jonathan Druart 08a4e352af
Bug 42330: (follow-up) Tidy
Something very messy happened during the last rebase before push.
Sorry!

I've restored the original version:
git rebase -i HEAD~258
Edit commit b02ca1270d and remove the next one (Tidy)
perl misc/devel/tidy.pl $(git diff --name-only HEAD~1)
git commit -a
git rebase --continue
Luckily there was no conflict!

I then picked the version of those files and here is the commit.

Use this to confirm:
`git show -w ad694a3411e677fcea96f315758793fd1fe0f203`
You should see changes that are not related to tidiness

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:11:34 +00:00
Martin Renvoize dfea147183
Bug 42385: (QA follow-up) Test PickupLibrary fall-through to ItemHomeLibrary
The original _GetCircControlBranch tests covered the case where CircControl
is PickupLibrary but no userenv branch is available, in which case the
resolution falls through to the ItemHomeLibrary behaviour. That coverage was
lost when the tests moved to t/db_dependent/Koha/Policy/Circulation.t, since
the PickupLibrary subtest always mocks a userenv.

This restores coverage for the fall-through path: with CircControl set to
PickupLibrary, no userenv, and no pickup_library_id, the controlling library
is resolved from the item's home library.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/Koha/Policy/Circulation.t
=> SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
(cherry picked from commit 6a13c6e5066384e59e52f0d48f5a430958671ef2)
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:11:33 +00:00
Tomás Cohen Arazi 275d870be7
Bug 42385: Remove _GetCircControlBranch
Removes the deprecated _GetCircControlBranch function and its export
from C4::Circulation. Removes the corresponding tests from
Circulation.t (now covered by t/db_dependent/Koha/Policy/Circulation.t).

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/Circulation.t \
          t/db_dependent/Koha/Policy/Circulation.t
=> SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
(cherry picked from commit 8e784622815a2db2ce6f558987d7c0b0951ceec0)
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>

# Conflicts:
#	C4/Circulation.pm
#	t/db_dependent/Circulation.t
2026-09-21 11:11:32 +00:00
Tomás Cohen Arazi 4f1b98160e
Bug 42385: Use Koha::Policy::Circulation everywhere
Replaces all calls to _GetCircControlBranch with
Koha::Policy::Circulation->circ_control_library in:
- C4::Circulation (10 call sites)
- Koha::Item (1 call site)
- Koha::Recall (1 call site)
- reserve/request.pl (1 call site)
- svc/letters/preview (2 call sites)

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/Koha/Policy/Circulation.t \
          t/db_dependent/Circulation.t \
          t/db_dependent/Koha/Items.t \
          t/db_dependent/Koha/Recall.t
=> SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:11:30 +00:00
Tomás Cohen Arazi 5c833281ed
Bug 42385: Add Koha::Policy::Circulation
Adds Koha::Policy::Circulation->circ_control_library($item, $patron)
class method that resolves which library controls circulation rules,
based on the CircControl and HomeOrHoldingBranch system preferences.

This follows the same pattern as Koha::Policy::Holds->holds_control_library.

An optional pickup_library_id parameter allows callers in API/daemon
contexts to pass the library explicitly instead of relying on userenv.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/Koha/Policy/Circulation.t
=> SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:11:29 +00:00
Martin Renvoize 255eeaf4f6
Bug 43375: (QA follow-up) Add test coverage for form-data fallback in userinfo parsing
_get_data_and_patron() falls back to Mojo::Parameters form-data
parsing whenever $tx->res->json fails to decode the userinfo
response body. That branch was untested: all existing userinfo_url
subtests mock json() to return a hashref, so the fallback path was
never actually exercised.

This adds a subtest that mocks a genuine url-encoded response body
(json() returning undef) and confirms the fallback still maps data
and finds the matching patron correctly.

Test plan:
1. Run:
   $ ktd --shell
  k$ prove t/db_dependent/Koha/Auth/Client/OAuth.t
=> SUCCESS: All 6 subtests pass, including the new fallback-path test
2. Sign off

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:11:28 +00:00
Tomás Cohen Arazi 8b1e8f80ba
Bug 43375: Use Mojo json() with form-data fallback for userinfo parsing
This patch simplifies the userinfo response parsing by relying on
Mojo::Message::Response->json which decodes JSON regardless of
Content-Type header parameters. If JSON decoding fails (returns undef),
it falls back to URL-encoded form data parsing.

This fixes integration with APIs that return extra Content-Type
parameters (e.g. Microsoft Graph returns odata.metadata,
odata.streaming, and IEEE754Compatible alongside charset) which
previously caused the strict regex to fail and silently misparse
the response as form data.

Test plan:
1. Apply the regression test (previous patch)
2. Run:
   $ ktd --shell
  k$ prove t/db_dependent/Koha/Auth/Client/OAuth.t
=> FAIL: Subtest fails - Content-Type with extra params not recognized
3. Apply this patch
4. Repeat step 2
=> SUCCESS: All tests pass
5. Sign off :-D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:11:27 +00:00
Tomás Cohen Arazi 63ffc0b28f
Bug 43375: Regression tests
This patch adds a regression test demonstrating that the OAuth
userinfo_url response parsing fails when the Content-Type header
contains extra parameters beyond charset (e.g. Microsoft Graph API
returns application/json;odata.metadata=minimal;odata.streaming=true).

The strict regex in _get_data_and_patron only matches Content-Type
values with an optional ;charset=... suffix, causing valid JSON
responses to be silently misinterpreted as URL-encoded form data.

Test plan:
1. Apply this patch
2. Run:
   $ ktd --shell
  k$ prove t/db_dependent/Koha/Auth/Client/OAuth.t
=> FAIL: New subtest fails - email is undef, patron not found
3. Sign off :-D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:11:26 +00:00
Olivia Reynolds 6975b6616b
Bug 30319: Remove appropriate hidden fields on self-modification
Previously, the system preference
PatronSelfRegistrationBorrowerUnwantedField was used to remove hidden
fields regardless of whether the operation was self-registration or
self-modification. This patch uses the appropriate set of hidden fields
depending on the current operation (create/update).

Test plan:
1.  Set OPACPatronDetails to "Allow".
2.  Ensure PatronSelfModificationBorrowerUnwantedField is empty.
3.  Set PatronSelfRegistrationBorrowerUnwantedField to exclude
    Title (towards the bottom right of the modal).
4.  Log into the OPAC, and navigate to Your account > Personal details.
5.  Change your title and submit the request.
6.  Verify you see the notice "No changes were made." at the top of the
    page.
7.  Apply patch.
8.  Change your title again and submit the request.
9.  Verify the request is submitted successfully.
10. Return to the personal details screen, but don't submit.
11. Set PatronSelfModificationBorrowerUnwantedField to exclude Title.
12. On the preloaded personal details screen, update the title and
    submit.
13. Verify you see the notice "No changes were made." at the top of the
    page.

Sponsored-by: Open Fifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-21 11:11:22 +00:00
Baptiste Wojtkowski 0fc43dd5a3
Bug 41742: (follow-up) Add pod to AddBudgetPeriod
Create a pod coverage for AddBudgetPeriod

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-16 14:42:14 +00:00
Olivia Reynolds 0ae6c196ec
Bug 39337: (follow-up) Mock OpacSuppression in Output tests
Before the refactoring in this bug, redirect_if_opac_suppressed was
intended to be called after checking OpacSuppression was enabled. Now
it performs this check internally, so to adapt these unit tests
correspondingly, we mock OpacSuppression to be enabled, as was the
implied state before.

Test plan:
1. Prove t/db_dependent/Output.t and verify all tests pass.

Sponsored-by: Open Fifth
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-16 13:10:56 +00:00
Pedro Amorim bb05ef7ce8
Bug 42310: DBRev 26.06.00.026
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:18 +00:00
Pedro Amorim 1b903bc3c5
Bug 42310: (RM follow-up) Refresh stale DBIC checksum on Koha::Schema::Result::Branch
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:17 +00:00
Tomás Cohen Arazi 76d156d4fc
Bug 42310: (QA follow-up) Clean up stale references to dropped calendar tables
- cleanup_database.pl: rewrite DeleteSpecialHolidays to purge the new
  library_single_closures and library_closure_exceptions tables via their
  Koha::Objects classes (was querying the dropped special_holidays table).
- kohastructure.sql: fix the comment header above library_repeating_closures
  that still referred to repeatable_holidays.
- C4::Overdues: drop the dead GetSpecialHolidays / GetRepeatableHolidays /
  GetWdayFromItemnumber routines (unreferenced, exported nowhere) that still
  queried the dropped special_holidays / repeatable_holidays tables, along
  with the now-unused Date::Calc and Date::Manip imports.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:15 +00:00
Tomás Cohen Arazi 51fd136eb1
Bug 42310: (follow-up) Retire t/db_dependent/Holidays.t
t/db_dependent/Holidays.t was a legacy file whose coverage is now
provided by the two dedicated calendar test files. Its calendar
behaviour assertions (is_holiday for weekly/day-month/single holidays)
are already exercised by t/db_dependent/Calendar.t and
t/db_dependent/Koha/Library/Calendar.t. The remaining unique cases
queried the special_holidays / repeatable_holidays tables directly,
which this bug drops during the migration to the
Koha::Library::Calendar::*Closure schema, so the file could no longer
run.

This commit folds the unique coverage into
t/db_dependent/Koha/Library/Calendar.t, rewritten against the new
closure objects/API (no raw legacy-table SQL):

- is_holiday DST edge case (America/Santiago days_between)
- next_open_days / prev_open_days throwing NoOpenDays when the library
  is never open, and finding an open day created via an exception
- copy_to completeness: repeating-closure copy, exception copy, and
  exclusion of past-dated single closures and exceptions, plus the
  no-duplication guarantee for all four closure types

and then deletes t/db_dependent/Holidays.t.

Test plan:
- prove t/db_dependent/Koha/Library/Calendar.t
- prove t/db_dependent/Calendar.t

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:14 +00:00
Tomás Cohen Arazi 755521ba0f
Bug 42310: (follow-up) Make 16.12.00.032 upgrade self-contained
The historical 16.12.00.032 upgrade step (Bug 12063) instantiated
Koha::Calendar to push reserves.expirationdate forward by the max
pickup delay in open days. Depending on a live application class from a
frozen upgrade step is fragile: when this bug renamed and removed
Koha::Calendar, the migration had to be edited to chase the new
namespace.

Frozen updatedatabase.pl steps should be self-contained and prefer
direct DB access. Reimplement the holiday-aware date advance inline,
reading the legacy repeatable_holidays / special_holidays tables (which
still exist at this point in the upgrade path) directly, with no
dependency on any Koha::* calendar class. This keeps the step working
regardless of later refactors and removes the calendar-class reference
from updatedatabase.pl entirely.

The weekly / day-month / single-closure logic and exception (open
override) handling mirror the historical C4::Calendar semantics.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:13 +00:00
Tomás Cohen Arazi f4268951f0
Bug 42310: (follow-up) Fix POD coverage for Koha::Checkouts::_type
The koha-qa pod_coverage check flags 'sub _type has no pod coverage'
in Koha/Checkouts.pm. The POD block above the sub was headed '=head3
type' while the sub is named '_type', so the documentation was not
associated with the sub. Correct the heading to '=head3 _type' to
match, mirroring the adjacent object_class POD.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:11 +00:00
Martin Renvoize 134ce6aa7c
Bug 42310: (QA follow-up) Add test coverage for add_* cache invalidation, empty calendars, and leap-year repeating closures
Coverage gaps identified during QA review:
- add_single_closure/add_exception were only ever tested against a cold
  cache; nothing proved they invalidate a cache already warmed with the
  stale answer before the add.
- No test exercised a library with no closures at all.
- No test exercised a repeating (day/month) closure on 29 February across
  a leap and a non-leap year.

Atomicupdate migration coverage is intentionally left out of scope.

Test plan:
1) prove t/db_dependent/Koha/Library/Calendar.t t/db_dependent/Calendar.t
   t/db_dependent/Holidays.t t/db_dependent/HoldsQueue.t

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:10 +00:00
Martin Renvoize 3ef756f5a0
Bug 42310: (QA follow-up) Refresh in-process closure state on add/delete_weekly_closure and add/delete_repeating_closure
add_weekly_closure/delete_weekly_closure and add_repeating_closure/
delete_repeating_closure returned $self without refreshing
$self->{weekly_closed_days}/{day_month_closed_days}, so a calendar object
would keep reporting the pre-change state to is_holiday (and everything
built on it - days_forward, next_open_days, etc.) until it was
re-instantiated. This is distinct from the _holidays Koha::Caches entry,
which single/exception closures already flush correctly on store/delete.

Call _init to rebuild both structures from the database after each
add/delete, so the same object reflects the change immediately.

Test plan:
1) prove t/db_dependent/Koha/Library/Calendar.t t/db_dependent/Calendar.t
   t/db_dependent/Holidays.t t/db_dependent/HoldsQueue.t
   t/db_dependent/Holds/WaitingReserves.t t/db_dependent/Koha/CurbsidePickups.t
   t/db_dependent/Reserves/CancelExpiredReserves.t
   t/db_dependent/api/v1/libraries.t

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:09 +00:00
Martin Renvoize 004fe4de21
Bug 42310: (QA follow-up) Reject in-place date changes on SingleClosure/Exception
store() only flushes the _holidays cache on insert, on the assumption that
updates only ever touch title/description. That assumption held for every
current caller (ModSingleholiday/ModExceptionholiday only ever change those
two fields), but nothing enforced it - a future caller updating date in
place would silently desync the cache for up to its TTL, with no error.

Make the assumption explicit: reject an update that changes date, rather
than silently allowing it. Callers that need to move a closure/exception to
a different date must delete and re-add.

Test plan:
1) prove t/db_dependent/Koha/Library/Calendar.t t/db_dependent/Calendar.t
   t/db_dependent/Holidays.t t/db_dependent/HoldsQueue.t

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:08 +00:00
Martin Renvoize 1399f8af42
Bug 42310: (QA follow-up) Restore weekday validation on weekly closures
C4::Calendar::insert_week_day_holiday used to croak "Invalid weekday
$weekday" unless $weekday =~ m/^[0-6]$/. That guard was dropped when the
insert delegated to Koha::Library::Calendar::WeeklyClosure, and nothing
replaced it - an out-of-range or undef weekday now persists silently to
library_weekly_closures instead of failing at the API boundary.

Add the check to WeeklyClosure->store() rather than the legacy wrapper, so
every caller (C4::Calendar, Koha::Library::Calendar->add_weekly_closure,
and any future REST/admin caller) gets it, not just the old entry point.

Test plan:
1) prove t/db_dependent/Koha/Library/Calendar.t t/db_dependent/Calendar.t
   t/db_dependent/Holidays.t t/db_dependent/HoldsQueue.t
   t/db_dependent/Holds/WaitingReserves.t

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:06 +00:00
Martin Renvoize d0645cc8b1
Bug 42310: (QA follow-up) Restore in-memory holiday cache sync in C4::Calendar
insert_single_holiday and insert_exception_holiday delegate to
Koha::Library::Calendar::SingleClosure/Exception, but stopped updating
$self->{single_holidays}/{exception_holidays} on the C4::Calendar object
itself, unlike the sibling insert_week_day_holiday/insert_day_month_holiday
methods which still do. isHoliday() reads exactly those hashes, so a date
just inserted via insert_single_holiday/insert_exception_holiday would read
back as "not a holiday" for the rest of that object's lifetime - reachable
from tools/newHolidays.pl and tools/exceptionHolidays.pl, which loop over
several dates on one $calendar object, checking isHoliday() before each
insert to skip duplicates.

While adding a regression test, also found that _init() has the same class
of bug on the DB-load path: it builds the single/exception lookup keys from
`split(/-/, $row->{date})`, which keeps the zero-padding from the DATE
string (e.g. "06"), while isHoliday() numifies its lookup key (e.g. "6").
Any single/exception holiday with a single-digit month or day was therefore
invisible to isHoliday() even on a freshly reloaded C4::Calendar object.
Both call sites now numify year/month/day before building the hash key.

Test plan:
1) prove t/db_dependent/Holidays.t t/db_dependent/Calendar.t
   t/db_dependent/Koha/Library/Calendar.t t/db_dependent/HoldsQueue.t

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:05 +00:00
Martin Renvoize e56aff8dfc
Bug 42310: (follow-up) Update Holidays.t for normalized calendar tables
The normalization commit dropped `repeatable_holidays` and
`special_holidays` but t/db_dependent/Holidays.t still referenced them
directly via raw SQL, so the test blew up on the first
`DELETE FROM special_holidays` with "Table doesn't exist". The earlier
namespace follow-up touched this file but only renamed Koha::Calendar
references, missing the SQL.

Replace the legacy table references with the four new normalized tables:

  repeatable_holidays (weekday set)    -> library_weekly_closures
  repeatable_holidays (weekday NULL)   -> library_repeating_closures
  special_holidays (isexception=0)     -> library_single_closures
  special_holidays (isexception=1)     -> library_closure_exceptions

- Two fixture-cleanup DELETEs now wipe all four new tables.
- The copy_to_branch 'no duplicates' check, previously two raw-SQL
  queries against the old two tables, becomes four queries against the
  new tables, keyed on each table's natural unique key
  (library_id+weekday / library_id+day+month / library_id+date).
- Plan for the copy_to_branch subtest bumps from 8 to 10 to cover the
  two extra duplicate-check assertions.

Test plan:
1. prove t/db_dependent/Holidays.t
2. All 15 top-level tests pass, including the 10 subtest assertions in
   copy_to_branch.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:03 +00:00
Martin Renvoize 811d8bfd37
Bug 42310: (follow-up) Move Koha::Calendar tree under Koha::Library::Calendar
Koha::Calendar is, by construction, a per-library object — it croaks
without a branchcode argument and every method is branch-scoped. The
top-level namespace hides that. Move the whole tree under
Koha::Library::Calendar to match the actual semantics, the existing
/libraries/{library_id}/calendar REST routes, and the established
Koha::Library::* convention (Group, Hour, FloatLimit, OverDriveInfo).

This commit:
- renames the Koha::Calendar(::*) tree to Koha::Library::Calendar(::*)
- updates package declarations, POD and internal 'use' statements
  within the moved tree
- updates the four DBIC Schema Result overrides (koha_object_class /
  koha_objects_class) to the new namespace
- updates callers inside C4::* modules (Calendar, Circulation,
  Reserves, Overdues, HoldsQueue), Koha::{Patron,Hold,Checkouts,
  CurbsidePickup}, circ/returns.pl, the installer and cronjobs
- updates the 42310-owned tests (Calendar tests, HoldsQueue.t,
  Holds/WaitingReserves.t, Holidays.t, Koha/CurbsidePickups.t,
  Reserves/CancelExpiredReserves.t and the libraries.t CRUD block)

Koha::Library->calendar, the REST controllers and the 42311 tests
are updated in separate follow-up commits under their respective
bugs.

Test plan:
- prove t/db_dependent/Koha/Library/Calendar.t
- prove t/db_dependent/Calendar.t
- prove t/db_dependent/Holidays.t t/db_dependent/HoldsQueue.t
- prove t/db_dependent/Holds/WaitingReserves.t
- prove t/db_dependent/Reserves/CancelExpiredReserves.t

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:02 +00:00
Martin Renvoize 7a8a220842
Bug 42310: (QA follow-up) Add closed_dates_in_range to Koha::Calendar
Introduces a public Koha::Calendar->closed_dates_in_range($from_dt,
$to_dt) method that returns an arrayref of ISO date strings representing
every closed day in the inclusive range, honouring weekly, annual
repeating, single and exception (open-override) closures.

This replaces callers that were reaching into Koha::Calendar private
state (_holidays, weekly_closed_days, day_month_closed_days) to
reconstruct the same logic. A follow-up under Bug 42206 converts the
REST controller; further callers can be migrated incrementally.

Test plan:
1. prove t/db_dependent/Koha/Calendar.t
2. Verify the new 'closed_dates_in_range' subtest passes

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:14:01 +00:00
Martin Renvoize 5875ce4c75
Bug 42310: (QA follow-up) Report duplicate rows skipped during migration
The new calendar tables add UNIQUE(library_id, weekday),
UNIQUE(library_id, day, month) and UNIQUE(library_id, date) constraints
that did not exist on repeatable_holidays / special_holidays, so the
INSERT IGNORE migration can silently drop legacy duplicate rows.

This patch compares source vs destination row counts for each of the
four migrations and emits a say_warning naming the constraint and
pointing the operator at their pre-upgrade backup so titles or
descriptions can be recovered if needed. Success lines now include
the migrated row count. DROP statements for the legacy tables are
guarded with TableExists and reported via say_success for
consistency with the rest of the atomicupdate.

Test plan:
1) Apply the update on a database containing legacy duplicates and
   verify the warning is emitted with the correct count.
2) Apply on a database with no duplicates and verify no warning
   appears.
3) Re-run the update and verify it is idempotent (no warnings, no
   re-migration).

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:59 +00:00
Martin Renvoize 548903ba02
Bug 42310: (QA follow-up) Harden Koha::Calendar->copy_to
Wrap the body in schema->txn_do so a failure partway through does not
leave the target library partially populated. Flush the target's
_holidays cache after the copy, and document the behaviour in POD:
weekly and repeating closures are always copied, but single closures
and exceptions are filtered to today-or-later dates by design.

Test plan:
1) prove t/db_dependent/Koha/Calendar.t
2) Confirm existing copy_to tests still pass.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:58 +00:00
Martin Renvoize c3880a70e6
Bug 42310: (QA follow-up) Fire per-row delete hooks in Koha::Calendar delete_* methods
The four delete_*_closure / delete_exception methods in Koha::Calendar
were calling ResultSet->delete, which issues a single bulk SQL DELETE
and never calls the row-level delete() override. The cache-invalidation
hooks on Koha::Calendar::SingleClosure->delete and
Koha::Calendar::Exception->delete were therefore skipped when callers
went through $library->calendar->delete_single_closure(...) or
delete_exception(...), and the <library>_holidays cache kept reporting
deleted dates as closed until its TTL expired.

The underlying tables enforce UNIQUE constraints on (library_id, weekday),
(library_id, day, month) and (library_id, date), so each search can
match at most one row. Switch the four methods to ->search({...},
{ rows => 1 })->single followed by ->delete on the row, which both
makes the single-row expectation explicit and ensures the per-row
delete override runs.

A regression test is added to t/db_dependent/Koha/Calendar.t asserting
that the _holidays cache is cleared after delete_single_closure and
delete_exception.

Test plan:
1) prove t/db_dependent/Koha/Calendar.t
2) Confirm the new 'delete_*_closure clears _holidays cache' subtest
   passes.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:57 +00:00
Tomás Cohen Arazi d4e4f57072
Bug 42310: (follow-up) Bake cache invalidation into Koha::Object classes
When using the new Koha::Calendar::* classes for writing the CRUD API
endpoints I noticed that in my attempt to keep C4::Calendar similar to what it
was, I ended up with cache invalidation not always done in the right place.

The holidays cache (keyed by {branchcode}_holidays) stores dates and their
open/closed status, not titles or descriptions. This means only inserts and
deletes need to invalidate it — metadata updates (title/description) do not.

Following the pattern established by Koha::AuthorisedValue and Koha::ItemType,
I added store() and delete() overrides to Koha::Calendar::SingleClosure and
Koha::Calendar::Exception:

- store() flushes the cache only on insert (!in_storage), not on updates,
  since the cache doesn't store titles or descriptions.
- delete() always flushes, since removing a closure changes the cached set.

This allowed removing the now-redundant _clear_cache calls from
Koha::Calendar::add_single_closure, add_exception, delete_single_closure,
delete_exception, and from C4::Calendar::ModSingleholiday and
ModExceptionholiday.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:55 +00:00
Tomás Cohen Arazi cb7968387a
Bug 42310: (follow-up) Add CRUD methods to Koha::Calendar, delegate C4::Calendar
Add to Koha::Calendar:
  add_weekly_closure, add_repeating_closure, add_single_closure,
  add_exception, delete_weekly_closure, delete_repeating_closure,
  delete_single_closure, delete_exception, copy_to, _clear_cache

C4::Calendar write methods now delegate to Koha::Calendar instead
of using raw DBI. No old table references remain in C4::Calendar.

Tests in t/db_dependent/Koha/Calendar.t cover all CRUD operations:
create, verify via is_holiday, delete, copy_to (including
idempotency).

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:54 +00:00
Tomás Cohen Arazi 4d23dff433
Bug 42310: (follow-up) Adapt Calendar tests to new schema
Replace all references to the dropped tables (RepeatableHoliday,
SpecialHoliday resultsets and raw DBI inserts) with Koha::Object
classes: Koha::Calendar::WeeklyClosure, RepeatingClosure,
SingleClosure, and Exception.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:52 +00:00
Martin Renvoize 166e9831ea
Bug 42310: DBIC schema manual updates
Add koha_object_class/koha_objects_class overrides for the four new
Result classes backing the normalized calendar tables, below the
auto-generated marker.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:51 +00:00
Martin Renvoize c0349ad142
Bug 42310: DBIC schema update
Regenerated via dbic --force after the library_weekly_closures,
library_repeating_closures, library_single_closures, and
library_closure_exceptions tables replaced repeatable_holidays and
special_holidays.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:49 +00:00
Tomás Cohen Arazi ca487a18e0
Bug 42310: (follow-up) Add missing POD
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:48 +00:00
Tomás Cohen Arazi 0629de1e54
Bug 42310: Refactor Koha::Calendar to use Koha::Calendar::* classes
Replace raw DBI queries in _init() and _holidays() with
Koha::Calendar::WeeklyClosures, RepeatingClosures, SingleClosures,
and Exceptions searches.

The internal data structures remain identical (weekly_closed_days
array, day_month_closed_days hash, _holidays hash with YYYYMMDD
keys) so all consumers (is_holiday, addDuration, next_open_days)
continue to work unchanged.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:46 +00:00
Tomás Cohen Arazi 402938f7cd
Bug 42310: Add Koha::Calendar::* Object classes
Four Koha::Object + Koha::Objects pairs following the Koha::City(ies)
pattern:

- Koha::Calendar::WeeklyClosure(s) -> library_weekly_closures
- Koha::Calendar::RepeatingClosure(s) -> library_repeating_closures
- Koha::Calendar::SingleClosure(s) -> library_single_closures
- Koha::Calendar::Exception(s) -> library_closure_exceptions

DBIC schema files to be generated via the 'dbic' alias in KTD
after the database migration runs.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:45 +00:00
Tomás Cohen Arazi 5a3e86d0b6
Bug 42310: Normalize calendar tables
Replace the two overloaded tables (repeatable_holidays, special_holidays)
with four purpose-specific tables:

- library_weekly_closures: weekly closed days (e.g. every Sunday)
- library_repeating_closures: annual repeating closures (e.g. Dec 25)
- library_single_closures: one-off closures with a DATE column
- library_closure_exceptions: open overrides with a DATE column

All use library_id FK and follow current community standards. It points to API
usage in a short term so it follows the API naming conventions.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 16:13:40 +00:00
Martin Renvoize 4ff58e553b
Bug 31391: (QA follow-up) Remove unused variables in ModReserve recall conversion
$due_interval and $due_date (and $recall itself) returned by
Koha::Recalls->add_recall were captured but never used in either
branch of the new rank => "recall" handling. Drop the unused
assignment - no behaviour change.

Test plan:
1) prove t/db_dependent/Holds.t t/db_dependent/Reserves.t - all pass

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:16 +00:00
Martin Renvoize 0b7d3b856a
Bug 31391: (QA follow-up) Add missing test coverage for hold-to-recall conversion
Two code paths introduced by this bug had no direct test coverage:

- C4::Reserves::ModReserve's title-level branch of the new
  `rank => "recall"` conversion (only the item-level branch was
  tested).
- The `hold_convert` bypass in Koha::Item::can_be_recalled, which
  skips the "patron already has a hold on this item" check so an
  existing hold can be converted to a recall for the same patron.

No production code changes here, coverage only.

Test plan:
1) prove t/db_dependent/Holds.t t/db_dependent/Koha/Item.t - all pass
2) Confirm the new tests actually exercise the code they claim to:
   reverting the `unless ( $params->{hold_convert} )` guard in
   Koha/Item.pm locally makes the new Item.t subtest fail, as
   expected.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:15 +00:00
Martin Renvoize cd30d78c0e
Bug 31391: (QA follow-up) Fix UseRecalls check in Transfers REST controller
UseRecalls was converted from a YesNo syspref to a Choice preference
("off"/"opac"/"staff"/"opac_and_staff") earlier in this bug, and every
call site was updated from a truthy check to `ne "off"` - except this
one. Since even the string "off" is truthy in Perl, the guard in
Koha::REST::V1::Transfers::cancel was silently defeated: cancelling a
transfer via the REST API always attempted to revert an in-transit
recall, even with recalls disabled.

Test plan:
1) prove t/db_dependent/api/v1/transfers.t - new subtest
   'cancel() - UseRecalls disabled' fails against the old code
   (the in-transit recall gets reverted even though UseRecalls is off)
2) Apply this patch
3) Same test now passes

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:14 +00:00
Martin Renvoize 85c37deb77
Bug 41898: (follow-up) Guard bookings.js against missing DOM when patron has no bookings
js/tables/bookings.js unconditionally called AdditionalFilters.init()
for 'filter-completed' and 'filter-cancelled' on every page load where
the script is included. patron-detail-tabs.inc only renders those
filter elements (and #bookings_table itself) when the patron has at
least one booking, past or present. For any patron with no booking
history, this produced two console.debug 'Element not found' messages
on moremember.pl and circ/circulation.pl.

Guard the ready handler on the presence of #bookings_table, mirroring
the existing pattern in holds.js.

Test plan:
1) Open moremember.pl for a patron with no bookings; confirm no
   AdditionalFilters console messages appear.
2) Open moremember.pl / circ/circulation.pl for a patron with
   bookings; confirm the Bookings tab, filters, and table still work
   as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:13 +00:00
Pedro Amorim d0a79a677c
Bug 41920: (RM follow-up) Remove dead resultset build in Koha::Reports::running
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:12 +00:00
Lucas Gass 8d9e7e641a
Bug 41920: Add unit tests
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:11 +00:00
Lucas Gass fd82dca909
Bug 41920: Remove code from reports/guided_reports.pl
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:10 +00:00
Lucas Gass ada73c317c
Bug 41920: Move total_running_reports_per_instance_limit to Koha/Reports.pm
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:09 +00:00
Kyle M Hall 7ba7bbab7d
Bug 41920: Limit number of concurrent reports that can be run simultaneously for an instance of Koha
As a continuation of bug 41919,  we should limit the total number of simultaneous reports that can be run at a time.

Test Plan:
1) Set total_running_reports_per_instance_limit to 2 in your koha-conf.xml
2) Create 3 long running reports like: SELECT COUNT(*) FROM items i1 JOIN items i2 JOIN items i3 JOIN items i4 JOIN items i5 JOIN items i6
3) Run each report in turn by opening a new tab, browsing to the reports
   module, and choosing to run that report
5) Note the first two continue to run, but the last one gives an error
   that you have too many reports running simultaneously!

Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:08 +00:00
Thibaud Guillot 92cbbd59d5
Bug 32748: (QA follow-up) Fix duplicate AV when limited to several libraries at once
When an authorised value is limited to more than one library, the
no_limit query in GetAuthorisedValues joins authorised_values_branches
and returns one row per associated library, so the value was listed
several times in the item edition dropdown.

Group by the authorised value so each one is only returned once, and
aggregate the restricted flag with MIN() so the value stays
unrestricted as soon as one of its libraries matches the current one.

Sponsored-by: BibLibre
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:06 +00:00
Thibaud Guillot d7f1426d58
Bug 32748: (QA follow-up) Tidy test file
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:05 +00:00
Thibaud Guillot e2506598c8
Bug 32748: (QA follow-up) Fix branchcode filtering on itemtypes
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:04 +00:00
Thibaud Guillot 0794f75adc
Bug 32748: (QA follow-up) Fix cache key and add tests
Sponsored-by: BibLibre
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:03 +00:00
Lisette Scheer 5205fc8bcc
Bug 32748: (QA follow-up) Fix spelling and tidy
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:45:02 +00:00
Thibaud Guillot b443b31998
Bug 32748: Fix item edition when authorised values is restricted
When we set an authorised value linked to one library,
users who do not belong to this library do not get the
value and can therefore overwrite it.

This patch add current value to the dropdown list even if it comes from an authorized value of another library.

Test plan :

1) Edit an item with one of these fields has an authorized value from
another library.
2) On 'edit' you will see AV but with '(Not an authorised value)' string
   and a warn
3) Apply this patch
4) Refresh page and edit item again
5) Now if the authorised value belongs to another lib and is the current
   value, you will see it with a tooltip and a warn. But if there are
authorised values restricted to another lib and isn't current value,
normally it will be not present in the option list.

Sponsored-by: BibLibre

Signed-off-by: Mathieu Saby <mathsabypro@gmail.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-15 14:44:51 +00:00
Pedro Amorim f2aa1ecb53
Bug 31391: DBRev 26.06.00.025
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:16:04 +00:00
Pedro Amorim 8ccba18691
Bug 31391: (RM follow-up) Enforce recalls permission when converting holds to recalls
The previous follow-up only blanked the button's label via JS for
users without the recalls permission; the button still rendered,
was still clickable, and modrequest.pl's cud-convertall had no
permission check at all, so the conversion still succeeded.

Adds CAN_user_recalls to the button's template condition (matching
the existing "Move selected holds" button) so it doesn't render at
all without the permission, drops the now-redundant JS check in
holds.js, and adds a manage_recalls check in modrequest.pl itself.
checkauth()'s userid is the login username, not a borrowernumber, so
this uses C4::Auth::haspermission() directly rather than
Koha::Patrons->find().

Test plan:
1) Set a default recalls rule in admin/smart-rules.pl.
2) Set UseRecalls to 'staff' or 'opac_and_staff'.
3) Give a test patron 'catalogue' + 'reserveforothers' but not
   'recalls'.
4) Check out an item, place a hold on it for another patron.
5) As the test patron, go to that record's Holds tab, confirm
   'Convert selected to recall' doesn't appear.
6) Since the button is gone, confirm the server side is enforced too:
   post op=cud-convertall directly to modrequest.pl and confirm it no
   longer converts the hold.
7) Grant 'recalls' and confirm the button reappears and works.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:16:03 +00:00
Katrin Fischer 324b4d2fbe
Bug 31391: (QA follow-up) Hide "Convert selected to recall" for users with no recalls permission
This introduces a permission check on "recalls" for the link on the holds summary page.

1) Go to http://localhost:8081/cgi-bin/koha/admin/smart-rules.pl, and under the default rule, set:
- 'Recalls allowed (total)': 5
- 'Recalls per record': 3
- 'On shelf recalls allowed': if any unavailable
- 'Recall due date interval': 5
- 'Recall overdue fine amount': 5
- 'Recall pickup period': 2
2) Go to 'Administration' > 'Global system preferences' (http://localhost:8081/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=UseRecalls), set 'UseRecalls' to 'can be placed through the staff interface only'.
3) Go to Henry's patron account, borrowernumber 19, set him a password (http://localhost:8081/cgi-bin/koha/members/member-password.pl?member=19), then edit his permissions (http://localhost:8081/cgi-bin/koha/members/member-flags.pl?member=19) and give him only 'catalogue' and 'reserveforothers' (place holds), do not give him 'recalls'.
4) As a superlibrarian, check out item 39999000003154 (biblionumber 76) to borrowernumber 23, then place a hold on that same record for borrowernumber 49
5) Log out and log back in as Henry.
6) Go to http://localhost:8081/cgi-bin/koha/reserve/request.pl?biblionumber=76, tick the checkbox next to the hold, click 'Convert selected to recall', confirm in the modal.
7) The hold gets cancelled and a recall gets created, even though Henry has no recalls permission at all, proving the action isn't actually gated by it.
8) Apply patch, re-run test plan to 6) - The link to convert should no longer display.
9) Toggle the recalls permission for Henry, link to convert should now reappear.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:16:02 +00:00
Katrin Fischer 3df7914925
Bug 31391: (QA follow-up) Make sure pickup-library is kept when converting holds to recalls
The code was using the patron's home library instead of passing the pickup-location
of the original hold. This is a simple variable change in 2 places for record level and item
level holds.

A) Set 'UseRecalls' to 'staff' at http://localhost:8081/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=UseRecalls
B) At http://localhost:8081/cgi-bin/koha/admin/smart-rules.pl set the default rule: 'Recalls allowed' = 5, 'Recalls per record' = 3, 'On shelf recalls allowed' = 'if any unavailable' (not 'all').
C) Use biblionumber '437' (has just one item, barcode '39999000019186'), check that item out to 'Henry Acevedo' (Patron A).
D) Place a record-level hold on it for 'Johnny Bryan' (Patron B), who's from 'Midway' — but set the hold's pickup location to 'Centerville' instead.
E) On http://localhost:8081/cgi-bin/koha/reserve/request.pl?biblionumber=437, tick the hold, click 'Convert selected to recall', confirm.
F) Check the new recall's pickup location on http://localhost:8081/cgi-bin/koha/recalls/request.pl?biblionumber=437
- Expected: 'Centerville'
- Without the patch applied, it will be the patron's home library: Midway
- With the patch applied, the pickup-location of the hold is kept: Centerville.

G) Cancel the recall.
H) Repeat the test plan, but in step D) place an item level hold instead of a record level hold.

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:16:01 +00:00
Katrin Fischer e128051d09
Bug 31391: (QA follow-up) Fix check for UseRecalls for messaging preferences in OPAC
We are skipping the notice, if UseRecalls == "off".
This now matches the check in staff too.

A) Go to the KTD staff interface at http://localhost:8081/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=UseRecalls and set 'UseRecalls' to any non-'off' value, e.g. 'can be placed through the OPAC only'.
B) Log into the OPAC at http://localhost:8080/cgi-bin/koha/opac-user.pl as any patron.
C) Go to http://localhost:8080/cgi-bin/koha/opac-messaging.pl
D) Look for the recall-related notice row (item recalled / recall notices) — it's missing, even though recalls are enabled.
E) Now flip 'UseRecalls' back to 'off' in step 1's page.
F) Reload http://localhost:8080/cgi-bin/koha/opac-messaging.pl — the recall notice row now shows up, even though recalls are off.
G) Steps 4 and 6 are the reverse of what should happen — confirms the condition is inverted.

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:16:00 +00:00
Katrin Fischer a5e8fdccbd
Bug 31391: (QA follow-up) Adjust for changes made by bug 42443
This fixes a test to use 'opac' in transfers.t

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:59 +00:00
Katrin Fischer 77f3567b59
Bug 31391: (QA follow-up) Fix mocked value in Holds.t
The pref was mocked to '1' (the old value) in Holds.t
Test now uses 'opac_and_staff'

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:58 +00:00
Katrin Fischer 5850a591bd
Bug 31391: (QA follow-up) Fix Recall button visibility in holdings table
Fixes a problem with the template variables that made the recall button
show in some places when UseRecalls was set to off.

As the problem was in item-status.inc that is used in several pages,
a small fix goes far here.

Part 1: opac-detail.tt / opac-basket.tt (default config, useRecalls = 'off')
1) Confirm UseRecalls is at its default (off)
2) Check out item 39999000003154 to patron 'Henry Acevedo'
3) Log into the OPAC as a different patron (e.g. koha user)
4) As koha user, go to the biblio detail page for the record we just checked out:
http://localhost:8080/cgi-bin/koha/opac-detail.pl?biblionumber=76
5) Look at the item's status row: next to "Checked out", a "Recall" button is visible, even though UseRecalls is off.
6) Click it, lands on opac-recall.pl, shows a "Recalls have not been enabled. Please contact your library.", which confirms it's a dead-end button that never should have rendered.
7) Same thing reproduces in the basket: as koha user, add that biblio to your cart (Add to cart on the results/detail page), then click the cart icon on the top left. The same button erroneous will show.

Part 2: opac-course-details.tt — staff-only mode still leaks the OPAC link
1) Continuing from the previous instructions: Set UseRecalls to "can be placed through the staff interface only".
2) Add the same checked-out item (39999000003154) to a course reserves list — http://localhost:8081/cgi-bin/koha/course_reserves/course-details.pl?course_id=1
3) Click "Add reserves"
4) As koha user on the OPAC, view that course's page: http://localhost:8080/cgi-bin/koha/opac-course-details.pl?course_id=1
5) The "Recall" button shows here too, even though UseRecalls=staff should mean OPAC patrons can't place recalls at all.

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:57 +00:00
Aleisha Amohia ca977dcd78
Bug 31391: Allow cancel recall if UseRecalls enabled, remove unnecessary vars
This follow-up includes the following changes:
- only allow a recall cancellation via the OPAC if UseRecalls is enabled for the OPAC.
- uses singleBranchMode var passed by C4::Auth in staff interface biblio recalls
- drops unused attribute_type_codes var
- drops unnecessary items var and uses biblio.items in template

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:56 +00:00
Katrin Fischer 65b8a6129d
Bug 31391: Add "Place recall" button in detail page toolbar
Adds a new entry "Place recall" to the detail page's toolbar, visible when:

- UseRecalls is set to allow recalls in staff
- User has recalls permission

This matches the behavior for holds and bookings, so seems to increase
consistency.

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:55 +00:00
Aleisha Amohia 98c8add185
Bug 31391: Request recalls and convert reserves on staff interface
This enhancement adds the ability to place recalls via the staff
interface by changing the UseRecalls system preference to have choices which
describe where recalls can be placed. Recalls can either be placed via
the OPAC, or the staff interface, or both. To be set to OPAC would be
consistent with current 'on' behaviour. UseRecalls is turned off by default.

This enhancement also adds the ability to convert existing holds to
recalls easily via the staff interface. The hold will be cancelled
and a recall placed using the hold's information.

To test:
1) Update database and restart services
2) Set UseRecalls to OPAC and set the relevant recalls circulation rules
OPAC only by default
3) Check out Item A to Patron B.
4) Log into the OPAC as Patron A. Confirm you can place, view and cancel
recalls as normal.
5) In the staff interface, set UseRecalls to Staff interface only.
6) In the OPAC, confirm you can still view and cancel recalls, but can
no longer place recalls.
7) In the staff interface, search for Item A and go to the Holds tab.
Place a hold for Patron C.
8) Check the checkbox for your hold and confirm the counts in the buttons above the Holds table are incremented as expected.
9) Click the button to Convert selected holds to recalls, a modal should pop up. Confirm all of the information is correct.
10) Confirm the conversion and once the page refreshes, your hold should be correctly cancelled using the new RECALLED reason.
11) Go to the Recalls tab. Confirm your hold has been converted to a
recall, and details like the patron, expiration, and pickup location have
carried across to the recall.
12) On this Recalls page, use the patron search to find Patron D. Test
placing a record-level recall by choosing to recall the next available item.
13) When the page refreshes, confirm the recall was successfully placed
with the correct details stored.
14) Repeat steps 12 and 13 with Patron E, this time place an item-level
recall by choosing an item in the 'place a recall on a specific item'
table. When selecting one of these items, the 'recall next available
item' section should become disabled.
15) Set UseRecalls back to OPAC only. Go back to the record and
view the Recalls tab.
16) Confirm you cannot search for a patron to place a recall, but can
still view and cancel recalls. Confirm you also cannot convert holds
to recalls.
17) Set UseRecalls to both staff interface and OPAC. Confirm you
can place recalls on both interfaces.
18) On the staff interface, test placing recalls that your circulation
rules do not allow. For example, if 'recalls per record' is 1, ensure
you're blocked from placing a second recall on a record for a patron. Another example, testing converting a hold to a recall when there are NO checked out items, as recalls can only be placed if something is checked out.
19) Place a hold for one of the patron's that already has a recall.
Once complete, try to convert this hold into a recall. Confirm you
are blocked and shown a message that the hold cannot be converted to a
recall.
20) Confirm patron messaging preferences for recalls notices can still be configured as expected.
21) Ensure tests pass:

* t/db_dependent/Circulation.t
* t/db_dependent/Circulation/transferbook.t
* t/db_dependent/Holds.t
* t/db_dependent/Koha/Biblio.t
* t/db_dependent/Koha/Item.t
* t/db_dependent/SIP/Transaction.t
* t/db_dependent/Search.t
* t/db_dependent/XSLT.t

Sponsored-by: Auckland University of Technology
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:54 +00:00
Aleisha Amohia 8211dd8d0f
Bug 31391: Add UseRecalls to Hea
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:53 +00:00
Aleisha Amohia 401a52b833
Bug 31391: Implement UseRecalls choices on staff interface
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:52 +00:00
Aleisha Amohia b9b80b9471
Bug 31391: Implement UseRecalls choices in tests
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:51 +00:00
Aleisha Amohia 6091466f51
Bug 31391: Implement UseRecalls choices on OPAC
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:49 +00:00
Aleisha Amohia 0f35039974
Bug 31391: Implement UseRecalls choices in modules
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:48 +00:00
Aleisha Amohia f9c159e0a3
Bug 31391: Update the UseRecalls system preference to have choices
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:47 +00:00
Aleisha Amohia 0ebf8a632e
Bug 31391: Recalled authorised value for hold cancellation
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 14:15:42 +00:00
Lucas Gass d9d4c6c4fd
Bug 36868: (follow-up) Remove 2x call of holds.js, hold-group.js
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 11:42:46 +00:00
Lucas Gass 11012af369
Bug 36868: (follow-up) Remove double instanciated hold_table_settings
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 11:42:45 +00:00
Lucas Gass 3405bb206f
Bug 36868: (follow-up) fix failing tt_kohaTable_tidy.t test
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-14 11:42:38 +00:00
nina martinez d1b39adfc5
Bug 39890: Add search, menu and footer skip links to the OPAC.
Before starting the test plan, you have to enable the following system
preferences:
CurbsidePickup, useDischarge, OPACHoldsHistory,
ILLModule,EnhancedMessagingPreferencesOPAC, CookieConsent, OPACPrivacy,
opacreadinghistory, UseRecalls, OPACHoldRequests, RoutingSerials,
EnableOpacSearchHistory, virtualshelves,suggestion,
OPACViewOthersSuggestions, TagsEnabled, OpacTopissue,OPACVirtualCard.

Test plan:

A) Add skip link to menu

1. Apply the patch, you don't have to be logged in for this part.
2. Go to these different pages: search result page, record details page
   (try the different views), and Most popular page.
3. Try navigating with the keyboard, fix the focus at the top of the
   page
4. You should see a skip link in red, 'skip to menu' appear. After
   pressing enter, the focus should be on the menu.
5. Now go to these different pages: Purchase suggestions page, search
   history page, Lists page.
6. Repeat the third step and check if the skip link "Skip to menu" does
   not appear on these pages.
7. Now you have to log in to a patron account.
8. Go to your Summary page.
9. Repeat the third step with the different pages of the menu (Summary,
   Charges, Personal details, Consents, Tags, Change password, Search
   history, Checkout history, Privacy, Holds history, Recalls history,
   Purchase suggestions, Messaging, Lists, Ask for discharge,
   Interlibrary loan request, Curbside pickups, My virtual card).
10. You should see the "Skip to menu" link, after pressing enter, the
    focus should be on the menu.

B) Add a skip link to the search box

1. Try navigating with the keyboard, fix the focus on the top of the
   page.
2. Check if there is a skip link "Skip to the search" in red on each
   page of the OPAC, except on the advanced search page.

C) Add a skip link to the footer

1. Try navigating with the keyboard, fix the focus at the top of the
   OPAC  page.
2. Check if there is a skip link 'Skip to footer' after the skip link
   'Skip to main content' on each page of the OPAC.
3. Now, disable the system preferences (OPACReportProblem and
   KohaAdminEmailAddress, CookieConsent, OpacKohaUrl,
   OpacLangSelectorMode
   = 'both', or OpacLangSelectorMode = 'footer') to remove the OPAC
   footer.
4. Repeat step 2. Now, you should not have the skip link to the footer.

D)  If all the tests succeed, sign off !

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:06:06 +00:00
Pedro Amorim c6bf858253
Bug 34632: (QA follow-up) Use Koha::Uploader for patron import files
Large files were failing to enqueue/process because the whole CSV was
embedded in the job's JSON args, hitting DB/broker size limits. This
now uploads the file via Koha::Uploader first (same pattern as MARC
staging) and only passes a small uploaded_file_id through the job,
and fixes the enqueue-failure message to not claim nothing happened
when a job was in fact created.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:06:05 +00:00
Pedro Amorim 5ca53cf2bc
Bug 34632: Add patron_import background job display template
patron_import.inc provides the report/detail/js blocks consumed by the
background job viewer: a summary of counts when finished, a live progress
bar while running, and the full feedback and error analysis in the detail
section. background_jobs.tt gains a patron_import entry in the job type
filter dropdown.

Test plan:
1) Apply patches, restart plack.
2) Navigate to the patron import tool:
  <staff_url>/cgi-bin/koha/tools/import_borrowers.pl
3) Upload the provided sample CSV file and click 'Import patrons'.
4) Confirm the page shows "The patron import job has been enqueued!" with a link to view the job.
5) Follow the link to the background job. Confirm a progress bar is shown while the job is running.
6) Once finished, confirm the report shows the correct counts (imported, overwritten, already_in_db, invalid, total).
7) Confirm the imported patrons exist under 'Patrons'.
8) Re-import the same file with 'Overwrite cardnumber' unchecked. Confirm 'already_in_db' count is 5.
9) Re-import again with 'Overwrite cardnumber' checked. Confirm 'overwritten' count is 5.
10) Visit the background jobs admin and filter by type 'Import patrons':
  <staff_url>/cgi-bin/koha/admin/background_jobs.pl
11) Confirm the jobs appear in the list.
12) Run the tests:
  prove t/db_dependent/Koha/BackgroundJob/PatronImport.t

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:06:04 +00:00
Pedro Amorim 88644d26af
Bug 34632: Use PatronImport background job in import_borrowers
import_borrowers.pl now reads the uploaded file into memory and calls
Koha::BackgroundJob::PatronImport->enqueue() instead of running the import
synchronously. On success the template shows a confirmation with a link to
the background job viewer; on failure it shows an error alert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:06:03 +00:00
Pedro Amorim c8c24445af
Bug 34632: Add tests for Koha::BackgroundJob::PatronImport
Tests cover enqueue() (status, queue, job_size, job_type) and six
process() scenarios: happy path, import errors (with validation that
the 25-error cap has been removed), already_in_db and overwritten,
patron list creation, and cancelled job handling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:06:02 +00:00
Pedro Amorim ae60b6ebaa
Bug 34632: Add Koha::BackgroundJob::PatronImport
Introduces a new background job class for patron imports. enqueue() reads
the uploaded file content into the job_args and uses line count as job_size
for progress tracking. process() reconstitutes the file handle from the
stored content, delegates to Koha::Patrons::Import with step_callback wired
to $self->step, handles patron list creation, stores the import results, and
clears file_content from the stored data to free space.

Also registers patron_import in Koha::BackgroundJob::core_types_to_classes
so _derived_class can resolve the job correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:06:01 +00:00
Pedro Amorim 770e5ca6ec
Bug 34632: Add step_callback support to Koha::Patrons::Import
Adds an optional step_callback coderef parameter to import_patrons(). When
provided it is called after every row — whether the row succeeds, is skipped
as a duplicate, or is rejected as invalid — so callers can track progress.

Also removes the 25-error cap that was there to protect browser response
size; that constraint no longer applies when results are stored in a
background job's data field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:05:59 +00:00
Kyle M Hall 115d26ecae
Bug 42679: (QA follow-up) Add tests for missing permissions and non-overdue checkouts
The existing tests pass even if the endpoint counts all checkouts
instead of overdues only. This patch adds a checkout due in the future
and tests the count is unchanged. It also tests that a patron without
the borrowers flag gets a 403.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:05:58 +00:00
Kyle M Hall 14e4d6cb43
Bug 42679: (QA follow-up) Remove search tag that belongs to bug 42508
This tag describes the /search/patrons endpoints added by bug 42508.
Nothing in this bug uses it, and if this bug is pushed alone main would
carry a tag for endpoints that don't exist. Bug 42508 should add it.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:05:57 +00:00
Tomás Cohen Arazi f7caec076f
Bug 42679: Add GET /patrons/{patron_id}/overdues_count endpoint
Returns the count of overdue checkouts for a patron as a plain integer.
Computed live from the database (date_due < NOW()) to ensure accuracy,
as overdue status is time-based with no discrete event to trigger updates.

This endpoint is intended for async loading in the patron search table,
allowing the main search results to render immediately while overdues
are fetched separately per visible page.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/api/v1/patrons_overdues_count.t
=> SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:05:56 +00:00
Laura_Escamilla 5d75b6fdad
Bug 39420: (QA follow-up) Replace tabs with spaces
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:05:55 +00:00
Samuel Sowanick 97235dea0d
Bug 39420: adjusted additem.tt breadcrumb to lead back to item
Bug 39420: removed old breadcrumb - Update additem.tt

Bug 39420: Removed old edit breadcrumb.

Bug 39420: Fixing syntax error in END tag

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 16:05:52 +00:00
Jonathan Druart 693c447736
Bug 41995: Show all entries
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:07:05 +00:00
Jonathan Druart fe8a9a5529
Bug 43330: Simplify tests
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:07:04 +00:00
Laura_Escamilla d5f522e526
Bug 43330: Add regression test for TagsExternalDictionary
To test:

1. Set the TagsExternalDictionary system preference to a non-empty value, for example:

       /usr/bin/ispell

2. Restart Plack if necessary.

3. Visit a staff interface page which loads C4::Tags, such as the staff home page or a bibliographic detail page.

4. Observe:

       Undefined subroutine &C4::Tags::check_install called at C4/Tags.pm line 44.
       BEGIN failed--compilation aborted at C4/Tags.pm line 53.
       Compilation failed in require.
       BEGIN failed--compilation aborted.

5. Clear TagsExternalDictionary and clear/update the system preference cache.

6. Apply the patch.

7. Set TagsExternalDictionary to an empty value.

8. Verify that you can access the home page and other pages.

9. Set TagsExternalDictionary to a valid non-empty path such as
	/usr/bin/ispell

10. Verify the staff home page, catalog detail page, and Tools page load without HTTP 500 errors.

11. Prove t/db_dependent/TagsExternalDictionary.t
12. Sign off and have a great day! :D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:07:03 +00:00
Laura_Escamilla 2934511182
Bug 43330: Fix C4::Tags compile failure with TagsExternalDictionary
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:07:02 +00:00
David Nind 7d52823ac5
Bug 43432: Move ReportsExportLimit to reports category
Move the ReportsExportLimit system preference from the
staff interface category to the reports category, as
it relates only to the reports module.

1. Go to Administration > System preferences.
2. Search for the ReportsExportLimit system preference.
3. Note that it is in the Staff interface > Options category.
4. Apply the patch.
5. Repeat step 2.
6. Note that it is now in the Reports > Options category.
7. Note that the "Reports access" section is now in sentence case.

Signed-off-by: Bastien CHAUVET <bastien.chauvet@inlibro.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:07:01 +00:00
David Nind 87e8f605bb
Bug 43432: Update ReportsExportLimit system preference description
The current description uses "records". It actually controls the
number of rows (or results) exported, which may or may not be records.

Additional updates were also made to improve the description notes.

1. Go to Administration > System preferences.
2. Search for the ReportsExportLimit system preference.
3. Note the use of "records" in the description - results returned may or
   may not necessarily be records.
4. Apply the patch.
5. Refresh the page.
6. Note the updated description:
   - "records" changed to "rows"
   - changes to the notes:
     . NOTE changed to bold, consistent with other descriptions
     . now uses an unordered list
     . active voice used
     . removed note about requiring a positive vale
     . added a note about the "Open in DataTables" option
7. Sign off 8-).

Current description:
Download a maximum of _____ records when downloading from the
reports module.
NOTE: Only a positive value will enforce a limit. A specific
limit in the report overrides this setting.

Updated description:
Download a maximum of _____ rows when exporting report results.
NOTE:
- Using LIMIT in a report overrides this setting.
- It does not limit results when using 'Open in DataTables'
  export options.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:07:00 +00:00
Andreas Jonsson efb310cda0
Bug 43346: MarkIssueReturned with no userenv
Since it is not clear what checkin_library signifies when marking an
issue on a lost item as returned we might as well fallback on setting
it to the issuing library when there is no userenv.

Test plan

* Run the unit test t/db_dependent/Koha/Account/Line.t

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:06:59 +00:00
Andreas Jonsson 7dbc37e187
Bug 43346: Unit test for payment without userenv
MarkIssueReturned needlessly fails when called from a session with no
logged in user such as a script or anonymous callback.  A specific
application where this is a problem when completing a payment
transaction in a plugin for opac online payment wich may be done via
polling or callback.

This unit test verifies that a payment can be completed when paying a
fee for a lost item with MarkLostItemsAsReturned set to onpayment and
no userenv set.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:06:58 +00:00
Owen Leonard f8ef545ca4
Bug 38638: Offer non-priority option when placing multiple holds
This patch adds a checkbox for setting holds as non-priority when
placing multiple holds at once.

To test, apply the patch and make sure the DisplayMultiPlaceHold system
preference is enabed.

- Perform a catalog search in the staff client which will return
  multiple results.
- Check the checkboxes for 2 or more titles in the results and click
  "Place hold" in the toolbar.
- When confirming the holds, check the "Non priority hold" checkbox.
- Confirm that the holds are saved as non priority holds.
  - You can check by viewing the list of holds on a title. The "Details"
    column for that hold should say "Non priority hold."

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:06:56 +00:00
David Nind b8d1993582
Bug 43225: Fix punctuation for renewal confirmation message
A renewal confirmation message (when the AllowFineOverrideRenewing
system preference is set to "Allow") is missing a period and space
between the amount due and the second sentence.

It currently reads:
"The patron has a debt of [amount]Are you sure you want to renew checkout(s)?"

Test plan:
1. Set the AllowFineOverrideRenewing system preference to "Allow".
2. Check out an item to a patron (for example, 39999000011418 to Mary Burton).
3. Add a $110 manual charge to the patron's account:
   - [Patron account] > Accounting > Create manual invoice
   - Should now have a message on the patron's page:
     Charges: Patron has outstanding charges of 110.00. Make payment Pay all charges
4. Attempt to renew the checked out item from the checkouts section:
   - Check out > Checkouts > Show checkouts
   - Below the checkouts table, select "Override fine restrictions"
   - Click "Renew all"
5. There is a pop-up message with incorrect punctuation:
     The patron has a debt of 110.00Are you sure you want to renew checkout(s)?
6. Click the cancel option.
7. Repeat steps 4 to 6 from the details section (Details > Checkouts >
   Show checkouts) and note the same incorrect punctuation.
8. Apply the patch.
9. Repeat steps 4 to 7.
10. The pop-up message now reads:
      The patron has a debt of 110.00. Are you sure you want to renew checkout(s)?
11. Sign off.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:06:55 +00:00
David Cook 448419f918
Bug 43401: (QA follow-up) tweak rspack.config.js comment and fix Makefile.PL
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:06:54 +00:00
Paul Derscheid 8339307961
Bug 43401: (follow-up) Note the Makefile.PL coupling in rspack.config.js
- Add a file-level comment reminding that every shipped output.path needs a
  matching 'NONE' target-map entry and cp step in Makefile.PL

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:06:53 +00:00
Lucas Gass 55f621fd14
Bug 43401: Send compiled js vue for the OPAC to the right dir
The OPAC vue bundle (koha-tmpl/opac-tmpl/bootstrap/js/vue/dist) was not
handled like its intranet counterpart in Makefile.PL: exclude it from the
generic target map and copy it explicitly into blib in move_compiled_js.

Test plan:
1. In a KTD shell, from the koha checkout:
     perl Makefile.PL
   (accept the defaults, 'standard' install mode is fine)
2. make -n move_compiled_js | grep 'js/vue/dist'
   => without this patch only the intranet cp line is printed;
      with it there is also a line copying
      koha-tmpl/opac-tmpl/bootstrap/js/vue/dist into
      blib/OPAC_TMPL_DIR/bootstrap/js/vue/
3. make
4. ls blib/INTRANET_TMPL_DIR/prog/js/vue/dist blib/OPAC_TMPL_DIR/bootstrap/js/vue/dist
   => both directories exist and contain the compiled bundles
      (e.g. islands.esm.js); no vue/dist directory is left anywhere
      else under blib.
5. git clean -xdf blib pm_to_blib Makefile Makefile.old && git checkout -- . to
   clean up.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-11 15:06:50 +00:00
Jan Kissig 374295c25c
Bug 42296: auto tidy for follow-ups
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:43 +00:00
Jan Kissig 0602877c31
Bug 42296: (follow-up) Remove unused base_url on barcode-print
The JS variable base_url on barcode-print is never referenced. The remaining JS block did not contain TT interpolation, so data-tt-allowed can be removed.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:41 +00:00
Jan Kissig 667c8883e6
Bug 42296: (follow-up) Split JS block in auth.tt
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:40 +00:00
Jonathan Druart 4a4843e481
Bug 42296: Tidy all script tags - trivial occurrences
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch only adds 'data-tt-allowed="true"' when needed. The work to
move the JS variable to a separate script tag has been done already on
bug 41324.

Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:38 +00:00
Pedro Amorim 80abb65c62
Bug 41318: DBRev 26.06.00.024
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:36 +00:00
Katrin Fischer 5ae82ddd18
Bug 41318: (QA follow-up) Change Options to Examples in system preference description
While 'Options' might give the impression that the selection is
limited to the listed values, 'Examples' seems the better wording.

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:35 +00:00
Jonathan Druart 18134d8343
Bug 41318: Rename AmazonLocale to AmazonLocaleTld
To be more flexible we could allow the top-level domain instead of
having a predefined list.
However this patch still list the different available options (and adds
Brazilian) for easy selection.

Note that this system preference is not used to retrieve the image but
only to link to from the image. Contrary to what the reporter of the bug
report thought. And actually what I thought as well until I finished to
write the patch and started testing it!
Anyway now the patch is written, there should not be any changes in
behaviour. Most of the time the links are actually broken when not using
.com. So I think we either broke the whole feature at some point, or it
never really worked. On a separate bug we could eventually have a look
at fetching from the locale tld.

Test plan:
Turn on OPACAmazonCoverImages and AmazonCoverImages
http://localhost:8081/cgi-bin/koha/catalogue/detail.pl?biblionumber=5
http://localhost:8080/cgi-bin/koha/opac-detail.pl?biblionumber=5
=> You see the PBP cover
click on it, it opens the lightbox, click "(see the original image)" at
the bottom
=> This is where the syspref AmazonLocale/AmazonLocaleTld acts

Modify the syspref's value and confirm that the link is modified and
point to the correct domain (leading to 404 or broken pages most of the time,
at least in my tests).

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:34 +00:00
Pedro Amorim 3b2a4f53ed
Bug 42344: DBRev 26.06.00.023
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:33 +00:00
Jonathan Druart 9d0f7d2f3c
Bug 42344: Remove CircSidebar
Test plan:
Run the updatedatabase script and notice that the sidebar is visible in
the circulation module but that you can hide/collapse it on demand.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:31 +00:00
Pedro Amorim 35004269a4
Bug 10190: DBRev 26.06.00.022
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:30 +00:00
Chloé Zermatten 68d3ba779b
Bug 10190: (QA follow-up): fix: only remove restrict if no restricting overdues found
Where a patron had multiple overdues that ought to cause debarment, it was found that removing one removed the restriction.

This is because has_restricting_overdues() was looking up the ItemType object tied to the overdue issue, then passing it down to Koha::CirculationRules->get_effective_rules(). get_effective_rules(), however, expects an itemtype code.
This resulted in false negatives: the patron was treated as having no restricting overdues left where they in fact did.

Amend this by calling effective_itemtype() instead.

Note on side effects: I think this also will activate the previously potentially dead dedup logic in has_restricting_overdues (eq had been used to compare obj instance to obj instance which had no effect (itemtype() runs a find() on each iteration), and is now used to compare itemtype code to itemtype code).

Test plan (based on Pedro's comment number 949):

0) Pick a test opac patron, if using Henry from above remove the permissions
1) http://localhost:8081/cgi-bin/koha/admin/categories.pl, edit the test patron's category, confirm 'Overdue notice required' is set to 'Yes'.
2) Set syspref AutoRemoveOverduesRestrictions to when_no_overdue_causing_debarment.
3) http://localhost:8081/cgi-bin/koha/admin/circulation_triggers.pl, 'Add new trigger'. Leave 'Library' as 'All libraries' and 'Patron category' as 'All categories', set 'Item type' to 'Books'. For 'Letter' pick ODUE, check 'Email' under 'Transport type(s)', set delay to 5 days, for 'Restricts checkouts' select 'Yes', 'Submit'.
4) Check out barcodes 39999000019186 and 39999000007558 to the test patron (confirm both are itemtype 'Books'). Backdate both due dates 6+ days overdue.
5) perl misc/cronjobs/overdue_notices.pl -n — restricts the patron.
6) Check the patron's 'Restrictions' tab: confirm restricted.
7) Return barcode 39999000019186 only, leave 39999000007558
8) Check the patron's 'Restrictions' tab again. Notice they are still restricted (39999000007558 is still overdue).
9) Return barcode 39999000007558 also.
10) Check the patron's 'Restrictions' tab again. Notice the restriction is now removed (a hard refresh may be needed) as no restricting overdues are left.

Assisted-by: Claude Opus 5 (Anthropic)
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:29 +00:00
Chloé Zermatten 99442e36b3
Bug 10190: (QA follow-up): fix: prevent delay-less rule sets
Since the decision was made that the add / edit trigger form should
never let a trigger be created or edited to not have a delay, it
follows that this must apply to the rule set reset flow as well.

Retain the warning message to the user, and also disable the
'Confirm reset' button if the reset would result in delay-less
sets.

The same applies to the trigger deletion functionality: if deleting
a trigger that is a default for all libraries would then leave any
libraries with one or more delay-less rule set, block the trigger
deletion (deactivate 'Confirm deletion') and inform the user. Here
as well we also still display the warning message.

Test plan:

A) Create one or more 'Default for all libraries' triggers.
B) Create one or more library-specific triggers.
C) For one or more of these, add itemtype and/or category specific overrides with not delay override.
D) For one or more of these, add itemtype and/or category specific overrides with a delay override.
E) Select 'Reset' on a rule set with overrides of type D) and notice that the 'Confirm reset' button is enabled.
F) Select 'Reset' on a rule set with overrides of type C) and notice that the 'Confirm reset' button is disabled,
    and that the warning message explains why reset is unavailable
G) Select 'Delete' on the highest 'Default for all libraries' trigger, and notice that, where one or more library-specific overrides are of type D), 'Confirm deletion' is disabled, whereas it is otherwise enabled.
H) Delete a trigger for which no override exists and confirm success.
I) Delete a trigger for which only type D) overrides exist and confirm success.
J) Reset a rule set for which only type D) overrides exist and confirm success.
K) Reset a rule set for which no override exists and confirm success.

Assisted-by: Claude Opus 5 (Anthropic)
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:28 +00:00
Chloé Zermatten b38d6d5201
Bug 10190: (QA follow-up): fix: keyboard delay delete behaviour
If the 'Delay' input field value is deleted using backspace or other,
block form submission. The same behaviour apply if the delete button
is used, as both clear out the value.

As triggers with no delay are not processed and would also cause
higher triggers to then also not be processed, we do not allow the
creation of triggers without a set delay.

Triggers when edited individually do not check for impact on any overrides
they might have. It is therefore safest not to allow submission of a trigger
where the user explicitly unset the value.

An explicit 'reset to fallback' feature is possible and would constitute a follow up enhancement.

Test plan (based on comment 872):

A) 'Administration' > 'Circulation triggers' > 'Add new trigger' for any library/category. Set a Delay, a Notice, and a transport type.
B) Select the Delay field's value and clear it via the 'X' clear button.
C) Notice 'Submit' is disabled.
D) Select the Delay field's value and clear it via the keyboard (Backspace/Delete). Click 'Submit'.
E) Notice 'Submit' is disabled.

Assisted-by: Claude Opus 5 (Anthropic)
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:27 +00:00
Chloé Zermatten e40291cb2c
Bug 10190: (QA follow-up): fix: missing rewrite rule in koha-httpd.conf
Looking at Bug 41167, this ought to fix the 404 error noted in comment 192 for package installs.

We do have a test asserting that RewriteRules must be carried over, so:

Test plan:
1. Apply patch
2. prove xt/check_apache_conf.t
3. Confirm it passes

That said, I don't think it is replicable in ktd - the copied file at /etc/koha/apache-shared-intranet.conf
will remain stale unless cp_debian_files.pl runs. The migration issue itself might be ktd-specific.

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:26 +00:00
Chloé Zermatten ec22f77b76
Bug 10190: (QA follow-up): fix: permissions
Important note: the following assumes manage_circ_triggers is intended
as a sibling to manage_circ_rules, and that of the two, only
manage_circ_triggers ought to grant access to client-side circulation
trigger configuration actions.

When manage_circ_triggers was introduced, it was not applied client-side,
resulting in the router and menu permission checks incorrectly expecting
manage_circ_rules. Amend that.

Additionally, while the API endpoint blocked a user with only
manage_circ_triggers from editing non-triggers circulation rules,
it did not block a user with only manage_circ_rules from editing
trigger circulation rules. Add a guard clause.

Test plan (based on comment 872):
A) Go to a patron's 'More' > 'Set permissions' tab. Check 'catalogue' (needed for basic staff login), and under circulation-rules sub-permissions, check only 'Manage overdue notice triggers' - leave 'Manage circulation rules' unchecked.
B) Log in as that patron, go to 'Administration' - 'Circulation triggers' link present (admin-home.tt now correctly checks CAN_user_parameters_manage_circ_triggers), 'Circulation and fine rules' is not present.
C) Visit http://localhost:8081/cgi-bin/koha/admin/circulation_triggers directly - you are granted access.
D) Confirm that you can create and edit circulation triggers.
E) Go to a patron's 'More' > 'Set permissions' tab. Check 'catalogue' (needed for basic staff login), and under circulation-rules sub-permissions, check only 'Manage circulation rules' - leave 'Manage overdue notice triggers' unchecked.
F) Log in as that patron, go to 'Administration' - no 'Circulation triggers' link present (as we require manage_circ_triggers), however 'Circulation and fine rules' is present.
G) Visit http://localhost:8081/cgi-bin/koha/admin/circulation_triggers directly - you see "Error: You do not have permission to access this page" and are prompted to log in as a different user (as we require manage_circ_triggers).
H) Go to http://localhost:8081/cgi-bin/koha/admin/smart-rules.pl, and confirm that you can edit circulation rules for your library (no regression).

Assisted-by: Claude Opus 5 (Anthropic)
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:25 +00:00
Chloé Zermatten a4be8b914e
Bug 10190: (QA follow-up): fix: itiva script
The following issues were identified and resolved:

- items.itemtype does not exist -> swap to items.itype
- leftover WHERE clause still referred to overduerules -> removed
- with the join on overduerules removed there is no need to GROUP BY anymore
- Koha::CirculationRules->all -> use next() instead

Test plan (as per comment 872):
A) In KTD, set 'TalkingTechItivaPhoneNotification' to 'On'.
B) Run perl misc/cronjobs/thirdparty/TalkingTech_itiva_outbound.pl --type=OVERDUE -o /tmp/out.txt -v
C) No errors / the script does not die mid run.

Quick disclaimer: I am not overly familiar with that script:
while I did remove the causes behind the script dying mid run,
I won't have spotted issues with expected output / outcome.

Assisted-by: Claude Opus 5 (Anthropic)
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:24 +00:00
Nick Clemens 14e7814c15
Bug 10190: (QA follow-up) Fix spelling for QA tools
Other complaints are invalid:
ContextA spelling - this is a variable
Use of _("") - this is in regex for 'overdue_(' when checking rules
git manipulation - not ideal adding and removing in same set, but this is a long
   work, not asking for a squash here

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:23 +00:00
Nick Clemens a571b00a31
Bug 10190: (QA follow-up) Add new permission to tests
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:22 +00:00
Nick Clemens 8150c532fa
Bug 10190: (QA follow-up) Adjust error message to list both permissions
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:21 +00:00
Chloé Zermatten 2e3f9bd1ce
Bug 10190: (QA follow-up): fixes phase four
- Display new manage_circ_triggers permission (add to template)
- Ensure loading messages are informative to the user

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:20 +00:00
Chloe Zermatten 763dc46e31
Bug 10190: (follow-up): test: cypress tests update
New tests:
- regression: mtt cells render as .fallback (bold italics)
- bg-success-subtle highlight on completed steps in the add flow
- disabled-Delete states:
  - patron category filter set
  - item type filter set
  - default library, if any library has a trigger higher that default's highest
- redirect to admin home if not permitted to access the page
- in the edit/add stepper form
  - check row actions are enabled at the 'selectOrAdd' step
  - check row actions are disabled once add mode is entered
- check the action column is always available on the main page

Clean up / general updates:
- replace the /api/v1/** catch-all intercept with per-endpoint intercepts so @get-rules waits resolve reliably
- remove now-redundant cy.wait("@get-rules") calls
- drop unused mount / dayjs / dates imports
- ensure expected strings match UI where strings have changed

Assisted-by: Claude Opus 4.7 (Anthropic)

AI used to draft enhancements tests. Changes were reviewed, tweaked, and commited manually and gradually (with a pre-submission commit squash). Tests were run multiple times and adjusted iteratively to match code changes, cover regressions as those were found, and handle vue page load.

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:19 +00:00
Chloe Zermatten d6f596c514
Bug 10190: (QA follow-up) UI/UX improvements and fixes - phase three
Trigger deletion and rule set reset
- warn the user that resetting or deleting a given set will break sets using it as fallback.
- indicate which sets will be affected.
- display a preview of the post reset state of each affected rule set.
- preparation: allow TriggerTable to take a title parameter.
- refactor to remove 'has_rules' from the store's rule suffixes list so those match the DB.
- DRY: refactor to implement TriggerTable rather than use custom templates.
- select context stage handles narrower viewports
- prevent crash on missing rule set

TriggersTable: refactor: the <a> elements used as buttons are now button elements
- this is so they may be marked as disabled.

Main page - trigger actions (delete button display)
- disable the trigger deletion button.
  - if the context has been narrowed down ('delete' deletes all explicit rule set per trigger per library).
  - if the trigger belongs to default library AND one or more libraries have a trigger of a number bigger than the selected one.
  This is to protect trigger sequentiality from being broken for the relevant libraries.
  We also include an on hover message to explain the disabled button.

Add/ Edit trigger form
- clarify letters 'undo' button behaviour:
- swap the native vue select X for a fa-undo icon.
- display 'undo override and reset to fallback' on hover.
- only display the add/edit button at the selectOrAdd step.
- restore selected trigger yellow highlight (regression fix).
- display any temporarily saved data from prior step in a green 'success' container.
- only messages and data pulled from the DB for informative purposes display in blue containers.

Other
- redirect to correct URL (admin home) if store load fails
- mtt fallbacks display as such consistently (bold italics) - is a bugfix.
- catch errors on store initialisation
- remove whitespace duplicate
- ensure that mtt, if set, always display

Assisted-by: Claude Opus 4.7 (Anthropic)

AI used to help trace back potential bug sources, flag potential regressions, and draft enhancements. Changes were reviewed, tweaked, and commited manually and gradually (with a pre-submission commit squash).

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:18 +00:00
Victor Grousset/tuxayo 622f0a4742
Bug 10190: (QA follow-up): fix pod of CirculationRules.pm
We have nothing to do with it but since the patches change the file, it
trips the QA script.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:16 +00:00
Chloe Zermatten 37a9684669
Bug 10190: (QA follow-up): UI/UX bug fixes - phase two
Fix: handle exiting modal
 - prevent data reload when the user exits the modal
 - as preparation for the above: refactor to use one handler only
Fix: new trigger saves with delay defined
 - Identified an old refactor omission - we were still passing 'i' despite the from now relying on this.triggerNumber
Fix: return to correct trigger tab after save or reset
Fix: restore delete button and bold italic info
 - restore display, fixes a regression introduced by the previous UI/UX patch set
Fix: order dropdown options by description or name (not code)
Fix: circ rule resets
 - restore reset funtionality
  (mtt was still occasionally passed as a string, and would then get compared to an array. Ensure this is not the case.)
 - do not trigger the data refresh if the current ruleset did not get deleted
 - make sure triggerNumber is defined when navigation is caused by reset
 - add the notice code in brackets to letter names for display (eg. Overdue notice (ODUE))

Assisted-by: Claude Opus 4.6 (Anthropic)

AI used to help trace back potential bug sources and draft fixes. Changes were reviewed, tweaked, and commited manually and gradually (with a pre-submission commit squash)

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:15 +00:00
Chloe Zermatten 0fa640b387
Bug 10190: (QA follow-up) UI/UX improvements and fixes
- Refactor (prepation): Extract form sub-components for better maintainability
  - Template stucture improvements (eg. consistent fieldset use)
  - Ensure triggerNumber is always cast to integer from query params
  - Add/Edit trigger form UI/UX improvements/fixes:
      - Restore add/edit button functionality on trigger tables in the form
      - Update Context section title to dynamically change depending on whether editing or displaying
      - Display trigger context as informational when not in confirmContext mode
      - Hide required labels and validation on view-only context fields
      - Always display trigger context at the top of the form
      - Add renewal/autorenewal summary (renewals allowed, automatic renewal) to circulation rules summary panel
      - Sort notice letter dropdowns by name rather than code
      - Update Circulation rules summary title to be more descriptive
  - Scroll behaviour improvements:
      - Extract scrollToElementById to store with polling and null guard
      - Block page scroll when modal is open
      - Disambiguate duplicate trigger-table element ids (trigger-table-main
        vs trigger-table-form) to fix unpredictable scroll-to behaviour

Assisted-by: Claude Sonnet 4.6 (Anthropic)

Experimenting a little here - not quite certain where we draw the line for substantial use so I thought I would add a description in case it's useful, especially since the AI code guidelines are quite fresh:

AI was used primarily to help parse longer Vue templates (CirculationTriggersFormAdd.vue) and help isolate elements for extractions as well as highlight template formatting inconsistencies and propose improvements. The refactoring itself was achieved starting from AI-proposed changes which were then thoroughly re-worked manually towards the code changes submitted.
I also trialled using it (currently experimenting with this) for minor coding tasks where there is a clear precedent (for example, to add renewal/autorenewal information and do so in a way that follows exactly what we had written).
In those cases, each code change made by AI was the outcome of multiple iterations with direct feedback (which it did require a fair amount of) from me to ensure the output matched what I would have written myself.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:14 +00:00
Chloe Zermatten 8f6ff0a73d
Bug 10190: (follow-up): feat: indicate mtt is required
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:13 +00:00
Chloe Zermatten 6dcfe9f878
Bug 10190: (follow-up): fix: handle fallbacks in edit/add
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:12 +00:00
Chloe Zermatten b12d701494
Bug 10190: (follow-up): fix: reset rule info message
The information message showing in the reset modal
still referred to an earlier version of the UI.
Update it to correctly name the relevant filter.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:11 +00:00
Chloe Zermatten aa905b0801
Bug 10190: (follow-up): fix: mtt formating: handle unset
This is needed to handle creating new triggers
for libraries for which all earlier triggers are
defaults (do not exist in the DB , are not explicitly set)

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:10 +00:00
Martin Renvoize e4aecaa519
Bug 10190: Add manage_circ_triggers permission for notice trigger access
Replaces the broad manage_circ_rules permission requirement on the
circulation triggers page with a new dedicated manage_circ_triggers
sub-permission (under parameters, flag 3).

- Add manage_circ_triggers to userpermissions.sql
- Update atomicupdate to add the permission, grant it to existing
  edit_notice_status_triggers users, and remove the old permission
- Update admin/circulation_triggers.pl to require manage_circ_triggers
- Add manage_circ_triggers as an OR option on all circulation_rules
  API endpoints (GET/PUT /circulation_rules, /kinds, /config)
- Restrict manage_circ_triggers users in the API controller to
  overdue_* rule names only on write

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:09 +00:00
Martin Renvoize fabe480e52
Bug 10190: (follow-up) Add missing CSP nonce to inline script tag
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:08 +00:00
Chloe Zermatten f2951ab377
Bug 10190: (follow-up) mtt formatting
The Transport Type options when displaying in 'edit' mode were showing as
a comma separated string rather than as separate options (as they do when being set).
Ammends this so that they display as separate options consistently.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:07 +00:00
Martin Renvoize 4fdc9c76e1
Bug 10190: Fix false conflict error when editing specific trigger rules
When editing a specific rule (e.g. */Kids/Books) alongside an all/all/all
rule, the conflict check incorrectly reported a conflict. The effective=true
API response always returns context as */*/* (fallback), so ruleSetInDb.context
differed from currentRuleSet.context (which was overridden to the real context).

Fix by overriding ruleSetInDb.context before the hasConflict comparison,
consistent with the existing pattern in setCurrentRuleSet.

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:06 +00:00
Martin Renvoize b760367e25
Bug 10190: Fix patron_categories permission format
The x-koha-authorization was using 'parameters: catalogue' which the
Koha auth system interprets as a 'catalogue' subpermission within the
'parameters' module (which doesn't exist), causing a 403 for any user.

The correct format for the top-level 'catalogue' permission is
'catalogue: "1"', consistent with all other read-only API endpoints.

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:05 +00:00
Martin Renvoize bc461cdde1
Bug 10190: Center tables in add modal
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:03 +00:00
Martin Renvoize 88b16e6445
Bug 10190: Enforce manage_circ_rules_from_any_libraries permission
- Add OR-type x-koha-authorization to /circulation_rules, /circulation_rules/kinds,
  and /circulation_rules/config endpoints so users with either
  circulate:circulate_remaining_permissions OR parameters:manage_circ_rules
  can access them
- Enforce manage_circ_rules_from_any_libraries in the set_rules controller:
  users without this permission may only set rules for their own library and
  cannot set default (*) rules
- Add explicit use C4::Auth qw(haspermission) import to CirculationRules.pm
- Lock the library selector in the Vue UI for users who lack
  manage_circ_rules_from_any_libraries, defaulting them to their own library
- Update set_rules() tests to cover the new permission restrictions

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:02 +00:00
Martin Renvoize 14fe8cb2f5
Bug 10190: (QA follow-up) Fix six issues reported during QA
1. Letter context: setFilteredLetters() was reading library_id from
   currentRuleSet (which has no top-level library_id property) instead
   of this.context.library_id, causing library-specific letter templates
   to never match.

2. False conflict on edit: conflict check fetched rule set from DB with
   effective=false (raw), but the in-memory snapshot was fetched with
   effective=true (with fallbacks). Comparing raw vs effective always
   triggered a conflict when a context inherited any value from a more
   general rule. Fixed by using effective=true for the DB fetch too.

3. Incomplete deletion: updateCircRuleSets() inside deleteRuleSet() was
   called without await, creating a race condition where the page refresh
   could complete before the deletion API call, leaving stale data visible.
   Also added a guard to skip rule sets with no explicit rules for the
   trigger being deleted, and simplified rulesForDeletion construction.

4. No rules displayed on initial load: the route watcher in
   CirculationTriggersList fired immediately before init() had loaded
   patron categories and item types, causing setAllExhaustiveEffectiveRuleSets()
   to produce an empty result. Fixed with a metaInitialized flag that the
   watcher waits for before proceeding.

5. DefaultToLoggedInLibraryOverdueTriggers syspref not respected: the
   default_view template variable was computed but never applied to the
   store's currentLibraryId. Fixed by passing it to init().

6. UI colors: info box background changed from #84c1f3 to #DAECFB (lighter,
   matching the circulation rules page). Error alert messages changed from
   alert-info (blue) to alert-warning (yellow) to match Koha conventions.

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:01 +00:00
Martin Renvoize 8f6cee51ea
Bug 10190: Fix permission mismatch on /circulation_rules/config endpoint
The config endpoint was gated on parameters: manage_circ_rules, while the
actual GET/PUT /circulation_rules endpoints require circulate:
circulate_remaining_permissions. Any user who can read/write circulation
rules should also be able to load the config that drives the UI.

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:25:00 +00:00
Martin Renvoize e41d6b54ca
Bug 10190: Patron categories fetch shouldn't require full manage permission
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:59 +00:00
Martin Renvoize c53a2ead10
Bug 10190: fix failing Permissions.t and overhaul CirculationTriggers Cypress spec
Permissions.t:
- Remove edit_notice_status_triggers from userpermissions.sql so fresh installs
  no longer seed this deprecated permission (existing installs are handled by
  the atomic update)
- Add a defensive DELETE in the test to cover DBs that pre-date the atomic update

CirculationTriggers_spec.ts:
- Fix getMockCirculationRule: the helper was returning the old {rule_name,
  rule_value, branchcode, ...} format; update to the current API format
  {context: {...}, overdue_N_delay: ..., ...}
- Remove assertions for UI elements that do not exist (success/error flash
  messages, "Existing triggers" heading, "rule set will be removed" text)
- Fix reset confirmation text to match actual component output
- Fix duplicate patron_category_id "ST" in mock patron categories
- Remove aspirational tests that checked non-implemented behaviour
- Add describe block for the no-default-triggers warning (no rules anywhere,
  rules only at specific libraries, library link switches the filter)
- Add regression test for NaN trigger number when adding first rule for a library

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:58 +00:00
Martin Renvoize f631df5afd
Bug 10190: Fix test count in overdues.t
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:57 +00:00
Martin Renvoize b80df78435
Bug 10190: warn when no default triggers exist and show libraries with rules
When viewing the default library context with no triggers defined, show
a warning message explaining defaults apply to all libraries. If any
libraries have specific triggers, list them as clickable links that
switch the filter to that library. If no rules exist anywhere, show a
prompt to add the first trigger.

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:56 +00:00
Martin Renvoize 17c3aab6c4
Bug 10190: fix NaN trigger number when adding first rule for a library
When no circulation rules exist for a library, triggerCounts[library_id]
is undefined, causing undefined + 1 = NaN. Use nullish coalescing to
default to 0 so the first trigger correctly gets number 1.

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:55 +00:00
Martin Renvoize 7413d2724e
Bug 10190: remove has_rules sentinel from circulation_rules table
has_rules was a purely frontend marker stored as a circulation rule to
signal that a context had explicitly-saved overdue trigger rules. The
backend never reads it; the information is fully derivable from whether
any of delay/notice/mtt/restrict are non-null for a given context.

Replace all has_rules DB reads/writes with a hasExplicitRulesForTrigger()
helper that checks the four real rule suffixes directly. has_rules remains
as a computed in-memory concept in the Vue store (derived in findEffectiveRule)
so templates require no changes.

Also adds an atomicupdate to delete existing has_rules rows from the DB,
and removes the has_rules insert from the bug_10190 migration script.

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:54 +00:00
Martin Renvoize 591263d5ef
Bug 10190: three minor issues
- overdue_notices.pl: fix verbose sprintf - remove spurious \$triggered
  argument (CLI 0/1 flag had replaced delay value in output) and use
  the \$borr string that was computed but never included in the message
- overdue_notices.pl: remove %already_queued hash which was populated
  in _enact_trigger but never read; dead code since the refactor
- C4/Overdues.pm GetBranchcodesWithOverdueRules: guard the global-rule
  expansion with @branchcodes check so an empty result (no rules at all)
  correctly returns an empty list instead of all branches

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:53 +00:00
Martin Renvoize 8659f4e016
Bug 10190: correction to x-koha-authorization on /circulation_rules/config
The config endpoint is used exclusively by the Circulation Triggers
Vue app, which is gated on parameters/manage_circ_rules. Using
acquisition/'*' and erm/1 was inconsistent copy-paste from an
unrelated endpoint and would 403 circ-rules admins while granting
access to unrelated module users.

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:52 +00:00
Martin Renvoize 228bf75845
Bug 10190: fix four more issues spotted in the overdues script
- Patron.pm has_restricting_overdues: sort ASC not DESC so the oldest
  (most overdue) item is checked per branch/itemtype; DESC caused the
  deduplication skip to discard the very items most likely to trigger
  a restriction
- overdue_notices.pl: fix --itemtypesout option name in GetOptions
  (was 'itemtypeouts', never matched the documented --itemtypesout flag)
- overdue_notices.pl: fix typo 'catagorycode' -> 'categorycode' in
  --borcatout search, which silently broke category exclusion
- circulation_triggers.pl: add flagsrequired => parameters/manage_circ_rules
  so the admin page enforces the correct permission; also add missing
  use Koha::Notice::Templates

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:51 +00:00
Martin Renvoize 6640e3e58a
Bug 10190: fix three regressions in overdue_notices.pl
- Store patron name/address fields in \$borrower_overdues so
  prepare_letter_for_printing has data for printed/CSV/text notices
- Fix email->print fallback: look up items under original MTT key,
  not the effective (fallback) key
- Set \$admin_email_address from \$library unconditionally in
  _enact_trigger; previously undef in non-patron_homelibrary mode,
  stripping the from_address on all queued notices

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:50 +00:00
Martin Renvoize d539637c5e
Bug 10190: rewrite atomicupdate with correct 1:1 rule migration
The previous migration had two fundamental errors:

1. It manufactured phantom empty rows for all patron categories that
   had no rule, which corrupted the "most frequent" calculation.

2. It created categorycode=NULL (all-categories) default rules for
   every branchcode seen, silently extending overdue notice coverage
   to patron categories that had no configured rule in the old system.
   The backend (overdue_notices.pl) does not consult has_rules, so
   these invented defaults would cause notices to be sent to patrons
   that were deliberately uncovered.

The rewrite migrates only explicit (branchcode, categorycode) pairs
that had a positive delay configured. No default rules are created.
branchcode='' maps to NULL as before. exit is replaced with return
to avoid killing subsequent atomicupdate scripts.

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:49 +00:00
Chloe Zermatten 8c8160761d
Bug 10190: Deprecate permission
user_permissions rows do not need manual cleanup because the FK on
user_permissions(module_bit, code) references permissions(module_bit, code)
ON DELETE CASCADE, so deleting from permissions automatically removes
per-patron rows.

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:48 +00:00
Chloe Zermatten 564f15ca59
Bug 10190: prevent duplicate ruleset load
- prevent the initial API call and subsequent exhaustive applied ruleset list generation logic from running twice

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:46 +00:00
Chloe Zermatten 3bb6f2e69c
Bug 10190: (QA follow-up) swap undef for all in update db output
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:45 +00:00
Chloe Zermatten a45a7aa8e1
Bug 10190: trigger add/edit form UI/UX enhancements
- scroll to the add/edit form on button click
    - prevent redundant full page reload upon the user swapping between adding and editing while the modal is open
    - ensure robust scroll behaviour, handling:
          - the URL being accessed directly
          - the edit button in the table in the main page being used to access the form directly
          - the stepper workflow being used
          - the user swapping between add and edit once already looking at the form
- dynamic display of 'Transport Type' consistency: only show if a 'Notice' has been selected
- if a notice has been selected, prevent form submission if transport type isn't set and display an alert on submission attempt

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:44 +00:00
Chloe Zermatten d3022cb950
Bug 10190: (QA follow-up) add missing filter
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:43 +00:00
Chloe Zermatten d508b600dd
Bug 10190: enhance trigger context display in modal
- improve modal scroll behaviour
- improve modal dimensions
- display contexts as a horizontal list to make better use of the available space
- fully contain dropdowns within the modal body

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:42 +00:00
Chloe Zermatten b8f8175539
Bug 10190: (QA follow-up) typo
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:41 +00:00
Chloe Zermatten 607ddb5a3f
Bug 10190: (QA follow-up) amend copyright statement
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:40 +00:00
Chloe Zermatten 53a2b75851
Bug 10190: (QA follow-up) add new line at end of MessageTransportType.pm
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:39 +00:00
Chloe Zermatten 9c6781302c
Bug 10190: (QA follow-up) fix unix permissions on circulation_triggers.pl
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:38 +00:00
Chloe Zermatten d176ba4d59
Bug 10190: (QA follow-up) fix unix permissions on bug_10190.pl
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:37 +00:00
Chloe Zermatten 298b4b6efc
Bug 10190: refactor to remove redundant try..catch
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:36 +00:00
Chloe Zermatten 7e70013450
Bug 10190: remove circ triggers link from tools menu
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:35 +00:00
Chloe Zermatten cf8230cdda
Bug 10190: fix permissions
- add a config API endpoint to Circulations Triggers so permissions can be fetched
- update the permissions in the Vue App so that manage_circ_rules is required
- update the permissions in the Circulation Rules API so that manage_circ_rules is required for PUT operations
- update the circulation-rules store to so that it is compatible with recently intorduced compostables
- implement a redirect into the route so that permission requirements are honoured

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:34 +00:00
Chloe Zermatten 839bc81e80
Bug 10190: (follow-up) Complete Vue.js rewrite of Circulation Triggers administration interface
This patch aims to resolve the following issues as reported by comment 193:

- missing atomic update for overde_X_has_rules (amended to account for comment 227)
- existing delays being overwritten by the client when a rule set is displayed
- inability to save circulation overdue rule sets for context for which circulations rules where already set
- address bug_10190.pl db script warnings (undef values were being concatenated with strings)
- add a guard clause to bug_101090.pl: check that the tables the script pulls the data from do still exist, otherwise exit.
- remove duplicate Letters options in the circulation triggers add / edit form

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:33 +00:00
Pedro Amorim 5dca36b1ce
Bug 10190: (QA follow-up): Fix removal of tables
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:32 +00:00
Martin Renvoize 1dc42db13b
Bug 10190: Comprehensive Cypress tests for Circulation Triggers UI
This patch significantly enhances the Cypress test coverage for the new
Circulation Triggers administration interface, expanding from basic
breadcrumb checks to comprehensive end-to-end testing.

Test Coverage:
- Initial page load and API error handling
- Context filtering (library, patron category, item type)
- View toggling between explicit and all applicable rules
- Tab navigation between multiple triggers
- Full CRUD operations (Create, Read, Update, Delete)
- Rule set reset functionality
- Letter template filtering by library context
- Form validation and constraint checking
- Loading states and empty states
- Error handling for all operations
- Browser navigation (back/forward buttons)
- Complex scenarios with inheritance hierarchies

New Features:
- Mock data helper functions for test fixtures
- Comprehensive API interception for all endpoints
- Tests for both success and error paths
- Placeholder value testing for fallback rules
- Increment/decrement button testing
- Multi-step form workflow validation
- Confirmation dialog testing for destructive operations

The tests follow Koha's Cypress testing patterns established in the
ERM module, using proper API mocking, clear test descriptions, and
organized test suites.

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:31 +00:00
Martin Renvoize bb88db9be3
Bug 10190: Update itiva script to reflect move to circ_rules
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:30 +00:00
Martin Renvoize 8d5d976ff0
Bug 10190: Remove old overduerules interface
This patch updates the link from the tools homepage to open the new
circulation triggers administration page.

We then remove the old overduerules controller and template, and finally
we drop the no longer referenced
C4::Overdues::GetOverdueMessageTransportTypes method and associated unit
tests.

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:29 +00:00
Martin Renvoize 4eb521ba18
Bug 10190: Migrate MarkIssueReturned test to circulation rules
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:28 +00:00
Martin Renvoize f056486560
Bug 10190: Update has_restricting_overdues to refer to circulation rules
This patch updates the Koha::Patron->has_restriction_overdues method to
refer to the migrated overdue rules.

We also correct an oversight in the method prior to this patch that
meant that not all overdues were being correctly checked for a
restriction.. we were incorrectly filtering to only overdues at the
current checkin branch.

Finally, we also take account of the itemtype level addition here too.

NOTE: There weren't any unit tests for this Koha::Patron method.. we
should add some here!

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:26 +00:00
Chloé Zermatten d2c575a93d
Bug 10190: Complete Vue.js rewrite of Circulation Triggers administration interface
This patch introduces a modern Vue.js-based interface for managing circulation triggers (overdue notice/status triggers), replacing the legacy Tools-based interface with a more intuitive, feature-rich administration page.

New Features

Enhanced Visibility & Navigation
- Exhaustive effective rule lists showing all rules that apply to each context
- Toggle between "all exhaustive rules" and "explicitly set rules" views
- Advanced filtering by patron category and/or item type
- Clear display of fallback/default rules for each context

Rule Management Actions
- Delete triggers (last trigger in a sequence for any given library)
- Reset entire rule sets for a given context (with safeguards)
- Edit existing triggers with improved context awareness
- Add new triggers with intelligent delay constraints

Improved User Experience
- Multi-step form wizard with clear workflow progression
- Context-aware letter template filtering (only shows applicable templates)
- Placeholder values showing fallback rules when clearing inputs
- Dynamic min/max constraints for delay values based on neighboring triggers
- Real-time form validation and feedback
- Partial page reloads (only reload affected sections, not entire page)
- Specific loading messages for different operations

Technical Improvements
- Centralized Vuex store for state management
- Reusable CirculationTriggersForm component
- API clients for libraries, circulation rules, item types, and patron categories
- Consistent async/await pattern throughout
- Separation of repositories, utilities, and services
- Improved naming consistency and code maintainability
- "Dumb" TriggersTable component for better separation of concerns

Test Plan

Prerequisites
1. Log in to staff interface with appropriate permissions (tools_edit_notice_status_triggers)
2. Ensure you have test data: multiple libraries, patron categories, item types, and notice templates
3. Navigate to Administration > Circulation triggers (new interface)

Part 1: Basic Navigation & Interface
1. Verify the new "Circulation triggers" link appears in Administration home under the Circulation section
2. Access the new interface and confirm it loads successfully
3. Verify the default view shows either "All libraries" or your logged-in library based on DefaultToLoggedInLibraryOverdueTriggers system preference
4. Confirm the interface displays a list of triggers organized by context (library/patron category/item type combinations)

Part 2: Viewing and Filtering Rules
1. Toggle between "All exhaustive rules" and "Explicitly set rules" views
  - Verify "All exhaustive rules" shows inherited/fallback rules
  - Verify "Explicitly set rules" shows only rules you've specifically configured
2. Test the patron category filter:
  - Select a specific patron category
  - Confirm only rules for that category are displayed
3. Test the item type filter:
  - Select a specific item type
  - Confirm only rules for that item type are displayed
4. Test combined filters (patron category + item type simultaneously)
5. Clear filters and verify all rules display again

Part 3: Adding New Triggers
1. Click "Add trigger" button
2. Step 1 - Confirm Context:
  - Select Library (required)
  - Select Patron category (required)
  - Select Item type (required)
  - Click "Confirm context"
3. Step 2 - Review Existing Triggers:
  - Verify existing triggers for this context are displayed
  - Note the trigger numbers and delays
4. Step 3 - Configure New Trigger:
  - Enter a delay value (should be constrained by neighboring triggers)
  - Try entering an invalid delay (too low or too high) - verify validation
  - Set "Restricts checkouts" (Yes/No/Fallback to default)
  - Note the placeholder showing the fallback value
  - Select a letter template
  - Verify only templates for the selected library context appear
  - Set "Debarring" options if desired
  - Select "Charged notice fees" if applicable
5. Submit the form
6. Verify the new trigger appears in the list without a full page reload
7. Verify success message is displayed

Part 4: Editing Existing Triggers
1. Locate an existing trigger in the list
2. Click the edit icon/button
3. Verify the form pre-populates with existing values
4. Test delay modification:
  - Try changing the delay within valid bounds - should succeed
  - Try changing delay beyond neighboring triggers - should show validation error
  - Use the increment/decrement buttons to adjust delay
5. Test clearing values:
  - Clear a field (e.g., delay or restrict checkouts)
  - Verify placeholder shows the fallback/default value
  - Submit with cleared value - should save as null and inherit fallback
6. Change the letter template to a different valid template
7. Submit changes
8. Verify the trigger updates in the list without full page reload
9. Verify the changes persist after refreshing the page

Part 5: Deleting Triggers
1. Identify a trigger that is the last in its sequence
2. Click the delete button/icon
3. Verify a confirmation dialog appears
4. Confirm deletion
5. Verify the trigger is removed from the list without page reload
6. Verify the deletion persists after page refresh
7. Test delete constraints:
  - Try to delete a trigger that's not the last in sequence
  - Verify appropriate error/warning message

Part 6: Resetting Rule Sets
1. Locate a context with explicitly set rules (not just inherited)
2. Ensure this is not the only rule set for this trigger sequence
3. Click the "Reset" button for that context
4. Verify confirmation dialog explains what will be reset
5. Confirm the reset
6. Verify the explicit rules are removed and context now shows inherited rules
7. Verify changes persist after page refresh
8. Test reset constraints:
  - Try to reset when it would leave no rules for that trigger
  - Verify appropriate warning/prevention

Part 7: Context-Specific Behavior
1. Test library-specific letter filtering:
  - Create/edit a trigger for a specific library
  - Verify only letter templates with matching branchcode or default templates appear
  - Switch to a different library context
  - Verify the letter list updates accordingly
2. Test default rules:
  - View rules for "All libraries" / "All patron categories" / "All item types"
  - Verify these are labeled as "Default rule for [context]"
  - Confirm these serve as fallback for more specific rules
3. Test DefaultToLoggedInLibraryOverdueTriggers preference:
  - Set preference to "Yes"
  - Reload circulation triggers page
  - Verify it defaults to your logged-in library
  - Set preference to "No"
  - Verify it defaults to "All libraries"

Part 8: Form Workflow & Validation
1. Test required fields:
  - Try submitting add/edit form without selecting library - should fail
  - Try submitting without patron category - should fail
  - Try submitting without item type - should fail
2. Test stepper navigation:
  - Start adding a trigger
  - Verify you must confirm context before proceeding
  - Verify you see existing triggers before editing/adding
  - Verify trigger form only appears after context confirmation
3. Test cancellation:
  - Start adding/editing a trigger
  - Click Cancel
  - Verify you return to the list without changes
  - Verify no partial data was saved

Part 9: Loading States & Messages
1. On slow connections or with many rules:
  - Verify "Loading context..." appears while fetching context data
  - Verify "Loading rule set information..." appears while fetching rules
  - Verify section-specific loading messages appear during updates
2. After form submission:
  - Verify appropriate success/error messages display
  - Verify loading indicators during async operations

Part 10: Permissions
1. Log in as a user without tools_edit_notice_status_triggers permission
2. Verify the "Circulation triggers" link does not appear in Administration
3. Try accessing /cgi-bin/koha/admin/circulation_triggers directly
4. Verify access is denied

Part 11: Data Integrity & Persistence
1. Create a complex set of rules with multiple triggers across different contexts
2. Refresh the page and verify all rules persist
3. Restart the Koha services
4. Verify all rules still display correctly
5. Check the database circulation_rules table:
  - Verify rules are stored with correct rule_name pattern (overdue_1_delay, etc.)
  - Verify null values are properly stored for inheritance
  - Verify the overdue_X_has_rules rule kind is present

Part 12: Edge Cases
1. Test with no existing triggers - verify empty state message
2. Test with maximum trigger sequences (e.g., trigger 1, 2, 3)
3. Test rapid successive edits
4. Test with special characters in letter template names
5. Test browser back/forward buttons during form workflow

Part 13: Comparison with Legacy Interface
1. Access the old interface if still available (via Tools menu)
2. Create a trigger using the new interface
3. Verify it appears correctly in the old interface (if applicable)
4. Create a trigger using the old interface (if still available)
5. Verify it appears correctly in the new interface

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:25 +00:00
Matt Blenkinsop d199a559c0
Bug 10190: Let the circulation_rules endpoint accept query parameters
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:24 +00:00
Martin Renvoize 6ff8024158
Bug 10190: Rework logic of overdue_notices
This patch enhances the overdue_notices script to understand the newly
introduced itemtype level for overdue rules.

The original logic for digesting overdues in the overdue notices script
was flawed given the additional itemtype handling introduced in this
patchset and the way in which it handles effective fallbacks for rules.

This patch tidies the file and reworks the logic so that we fetch
overdues by patron category and branch and then iterate through those
comparing to the correct effective rules and combing them into digest
groups based on notice + trigger level.

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:23 +00:00
Martin Renvoize d4e951b3d8
Bug 10190: Update GetBranchcodesWithOverdueRules to use CirculationRules
This patch updates the C4::Overdues method
GetBranchecodesWithOverdueRules to refer to the new location of these
rules in the circulation rules table. We also update the unit tests to
move the mocked data into the right location.

We could perhaps migrate this method in Koha::CirculationRules at a
later date?

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:22 +00:00
Martin Renvoize 40478e1871
Bug 10190: Add support for incrementable circulation rules
This patch adds support for _X_ grouped circulation rules to both the
internal and external API's for handling circulation rules.

This allows us to group overdue_X_rulename rules by an 'X' integer so we
can have triggers incremented and grouped together.

We also add the new overdue_x_* rule definition here for overdue trigger
rules.

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:21 +00:00
Martin Renvoize ca8fc3d8e9
Bug 10190: Fix unit test to exclude _X_ rules
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:20 +00:00
Martin Renvoize 372c5b8c8e
Bug 10190: Remove OverdueRule Koha Objects
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:19 +00:00
Martin Renvoize 00896a2137
Bug 10190: DBIC Schema update
Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:18 +00:00
Martin Renvoize 7b9bd02726
Bug 10190: DB migration for overduerules
This patch adds the data migration for overduerules to circulation
rules.

We attempt to create the default rules based on most commonly occuring
rule values and then add overrides for all other rule values we find.

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:16 +00:00
Pedro Amorim 0adb49bf2f
Bug 36868: DBRev 26.06.00.021
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:15 +00:00
Lucas Gass b369ba5d17
Bug 36868: Move array splice in OPAC js to after the -1 check
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:14 +00:00
Lucas Gass 5579974cd1
Bug 36868: Remove dead code in opac-user.pl
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:13 +00:00
Brendan Lawlor ae903a008d
Bug 36868: (QA follow-up) Fix deletion off by one error
When the biblionumber is not in the array -1 is returned.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:12 +00:00
Lucas Gass 9b5f75a79e
Bug 36868: (follow-up) Fix deletion when nothing is in the cart, and make DB update better
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:11 +00:00
Lucas Gass fac6a1adfe
Bug 36868: (follow-up) Fix variable names
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:10 +00:00
Lucas Gass d0e2ee1cac
Bug 36868: (follow-up) Remove unused variable in opac-reserve.pl
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:09 +00:00
Lucas Gass 1e8f39e7a6
Bug 36868: Add logic for OPAC
1. APPLY PATCH, updatedatabase, restart_all
2. Search for the new AutoDeleteFromCartWhenHoldPlaced system preference
3. Turn it on for both staff and OPAC
4. From the staff interface put some items in your cart.
5. Place a hold on all of them.
6. Once the hold(s) have been successfully done those items should no longer be in your cart.
7. Put more items into your staff cart, place a hold on some of them but not all.
8. Only the items you have placed a hold on should be removed.
9. Go to the OPAC and put some items in your cart.
10. Place a hold on all of them, those items should now be deleted from the cart.
11. Place more items into your OPAC cart, place a hold on some of them but not all.
12. Only the items you have placed a hold on should be removed.
13. Switch the AutoDeleteFromCartWhenHoldPlaced to only be for 'staff'.
14. Try steps 4-8 again.
15. Switch the AutoDeleteFromCartWhenHoldPlaced to only be for 'opac'.
16. Try steps 9 -12 again.

Signed-off-by: Sam Lau <samalau@gmail.com>

Signed-off-by: Sarah Fletcher <sfletcher@ncwlibraries.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:08 +00:00
Lucas Gass 42ca5a7b6a
Bug 36868: (follow-up) Fix staff interface code
Signed-off-by: Sam Lau <samalau@gmail.com>

Signed-off-by: Sarah Fletcher <sfletcher@ncwlibraries.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:07 +00:00
Lucas Gass 723258423c
Bug 36868: Add system preference AutoDeleteFromCartWhenHoldPlaced
Signed-off-by: Sam Lau <samalau@gmail.com>

Signed-off-by: Sarah Fletcher <sfletcher@ncwlibraries.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-10 14:24:03 +00:00
Lucas Gass 4d50205d4a
Bug 42722: Add noItemsOnBib error message
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 15:50:03 +00:00
Eric Phetteplace f87ee4aecb
Bug 42722: Improve invalidPickupBranch error message
Ensure you have the patch from Bug 41879 and follow its test plan.
You should see the message 'Target item or record has an invalid
pickup location. Either move the hold onto a valid item type or
select a valid pickup location.' when attempting an invalid hold.

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 15:50:01 +00:00
Owen Leonard 4c0424994c
Bug 42685: Capitalization corrections in some ILL files
This patch makes two capitalization corrections in files related to ILL:

"ISO18626 Messages" -> "ISO18626 messages"
"Requesting Agencies" -> "Requesting agencies"

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 15:50:00 +00:00
Andrii Nugged d64d02761d
Bug 43344: Use the current source alias in filter_by_active
Bug 36018 qualified the quantity column with the default "me" alias to
avoid ambiguity when joined tables also have a quantity column.

DBIx::Class does not always use "me" for related resultsets. The
many-to-many item_orders relationship uses "ordernumber", so evaluating
item.orders.filter_by_active from the OPAC refers to a non-existent
me.quantity column.

Use current_source_alias to qualify quantity with the alias of the actual
resultset. Add regression coverage for filtering active orders through the
item relationship.

Test plan:
Before applying the patch:
1. Set OPACAcquisitionDetails to Show.
2. Set AcqCreateItem to create an item when placing an order.
3. Create a basket and add an order for a bibliographic record, including an
   item.
4. Close the basket so the order is active and outstanding.
5. Open the bibliographic record in the OPAC.
6. Confirm the item is not shown as On order and the log contains an Unknown
   column 'me.quantity' error.

Apply the patch.

After applying the patch:
1. Open the same bibliographic record in the OPAC again.
2. Confirm the item is shown as On order and the SQL error is gone.

Co-Authored-By: Loontik <loontik@nugged.team>
Signed-off-by: Olivia Reynolds <olivia.reynolds@openfifth.co.uk>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 15:49:59 +00:00
Slava Shishkin 0d80c7d440
Bug 43296: Support valid patron attribute codes in circulation stats
Circulation statistics only recognise numeric patron attribute codes,
while patron attribute type codes are not restricted to numbers.

Validate Row, Column and Filter_borrower_attributes against the available
patron attribute types. Use generated SQL aliases and pass attribute
codes as bind parameters.

Test plan:
1. In Administration > Patron attribute types, create attribute types with codes
   SHOW-BCODE, SHOW.BCODE, SHOW BCODE and SQL); DROP TABLE borrowers; --..
2. Go to Reports > Statistics wizards > Circulation.
3. Use each attribute as Row and Column with an ordinary field on the other axis.
4. Confirm the report fails or uses the wrong field.

Apply the patch.
5. Restart Plack.
6. Repeat steps 3-4.
7. Confirm the reports succeed and use the selected patron attribute.
8. Test the same attributes as filters.
9. Confirm ordinary Row, Column and Filter fields still work.

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 15:49:58 +00:00
Owen Leonard 6de2c57c27
Bug 43325: Remove event attribute from point of sale receipt window
This patch removes event attributes from the point of sale receipt
template in favor of adding the "close_window" class to the links. The
class has a globally-defined event handler for the close window
operation

To test, apply the patch and enable the EnablePointOfSale system
preference if necessary.

- If necessary, got to Administration -> Debit types and create at least
  one debit type which has the "Can be sold" checkbox checked.
- Go to the point of sale page and add at least one item to purchase.
- Fill out the "Collect payment" form and confirm.
- When the payment is received, click the "Print receipt button" in the
  dialog.
- On the receipt window which opens, click the "Close" button. The
  window should close.
- Open the receipt window again and change the "accountlines_id"
  parameter in the URL to an accountlines ID which doesn't exist
- The page should show an error message instead of a receipt. Test the
  "Close" button in this view.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 15:49:57 +00:00
Owen Leonard 685be2e643
Bug 43319: Remove event attribute from library_groups.tt
This patch updates the library groups administration page so that an
event attribute is not necessary. We don't need an onclick event handler
to submit the form--we can just use a submit button as the menu item.

To test, apply the patch and go to Administration -> Library groups.

- If necessary, create a new library group.
- From the list of groups, click an "Add library" menu button and then
  click one of the libraries which appear as menu items.
- The page should reload and the library you selected should now show up
  as being part of that library group.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 15:49:56 +00:00
Matt Blenkinsop ec1691b59d
Bug 43320: (QA follow-up) Add html filter and fix translatability
Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 15:49:55 +00:00
Nick Clemens 8e663ae7bd
Bug 43320: OPAC adjustments
Similar test plan, but on the OPAC

Signed-off-by: Phil Ringnalda <phil@chetcolibrary.org>
Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 15:49:53 +00:00
Nick Clemens 403288c5cd
Bug 43320: Don't quoted already quoted searches and add feedback
Bug 31213 added a quoted search when no results were found,
this patch builds on that to:
1 - Notify the user when their search is repeated quoted
2 - Not repeat a search when already quoted

To test:
 0 - Set system preferences
     RedirectToSoleResult to 'Don't redirect'
     IntranetAddMastheadLibraryPulldown to Show
     IntranetCatalogSearchPulldown to Show
 1 - Add the sample records to your catalog
 2 - Search for the quoted phrase "happy family big" in the 'title' index
 3 - Note you get a result
 4 - Check the record - the phrase is not found, but the record contains all the terms
 5 - Search for the unquoted phrase: 6:20 man
 6 - Confirm search is quoted and result found
 7 - Apply patch, restart all
 8 - Repeat 5
 9 - Confirm you are now told search was quoted and result found
10 - Repeat 2
11 - Confirm no result found, no notice of repeated search

Signed-off-by: Phil Ringnalda <phil@chetcolibrary.org>
Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 15:49:52 +00:00
Nick Clemens b37d6989e5
Bug 43448: Remove meaningless conditional
We are in a conditional based on AllowHoldPolicyOverride being enabled,
a second conditional with an OR on the value will always be true

To test:
 1 - Do not apply patch
 2 - Disable AllowHoldPolicyOverride
 3 - Create a biblio B with an item I at library A.
 4 - Configure this library A to not be a pickup location
 5 - Add a "Default holds policy by item type" for item I type where "Hold
    pickup library match" is "item's home library"
 6 - Try to place a hold on biblio B
    You should not be able to place a hold because there is no valid
    pickup locations
 7 - Enable AllowHoldPolicyOverride
 8 - Try to place a hold on biblio B
    You should now see all valid pickup locations in a dropdown list
    (with an exclamation mark in front of each option) with none selected
    by default
 9 - Verify you can place a title-level hold and an item-level hold
10 - Apply patch, repeat test plan, verify nothing has changed

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 15:49:49 +00:00
Jacob O'Mara 6b0dcc83d9
Bug 39658: Schema: Add Koha object class mappings
Map PatronAccountLink to Koha::Patron::AccountLink(s).
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:44 +00:00
Pedro Amorim 50efc570df
Bug 39658: dbic
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:43 +00:00
Pedro Amorim f1b3ce6f30
Bug 39658: DBRev 26.06.00.020
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:42 +00:00
Jacob O'Mara 3e483cd61a
Bug 39658: (follow-up) block checkout when linked accounts over charge limit
When AllowFineOverride is disabled and NoIssuesChargeLinkedAccounts
is exceeded, hide the barcode field and show 'Cannot check out'.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:41 +00:00
Jacob O'Mara bfd2474a1a
Bug 39658: (follow-up) linked hold pickup all types
Linked account hold pickup check now applies to all hold
statuses (Reserved, Transferred, Processing), not just Waiting.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:39 +00:00
Jacob O'Mara 6d245a060e
Bug 39658: Add link_to_patron and linked_account_links methods
- link_to_patron($other_patron): Links patrons, handles group logic
- linked_account_links(): Returns all links in patron's group
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:38 +00:00
Jacob O'Mara 6b1e0a4fc9
Bug 39658: Add Koha::Exceptions::PatronAccountLink
Add typed exceptions for patron account linking:
- AlreadyLinked: patrons already in same group
- DifferentGroups: patrons in different groups
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:37 +00:00
Jacob O'Mara 77057b3628
Bug 39658: Tests: Add API tests for patron account links endpoints
Test GET/POST/DELETE /api/v1/patrons/{id}/account_links.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:36 +00:00
Jacob O'Mara 1cb693317e
Bug 39658: Tests: Add unit tests for Koha::Patron::AccountLink
Test linking, group management, and linked_accounts methods.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:35 +00:00
Jacob O'Mara f41cd57410
Bug 39658: Admin: Add linked account system preferences UI
Add prefs for EnablePatronAccountLinking and related settings.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:34 +00:00
Jacob O'Mara 23de8ce68e
Bug 39658: Patron detail UI: Add linked accounts section
Display linked patrons with Add/Leave link group buttons.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:33 +00:00
Jacob O'Mara fa8a860a49
Bug 39658: Patron detail: Pass linked accounts to template
Add linked_accounts and link_group data for display.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:32 +00:00
Jacob O'Mara 2b210d6da8
Bug 39658: Circulation UI: Add linked account fee/hold messages
Display warnings for combined debt and linked hold pickup.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:31 +00:00
Jacob O'Mara 81b62e4bdf
Bug 39658: Circulation: Add linked account hold pickup redirect
Enable linked patrons to pick up each other's holds when allowed.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:29 +00:00
Jacob O'Mara 454714a56e
Bug 39658: Circulation: Add linked account fee/hold checks
DEBT_LINKED_ACCOUNTS blocker and LINKED_ACCOUNT_HOLD_PICKUP alert.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:28 +00:00
Jacob O'Mara 194b5388a3
Bug 39658: API: Add REST endpoints for patron account links
Endpoints: GET/POST/DELETE /patrons/{id}/account_links
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:27 +00:00
Jacob O'Mara 6fa77edc48
Bug 39658: Add linked account methods to Koha::Patron
Methods: account_link, linked_accounts, all_linked_borrowernumbers,
linked_accounts_debt. Also adds charge limit checking.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:26 +00:00
Jacob O'Mara 3f83d79424
Bug 39658: Add Koha::Patron::AccountLink(s) objects
New objects to manage patron account links.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:25 +00:00
Jacob O'Mara 98ee7e3f0d
Bug 39658: DB: Add patron_account_links table and sysprefs
Adds table and 3 sysprefs for patron account linking feature.
Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Kellie Stephens <kellie.stephens@bywatersolutions.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:24 +00:00
Martin Renvoize 9371504d98
Bug 30144: (QA follow-up) Only show servicing instructions when EDIFACT is available
The new servicing instructions UI on the "add order" form was showing
unconditionally, even when the EDIFACT system preference is disabled
or the vendor has no EDI account configured. Elsewhere in acquisitions
(e.g. acqui/basket.tt) EDI-only UI is gated on
Koha.Preference('EDIFACT') && ediaccount, so apply the same check here.

Test plan:
1. With EDIFACT disabled, or EDIFACT enabled but the vendor has no
   entry in vendor_edi_accounts, open Acquisitions > a basket > Add
   order (neworderempty.pl)
   => No "Servicing instructions" field is shown
2. Enable EDIFACT and add a vendor_edi_accounts row for the vendor
   => The "Servicing instructions" field appears and behaves as before

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:23 +00:00
Martin Renvoize b210605ef7
Bug 30144: (QA follow-up) Fix t/Ediorder.t for JSON servicing_instruction format
Bug 30144 changed Koha::Edifact::Order::gir_segments() to expect
servicing_instruction as a JSON-encoded array of instruction groups
(added by Bug 41709 while working with the older plain-string format
that predated Bug 30144's change was never adapted to match).

As a plain string no longer decodes as JSON, gir_segments() silently
skipped the servicing instruction elements, leaving the GIR split and
special-character tests comparing against undef.

Test plan:
1. prove t/Ediorder.t
   => 4 tests failed before this patch, all pass after
2. prove t/db_dependent/Koha/Edifact/Order.t
   => still passes

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-09 12:07:21 +00:00
Jonathan Druart 58cda28f6a
Bug 41996: Fix kohastructure.sql
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-07 10:46:28 +00:00
Jonathan Druart 3594c6f60c
Bug 16631: Fix kohastructure.sql
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-07 10:46:27 +00:00
Jonathan Druart f612fda1e9
Bug 41995: Add nonce
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:24:14 +00:00
Jonathan Druart 76504e598a
Bug 41995: Remove extra JS code
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:24:12 +00:00
Jonathan Druart dd536cae2e
Bug 41995: Fix conflict with bug 37762
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:24:11 +00:00
Jonathan Druart 061cc915ad
Bug 41995: Add autocomplete on the admin home page
Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:24:10 +00:00
Jonathan Druart c9cfa985b6
Bug 41995: Autocomplete for syspref search
This patch adds autocompletion to the syspref search in the header.

Test plan:
1. Go to the admin home page - /cgi-bin/koha/admin/admin-home.pl
2. In the header start typing the name of a syspref and notice that
   there is now autocompletion (you need at least 3 chars)
3. Select an entry and confirm that you are correctly redirected to the
   correct search result

Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:24:09 +00:00
Pedro Amorim 52e6e3ad53
Bug 41728: (RM follow-up): Add AddReturn marks ISO18626 request as LoanCompleted
Regression test from the RM's QA review (attachment 204865, comment
104/105 on the bug): drives a real ISO18626 request through AddReturn
and confirms it gets marked LoanCompleted and receives a
supplyingAgencyMessage, rather than only checking that the $messages
hashref no longer contains a stray NotIssued key. This closes the gap
the "Remove leftover duplicate data-consistency block" follow-up left
- that fix restored the $iso18626_request assignment, but nothing
previously exercised the ISO18626 progress-reporting path end to end.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>

Test plan:
1. prove t/db_dependent/Circulation.t
=> SUCCESS: 91 tests pass.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:24:07 +00:00
Martin Renvoize e0d32e073a
Bug 41728: (QA follow-up) Add coverage for the Wrongbranch blocker
No test in the suite asserted on $messages->{Wrongbranch} content
despite this being one of the blockers AddReturn's rewired
availability check produces (AllowReturnToBranch enforcement, moved
into Koha::Item->can_be_returned_at / Koha::Item::Availability::Checkin
by this bug). Add coverage for it.

Test plan:
1. prove t/db_dependent/Circulation.t
=> SUCCESS: 88 tests pass.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:24:06 +00:00
Martin Renvoize 8bcd634fb9
Bug 41728: (QA follow-up) Remove leftover duplicate data-consistency block
d43945e7279 moved the issued/patron data-consistency check earlier in
AddReturn (before blocker handling), as its commit message describes,
but only copied the block instead of deleting the original. The
leftover block ran unconditionally afterwards: since $patron is
already guaranteed set whenever $issue is true by that point, its
"else" branch fired on every normal checkin of an already-issued item,
incorrectly setting $messages->{NotIssued} and clearing item.onloan.
This corrupted the not_issued message surfaced to circ/returns.pl, SIP,
and the checkin audit record on every ordinary return.

The same leftover block also buried $iso18626_request's assignment
after a die (making it dead code), so ISO18626 progress-reporting on
checkin was never triggered. Restore the assignment at the point
$issue/$patron are extracted from the availability context, where it
belongs.

Test plan:
1. Return an item that is currently checked out.
2. Confirm the returned $messages hashref no longer contains a
   NotIssued key, and that item.onloan is only cleared for items that
   were genuinely not checked out.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:24:05 +00:00
Tomás Cohen Arazi ce9ffcca52
Bug 41728: (follow-up) Replace require with use for Availability::Checkin
There is no circular dependency, so the runtime require can be
replaced with a compile-time use statement.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:24:03 +00:00
Martin Renvoize fbe7c7a60f
Bug 41728: (QA follow-up) Move Item::Checkin::Availability to Item::Availability::Checkin
Introduce a dedicated Koha::Item::Availability::* namespace for checkers
that return a Koha::Result::Availability. Keeping these under the Item
subject prefix keeps them co-located with the Item they describe, while
the Availability sub-namespace gives future parallel checkers
(Checkout, Hold, Book, ...) an obvious home rather than scattering them
as single-file directories alongside Koha/Item/Transfer/ etc.

The $item->checkin_availability convenience method on Koha::Item is
preserved; only its internal delegation target changes.

Renames:
  Koha::Item::Checkin::Availability -> Koha::Item::Availability::Checkin
  Koha/Item/Checkin/Availability.pm -> Koha/Item/Availability/Checkin.pm
  t/db_dependent/Koha/Item/Checkin/Availability.t
    -> t/db_dependent/Koha/Item/Availability/Checkin.t

Test plan:
  prove t/db_dependent/Koha/Item/Availability/Checkin.t
  prove t/db_dependent/Circulation/Returns.t

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:24:02 +00:00
Martin Renvoize be74523028
Bug 41728: (QA follow-up) Move Availability::Result to Result::Availability
Consolidate result base classes under the Koha::Result::* namespace
alongside the existing Koha::Result::Boolean. This keeps return-value
wrappers discoverable in one place and avoids a lone Koha::Availability::
namespace that would otherwise host a single base class.

Renames:
  Koha::Availability::Result -> Koha::Result::Availability
  Koha/Availability/Result.pm -> Koha/Result/Availability.pm
  t/Koha/Availability/Result.t -> t/Koha/Result/Availability.t

Call sites in Koha::Item, Koha::Item::Checkin::Availability, and the
test files are updated accordingly.

Test plan:
  prove t/Koha/Result/Availability.t
  prove t/db_dependent/Koha/Item/Checkin/Availability.t

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:24:01 +00:00
Tomás Cohen Arazi dece71cb3e
Bug 41728: (follow-up) Add no_short_circuit option to checkin availability
This follow-up replaces the unconditional removal of short-circuit logic
with a caller-controlled no_short_circuit parameter, following the same
pattern established by Koha::Patron->can_place_holds.

By default, check() short-circuits on the first blocker. This is the
correct behavior for AddReturn, which processes blockers sequentially
and only acts on the first one it finds (BlockedWithdrawn => early return,
Wrongbranch => early return, BlockedLost => sets doreturn=0). Running all
policy checks only to discard the results is wasteful, specially when
some of them (e.g. can_be_returned_at) hit the database.

When no_short_circuit => 1 is passed, all checks run and all blockers
are collected. This is the behavior API consumers will need: present the
full set of issues in a single response instead of requiring
fix-one-recheck cycles.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
  k$ prove t/db_dependent/Koha/Item/Checkin/Availability.t
=> SUCCESS: Tests pass!
  k$ prove t/db_dependent/Circul*
=> SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:59 +00:00
Martin Renvoize 84bcca727a
Bug 41728: (QA follow-up) Collect all blockers instead of short-circuiting
Remove early returns from Koha::Item::Checkin::Availability::check() so
that all blockers, confirmations, and warnings are collected in a single
pass. Previously, the first blocker triggered an early return, meaning
callers could only discover one problem at a time.

This is particularly useful for API consumers who want to present the
full set of issues to the user in a single response rather than
requiring fix-one-recheck cycles.

The change is safe for AddReturn because it already handles blocker
priority through its sequential if/return structure:
  1. BlockedWithdrawn → early return before status updates
  2. Wrongbranch → early return after status updates
  3. BlockedLost → sets doreturn=0

Even when multiple blockers are present, AddReturn processes them in
the correct priority order.

Also makes existing tests deterministic by explicitly setting withdrawn
and itemlost values on test items, preventing random TestBuilder values
from triggering unrelated blockers now that all checks always run.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:58 +00:00
Martin Renvoize 8391917bab
Bug 41728: (QA follow-up) Restore withdrawn warning and use from availability
Restore the withdrawn warning in Koha::Item::Checkin::Availability that
was incorrectly removed. This warning is set when an item is withdrawn
but BlockReturnOfWithdrawnItems is off, making it useful for API
consumers in subsequent work.

Update AddReturn to read the withdrawn state from the availability
result's warnings rather than re-checking $item->withdrawn directly,
avoiding duplication of logic.

Add test for the withdrawn warning (non-blocked) path.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:57 +00:00
Martin Renvoize 24854abf30
Bug 41728: (QA follow-up) Move return policy logic to Koha::Item
Move the AllowReturnToBranch and branch transfer limit checks from both
C4::Circulation::CanBookBeReturned and
Koha::Item::Checkin::Availability::_check_return_policy into a single
authoritative method on Koha::Item: can_be_returned_at().

This eliminates the duplicate implementations and follows the Koha
convention of placing item-related logic on the Koha::Item object.

Changes:
- Add Koha::Item->can_be_returned_at() with AllowReturnToBranch policy
  and transfer limit checks
- Make CanBookBeReturned a thin wrapper delegating to the new method
- Update Checkin::Availability to call $item->can_be_returned_at()
- Remove _check_return_policy and unused withdrawn warning from
  the Availability class

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:56 +00:00
Martin Renvoize b689dd07cc
Bug 41728: (QA follow-up) Restore original execution order for Wrongbranch
The refactoring changed the relative order of the Wrongbranch early
return and several status-update operations. In the original AddReturn,
the execution order was:

  1. checkout/patron lookup
  2. not-issued handling + RecordLocalUseOnReturn
  3. withdrawn check (early return)
  4. location_update_trigger
  5. UpdateNotForLoanStatusOnCheckin
  6. CanBookBeReturned / Wrongbranch check (early return)
  7. BlockReturnOfLostItems

After the refactoring, the Wrongbranch blocker returned early before
steps 2, 4 and 5, causing three regressions:

  - RecordLocalUseOnReturn was not recorded for non-issued items
    scanned at the wrong branch
  - location_update_trigger was skipped for Wrongbranch returns
  - UpdateNotForLoanStatusOnCheckin was skipped for Wrongbranch returns

Fix by splitting blocker handling into two phases:

  1. BlockedWithdrawn: earliest early return, before status updates
     (matches original withdrawn check position)
  2. Wrongbranch and BlockedLost: after RecordLocalUseOnReturn,
     location_update_trigger, and UpdateNotForLoanStatusOnCheckin
     (matches original CanBookBeReturned position)

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:54 +00:00
Martin Renvoize d46b0c1106
Bug 41728: (QA follow-up) Move data consistency check before blocker handling
The data inconsistency die (checkout exists but patron does not) was
placed after the blocker handling block. This meant that if an item had
corrupt data AND triggered a blocker (e.g. withdrawn+blocked), the
blocker handler would return early with patron as undef instead of dying
on the inconsistency, silently masking database corruption.

Move the check to immediately after extracting context from the
availability result, before any blocker handling, matching the original
AddReturn behavior where the die fired during checkout lookup.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:53 +00:00
Kyle M Hall 2e4459ea69
Bug 41728: (QA follow-up) Declare issue and patron variables together
There seems to be no reason to declare $patron near the start of the sub
while $issue is declared where they are both populated.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:52 +00:00
Kyle M Hall dad9fd4a30
Bug 41728: (QA follow-up) Fix test accessing wrong key for checkout
The test was checking $result->{issue} which doesn't exist on the
Result object. The correct accessor is $result->context->{checkout},
so the assertion was always trivially true.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:51 +00:00
Kyle M Hall 402b6559f8
Bug 41728: (QA follow-up) Match original blocker priority order
Reorder blocker checks to match the original AddReturn priority:
Withdrawn > Wrongbranch > Lost. Previously Lost was checked before
Wrongbranch, which changed behavior for items that were both lost
and at the wrong branch.

QA note: There are other checks in-between these, but I don't think
their order betwixt these blockers matters for retaining existing
behavior.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:49 +00:00
Kyle M Hall e822660311
Bug 41728: (QA follow-up) Populate checkout context before blocker checks
The availability check is returning early on blockers (BlockedWithdrawn,
BlockedLost, Wrongbranch) before checking if the item had an active
checkout. This causes AddReturn to lose checkout/patron context for
checked-out items that hit a blocker, resulting in incorrect NotIssued
messages, unintended onloan clearing, and incorrect local use recording.

Move the checkout check to run first so context is always populated
regardless of blockers.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:48 +00:00
Tomás Cohen Arazi cb3deeadcc
Bug 41728: (follow-up) Add transfer limit check and clean param names
- Rename params: branch => library, add to_library for destination
- Move $returnbranch computation before the availability check in
  AddReturn so it can be passed as to_library
- Add transfer limit check (bug 7376) to the Availability class
- Rename internal _can_book_be_returned to _check_return_policy
- Remove duplicate $returnbranch block and standalone transfer
  limit check from AddReturn
- Update tests for new param names

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:47 +00:00
Tomás Cohen Arazi 4b277a6e4a
Bug 41728: Make AddReturn use Koha::Item::Checkin::Availability
This patch refactors AddReturn to use the new Koha::Item::Checkin::Availability
class for validation logic, eliminating code duplication and centralizing
check-in validation.

Changes:
- Add use statement for Koha::Item::Checkin::Availability
- Look up item first, return BadBarcode if not found
- Call $item->checkin_availability() for validation
- Use Result object methods (available(), blockers, context)
- Replace inline withdrawn/lost/branch validation with blocker handling
- Remove duplicate CanBookBeReturned() call (now in Availability class)
- Preserve all existing behavior and error messages
- Maintain backward compatibility

The refactoring extracts ~40 lines of validation logic while maintaining
identical functionality. All validation results are obtained from a single
availability check that returns a Result object.

Note: The withdrawn message is set in two places depending on context:
- When BlockReturnOfWithdrawnItems is ON: set in blocker handler before early return
- When BlockReturnOfWithdrawnItems is OFF: set by existing check later in the flow
This preserves the original behavior where withdrawn items always get the
message, but are only blocked when the syspref is enabled.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
  k$ prove t/db_dependent/Koha/Item/Checkin/Availability.t
=> SUCCESS: Tests pass!
  k$ prove t/db_dependent/Circul*
=> SUCCESS: Tests pass!
3. Verify check-in operations work correctly:
   - Items not found return BadBarcode
   - Withdrawn items blocked when syspref enabled
   - Lost items blocked when syspref enabled
   - Wrong branch returns blocked per AllowReturnToBranch
   - Not issued items handled correctly
   - Normal check-ins complete successfully
4. Sign off :-D

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:45 +00:00
Tomás Cohen Arazi 9b8328c3f5
Bug 41728: Add Koha::Item::Checkin::Availability class
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:44 +00:00
Tomás Cohen Arazi b5041ae361
Bug 41728: Add Koha::Availability::Result base class
This patch introduces a standardized result class for availability checks,
providing a clean API with proper methods instead of raw hashrefs.

The Koha::Availability::Result class provides:
- add_blocker() - Add conditions that prevent the action
- add_confirmation() - Add conditions requiring user confirmation
- add_warning() - Add informational messages
- set_context() - Store related objects (item, checkout, patron, etc.)
- available() - Check if action can proceed (no blockers)
- needs_confirmation() - Check if confirmations are required
- Accessor methods for all categories
- to_hashref() for backward compatibility

This design:
- Provides a consistent interface across all availability checks
- Makes the API more discoverable and self-documenting
- Enables future enhancements without breaking changes
- Follows OO principles with proper encapsulation
- Can be used as base for checkout, hold, renewal availability

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
  k$ prove t/Koha/Availability/Result.t
=> SUCCESS: Tests pass!
3. Verify all methods work correctly
4. Sign off :-D

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:43 +00:00
Tomás Cohen Arazi 78906e00da
Bug 42443: (QA follow-up) Use POST /transfers/{transfer_id}/cancellation
Cancelling a transfer is a state change on the resource, not a deletion of
it, so it should be modelled as an action sub-resource rather than a DELETE
on the transfer itself. This aligns the endpoint with the API coding
guidelines (REST2: non-CRUD actions use action sub-resources; REST3.2:
conflicts return 409) and with the design used in bug 43454.

Changes:
- Replace DELETE /transfers/{transfer_id} with
  POST /transfers/{transfer_id}/cancellation
- Rename the controller method delete() to cancel(), look the transfer up
  with objects->find_rs, and return 200 with the updated resource
  representation instead of 204 with an empty body
- Make cancellation_reason optional, defaulting to 'Manual', matching the
  legacy returns.pl behaviour
- Return 409 with a structured error_code ('already_arrived',
  'already_cancelled') when the transfer cannot be cancelled, instead of a
  bare 400
- Update the transfers to receive page to POST to the new endpoint and to
  treat 404/409 as "already gone, refresh the table"
- Adapt the tests accordingly, including coverage for the default reason
- The *_date API attribute names introduced by this bug are kept, as they
  are the guideline-compliant form (REST1.3.4.1: *_date, not date_*)

Test plan:
1. Apply patch
2. Run the tests:
   $ ktd --shell
  k$ prove t/db_dependent/api/v1/transfers.t
=> SUCCESS: Tests pass!
3. Rebuild and validate the API spec:
  k$ yarn api:bundle
  k$ prove xt/api.t
=> SUCCESS: Tests pass!
4. In the staff interface, go to Circulation -> Transfers to receive,
   click "Cancel transfer" on a row
=> SUCCESS: The transfer is cancelled and the row drops off the table
5. Cancel the same transfer again from a second tab
=> SUCCESS: The API returns 409 (already_cancelled) and the row is
   treated as already gone
6. Sign off :-D

Assisted-by: Sonnet 4.6 (Anthropic)
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:41 +00:00
Martin Renvoize d7a87e2cbd
Bug 42443: (QA follow-up) Use library selects for Sent from and Home library filters
Replace the free-text filter inputs for the "Sent from" and "Home library"
columns with select dropdowns populated from the full library list, consistent
with other DataTables pages such as the pending bookings page.

The "Sent from" column is made visible again (it was previously hidden since
rowGroup provides the visual grouping); showing it also allows its filter cell
to render.

The "On hold for" column intentionally has no filter: it is computed from two
different embedded sub-objects (item.first_hold.patron or item.recall.patron)
and has no single API field to filter against server-side.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:40 +00:00
Martin Renvoize dcb423b766
Bug 42443: (QA follow-up) Add DBIC relationship aliases on Branchtransfer
The REST framework resolves _order_by parameters and embed prefetches by
walking prefetch_whitelist, which is built from DBIC relationship names.
The auto-generated relationships use the FK column names (itemnumber,
frombranch, tobranch), but the API exposes them as item, from_library,
and to_library. Sorting by item.biblio.title etc. threw a 500 because
those names were not in the whitelist.

Adding belongs_to aliases below the DO NOT MODIFY line bridges the API
names to the DBIC names, matching the convention used by other Koha
schema result classes (e.g. Koha::Schema::Result::Item).

Also updates the relation methods in Koha::Item::Transfer to use the new
aliases consistently, so the accessor names line up across both layers.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:39 +00:00
Martin Renvoize ebb77c8fa9
Bug 42443: (QA follow-up) Group transfers by sending library using rowGroup
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:38 +00:00
Martin Renvoize e5a07ccd25
Bug 42443: (QA follow-up) Test from_library_id query filter
The OpenAPI spec documents from_library_id as a supported filter
parameter but the test suite only exercised to_library_id. Add a test
to verify filtering by source library returns the correct transfer.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:36 +00:00
Martin Renvoize 02a02c7209
Bug 42443: (QA follow-up) Test UseRecalls integration in transfer cancel
The delete() controller reverts any in-transit recall tied to the item
when UseRecalls is enabled. Add a subtest to verify the recall status
is set back to 'requested' after the transfer is cancelled.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:35 +00:00
Martin Renvoize 19d1db91d2
Bug 42443: (QA follow-up) Fix escaping of branchcode in JS context
Using | html to embed a TT value inside a JS string literal is
incorrect: it produces HTML entities rather than JS-safe characters.
Store the branchcode in a data attribute (where | html is correct)
and read it from there in JavaScript.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:34 +00:00
Martin Renvoize 16b2ac663e
Bug 42443: (QA follow-up) Guard nullable embed access in JS renderers
from_library and home_library can be null if a transfer references an
orphaned branch row. Accessing .name on null throws a TypeError and
crashes the DataTable renderer; guard both accesses.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:33 +00:00
Martin Renvoize f80b599332
Bug 42443: (QA follow-up) Add to_library to embed enum
Koha::Item::Transfer exposes a to_library() method and the definition
already listed to_library as a valid property, but the x-koha-embed
enum was missing the entry. Add it so consumers can embed it.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:31 +00:00
Martin Renvoize f8d59a7b50
Bug 42443: (QA follow-up) Prevent cancelling completed transfers
The delete endpoint was passing force => 1 to cancel() with no state
guards, allowing already-received and already-cancelled transfers to
be re-cancelled, corrupting datearrived/datecancelled audit fields.

Add explicit 400 responses for both cases and tests to cover them.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:30 +00:00
Kyle M Hall c813fb2574
Bug 42443: Add ability to cancel a transfer via the REST API
This patch adds a DELETE /transfers/{transfer_id} route that cancels the
given transfer by id and reverts an in transit recall for the item the same
way as returns.pl does. The cancellation reason is passed in the request body
so this api endpoint could be used in other places we can cancel transfers.

Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:29 +00:00
Kyle M Hall 560d7b7ff7
Bug 42443: Convert transfers to receive page to ajax datatable
Updates circ/transferstoreceive.pl to use the new REST API with
an ajax DataTable to display transfer to receive.

Test Plan:
1) Apply this patch
2) Restart all the things!
3) Set an item in transit to your logged in library ( check it in at another
   branch so it is sent home, or place a hold to trigger a transfer )
4) Place a hold on a second transferred item, and a recall on a third
5) Browse to /cgi-bin/koha/circ/transferstoreceive.pl
6) Note the "On hold for" column shows the hold patron, the recall patron or
   "None"
7) Set TransfersMaxDaysWarning low and note the "days late" badge appears
8) Click "Cancel transfer" and note the transfer is cancelled
9) A library with no incoming transfers will get "No transfers to receive"

Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:28 +00:00
Kyle M Hall 4fc5617615
Bug 42443: Add unit tests
Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:26 +00:00
Kyle M Hall 9dac6290d7
Bug 42443: Add REST API for item transfers
Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:25 +00:00
Lisette Scheer 2aa2cc0e7f
Bug 42330: (QA follow-up) Fix file permissions on new test and tidy
This patch
- Fixes the file permissions on the new test
- Removes a variable declaration from the new test that's unused
- Removes a new line from the end of staff-global.scss

Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:24 +00:00
Jonathan Druart 6cb0f5161c
Bug 42330: Auto-tidy
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:23 +00:00
Jonathan Druart fc32990f49
Bug 42330: Add new template tools/restore-records.tt
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:21 +00:00
Jonathan Druart 210aa964f0
Bug 42330: Add an xt test to prevent new occurrences of <main> in staff templates
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:20 +00:00
Owen Leonard 248c0fe224
Bug 42330: (follow-up) Some corrections to sidebars
- The structure of the search facets, advanced MARC editor, and
  HTML customiztions sidebars need to be corrected in order for
  the collapse button to work correctly.
- The "toggle_sidebar" click handler should be inside a
  "$(document).ready()" in order to work correctly on pages where the JS
  is in the header.

Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:19 +00:00
Jonathan Druart ad694a3411
Bug 42330: Tidy
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:17 +00:00
Jonathan Druart b02ca1270d
Bug 42330: Use main-container from all our templates
This goal here is to uniformize the structure of our templates.
Using main-container.inc will also introduce the sidebar collapse (from
bug 42322) where it was missing

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-04 11:23:13 +00:00
David Cook 0acaa7fd55
Bug 43202: Make it easier to find ERM system preferences
This patch makes key ERM system preferences contain the words
ERM, e-resource, and electronic resource to improve discoverability
when searching system preferences.

Test plan:
0. Don't apply the patch yet
1. Go to Administration and type "electronic" into "Search system
   preferences" and search
2. Note you get the message "No system preferences matched your search
   for: electronic"
3. Apply the patch
4. Search for "electronic" in the system preferences again
5. Note that "ERMModule" and "ERMProviders" appear

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:54:04 +00:00
Aleisha Amohia 37e6568a9b
Bug 32419: Show item availability on lists view
To test:

1. Create or use a biblio record with multiple items, for example biblionumber 235.
2. Check out an item attached to the record to a patron, i.e. check out barcode 39999000010190 to your logged in user
3. Go to the Detail view for this record.
4. Edit another item attached to the record and give it a not for loan status, i.e. edit barcode 39999000010213 and set it to Not for loan
5. Click the Add to list button above the record. Add the record to an existing list, or create a new one and Save.
6. Go to the Lists module in the staff interface and view the contents of the list you just added your record to.
7. Confirm you see accurate Availability information in the list results within the callnumber column.

Sponsored-by: Pymble Ladies' College
Signed-off-by: Hayley Cheng <hacheng@pymblelc.nsw.edu.au>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:54:03 +00:00
Baptiste Wojtkowski e827290b9b
Bug 41742: Replace depth attribute with budget_level in acquisition
Test Plan:
1. Pre-checks:
   a. Observe plack-intranet-error.log.
   b. Make sure you have one fund with a subfund which also have a
   subfund.
2. After each followings, verify there are new logs in
   plack_intranet-error.log, check that the fund dropdown contains no ">"
   a. In staff interface go to Acquisitions module.
   b. Navigate to Advanced Search
   c. In a basket create an order by clicking on "Add to basket" ->
   "From existing orders (copy)"
3. Apply patch
4. run prove t/db_dependant/Budgets.t
5. Redo 2. and notice there is no log anymore and subfunds are marked
   with >, subsubfunds with >>

Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:54:02 +00:00
Baptiste Wojtkowski dbf3327cdb
Bug 41742: Remove CheckBudgetParentPerm
This function is not used in the code anymore

Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:54:01 +00:00
Paul Derscheid 987f302777
Bug 42997: (QA follow-up) Keep on-site checkouts due at the end of the day
circulation.pl still replaced the due date of an on-site checkout of a
booked item with the booking end date: with SpecifyDueDate disabled
the end-of-today due date computed for on-site checkouts was
clobbered, and with it enabled the same happened whenever staff left
the due date field empty. An on-site loan could therefore be due back
days after the on-site session.

- never stretch an on-site checkout to the booking end date; the due
  date behaves exactly as for a non-booked on-site checkout and
  AddIssue shrinks the booking end_date to the actual due date instead

Test plan:
1. Enable OnSiteCheckouts, disable SpecifyDueDate
2. Book an item for a patron starting today
3. Check the item out to them with 'On-site checkout' ticked
4. Without this patch the checkout is due at the booking end date;
   with it the checkout is due at the end of today and the booking
   end date follows it
5. Repeat with SpecifyDueDate enabled and the due date field left
   empty; the due date follows the circulation rules instead of the
   booking end date

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:53:59 +00:00
Paul Derscheid 90281d5a1f
Bug 42997: (QA follow-up) Don't invert the booking period when syncing end dates
Collecting a booking early (within the bookings lead period) with a
due date override before the booking start syncs end_date to a date
that precedes start_date. Koha::Booking->store has no interval
validation and the clash checks' date windows never see the inverted
period, so the broken row is stored silently - reproduced via AddIssue
on this patchset.

- pull start_date back to the checkout date whenever the synced due
  date precedes it, in both AddIssue and AddRenewal, so the booking
  window follows the loan instead of inverting
- cover both paths in Circulation.t; the AddRenewal case needs the
  Biblio::check_booking exemption from the previous patch, as the
  synced window no longer overlaps the stored one

Test plan:
1. prove t/db_dependent/Circulation.t
2. Set bookings_lead_period to e.g. 5 for an itemtype and enable
   SpecifyDueDate
3. Book an item of that itemtype for a patron starting in 3 days
4. Check it out to them today with an explicit due date of tomorrow
5. Without this patch the booking is stored with end_date before
   start_date; with it both dates follow the checkout period

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:53:58 +00:00
Paul Derscheid 8bd9790e47
Bug 42997: (QA follow-up) Don't let a booking's own checkout block it in Biblio::check_booking
Koha::Biblio->check_booking counts current checkouts on bookable items
against the pool of available items. Like Item::check_booking before
this patchset, it made no exception for the checkout that belongs to
the very booking being validated, so updating an issued booking's
period could clash against its own loan whenever the new period no
longer overlaps the stored one (the item_id exemption for booked items
only catches bookings that overlap the window under test).

This is masked in the end_date syncs earlier in this patchset because
the synced window always contains the stored start_date; it surfaces
as soon as a sync moves the whole period, and would equally bite the
extension pathway on bug 42790.

- skip checkouts whose booking_id matches the booking being validated
  when counting occupied bookable items, mirroring the
  Item::check_booking exemption
- cover the blocking and non-blocking checkout cases in Biblio.t

Test plan:
1. prove t/db_dependent/Koha/Biblio.t

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:53:57 +00:00
Martin Renvoize 11ab3a12f7
Bug 42997: Honour explicit due date overrides for booked items at checkout
When checking out a booked item, circulation.pl unconditionally
replaced the due date with the booking end date, silently discarding
any due date staff had explicitly specified - even after they had just
confirmed the BOOKED_DUE_DATE_CLASH warning saying they wanted it.

The booking end date remains the default when no due date is
specified; an explicit override (with SpecifyDueDate enabled) is now
passed through to AddIssue, which syncs the booking end_date with it
where possible and keeps the original end date when the override would
clash with a subsequent booking (as warned).

Test plan:
1) Create a booking and check the item out without specifying a due
   date; confirm the due date defaults to the booking end date
2) Return the item, book it again, and check out specifying an
   explicit due date; confirm the checkout uses that date and the
   booking end date follows it

Sponsored-by: OpenFifth <https://openfifth.co.uk/\>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/\>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:53:56 +00:00
Martin Renvoize 50115d8322
Bug 42997: Sync booking end_date with the due date at renewal
When a checkout satisfying a booking is renewed, the booking previously
kept its original end_date, blocking the booked period from other
patrons for the wrong window and leaving the two records out of sync.

This patch updates C4::Circulation::AddRenewal to pull the linked
booking's end_date along with the renewed due date, mirroring the
behaviour introduced for AddIssue earlier in this patchset. As at
issue time, if the new due date would clash with a subsequent booking
(possible when the renewal was explicitly allowed past a clash), the
booking keeps its original end_date.

Together with the previous patch this makes a renewal the natural way
to extend an issued booking: CanBookBeRenewed refuses renewals that
would run into the next booking, and AddRenewal keeps the booking in
step with the loan.

Test plan:
1) Run t/db_dependent/Circulation.t and confirm the new
   'AddRenewal | booking end_date sync' subtest passes
2) Check out a booked item, renew it from the checkouts screen and
   confirm the booking end date follows the new due date

Sponsored-by: OpenFifth <https://openfifth.co.uk/\>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/\>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:53:55 +00:00
Martin Renvoize 2da3248b41
Bug 42997: Prevent renewals running into the next booking
CanBookBeRenewed checked bookings by asking Koha::Item->find_booking
for the first booking that clashes with the renewal period. When the
renewing patron's own booking satisfies this checkout, that booking is
returned first and renewal is allowed - subsequent bookings for the
item were never considered. A patron could therefore renew a booked
item straight through the start of the next patron's booking.

Additionally, when staff renew with an explicit due date (the renewal
due date input on the checkouts screen, or the hard due date on
circ/renew.pl), CanBookBeRenewed validated a *calculated* due date
whilst AddRenewal then applied the *requested* one, so the requested
date was never checked against bookings at all.

This patch:

* repeats the booking lookup with the patron's own booking excluded,
  refusing the renewal ('booked') when it would run into the next
  booking for the item
* adds an optional $date_due parameter to CanBookBeRenewed, used in
  place of the calculated due date for the bookings check, and threads
  the requested date through from svc/renew and circ/renew.pl

Test plan:
1) Run t/db_dependent/Circulation.t - the extended
   'CanBookBeRenewed | bookings' subtest covers both the calculated
   and requested due date cases
2) Check out a booked item to its patron, add a subsequent booking for
   another patron, and attempt to renew past the second booking's
   start date from the checkouts screen; confirm the renewal is
   refused as 'booked'

Sponsored-by: OpenFifth <https://openfifth.co.uk/\>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/\>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:53:53 +00:00
Martin Renvoize 9a4a9415ab
Bug 42997: Warn staff when a due date override runs into the next booking
When checking out a booked item, staff may override the suggested due
date. If that override runs into the next booking for the item, the
previous patch would silently keep the booking's original end date -
staff had no way of knowing their override crossed another booking.

This patch adds an exclude_booking_id option to Koha::Item->find_booking
so a lookup can skip a specific booking (here: the patron's own booking
being collected), and uses it in CanBookBeIssued to detect when an
explicitly requested due date would run into the next booking for the
item. When it would, a new BOOKED_DUE_DATE_CLASH confirmation is raised
and displayed on the checkout screen, telling staff the linked booking
will keep its original end date if they continue.

Only explicitly requested due dates trigger the confirmation -
calculated due dates are clamped to the booking end date later in the
staff checkout flow, so checking them would produce false positives.
SIP is unaffected as it never passes a requested due date.

Test plan:
1) Run t/db_dependent/Circulation.t and t/db_dependent/Koha/Item.t
2) Create two consecutive bookings on the same item for two patrons
3) Check the item out to the first patron specifying a due date beyond
   the second booking's start date; confirm the new confirmation
   message appears and checkout only proceeds once confirmed

Sponsored-by: OpenFifth <https://openfifth.co.uk/\>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/\>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:53:52 +00:00
Martin Renvoize 41c453eff9
Bug 42997: Don't let a booking's own checkout block it in check_booking
Koha::Item->check_booking refuses any booking period that starts before
the item's current checkout is due back. That is correct when assessing
a new booking against someone else's loan, but it also fired when the
checkout in question is the booking's own linked loan - making it
impossible to ever move an issued booking's end_date, as any update
would 'clash' with its own checkout.

This patch skips the checkout guard when the checkout's booking_id
matches the booking_id passed for exclusion, i.e. when the checkout
belongs to the very booking being updated. This is a prerequisite for
keeping booking end dates in sync with checkout due dates in this
patchset, and for the booking extension pathway on bug 42790.

Test plan:
1) Run t/db_dependent/Koha/Item.t and confirm the check_booking
   subtest passes, including the two new checkout-related tests

Sponsored-by: OpenFifth <https://openfifth.co.uk/\>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/\>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:53:51 +00:00
Martin Renvoize 67e0051e1f
Bug 42997: Sync booking end_date with due date override at checkout
When a booked item is checked out, the due date defaults to the booking
end_date. Staff may override the due date at checkout time, which
previously left the booking end_date out of sync with the checkout,
blocking the booked period for longer or shorter than the actual loan.

This patch updates C4::Circulation::AddIssue to sync any due date
override back to booking.end_date the moment the booking transitions to
'issued', keeping the booking and the checkout aligned from the point
of issue. The existing Koha::Booking->store clash detection is used to
guard the update: if the overridden due date would clash with a
subsequent booking for the same item, the checkout proceeds with the
booking's original end_date instead.

Test plan:
1) Run t/db_dependent/Circulation.t and confirm the new
   'AddIssue | booking end_date sync with due date override' subtest
   passes
2) Create a booking for a patron on a specific item
3) Check the item out to the patron, overriding the suggested due date
4) Confirm the booking's end_date now matches the overridden due date

Sponsored-by: OpenFifth <https://openfifth.co.uk/\>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/\>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:53:50 +00:00
Marcel de Rooy 3ec3c465ba
Bug 36001: Show MARC21 362 on OPAC and staff
Test plan:
Find a serial.
Add a 362 with some content in $a
Check the normal view on OPAC and staff.

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: sabrina <sabrina.kiehl@gwdg.de>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 15:53:46 +00:00
Brendan Lawlor b7df6f071f
Bug 38196: (QA follow-up) Rename Include as Show
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:57:01 +00:00
Jan Kissig 54c5c2a9c4
Bug 38196: (follow-up) Fix issue with concern filters containing underscores
This fixes the issue that a filter for a ticket status is not applied when the underlying authorised value for that ticket status contains an underscore.
Also the fa icons are changed to be more intuitive.

Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:57:00 +00:00
Jan Kissig f14a046bb1
Bug 38196: (follow-up) Rename Filter to Hide in concerns.tt
This changes the filter names in concerns.tt from "Filter ..." to "Hide ..." to clarify that a filter here will remove items with that value from the table.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:56:59 +00:00
Jan Kissig 69217a2bc4
Bug 38196: Fix filter in catalog concerns to show tickets where status is NULL
This patch fixes issues on catalog concerns when filtering for self defined authorized values (AV).
When querying for status != AV, also rows with status = NULL are omitted as SQL here only checks for defined values in status.
The patch wraps the a filter query with an 'or status is NULL'.

Test plan:
apply patch dependencies (42283) if not already included and run restart_all
a) enable system preference CatalogConcerns
b) create some authorised values in TICKET_STATUS
c) create several concerns/tickets (biblio detail page -> new catalog concern)
d) go to /cgi-bin/koha/cataloguing/concerns.pl and change some ticket status to have some variation for filtering
e) play with the filters on the top (resolved and self created TICKET_STATUS)
- when clicking 'Filter resolved' tickets with status 'Resolved by ... date' get filtered
- when filtering self created TICKET_STATUS values tickets with that status AND tickets with status Open are disappearing
f) apply patch and run restart_all
g) retry steps in e) and check that only the filtered status are filtered from the results list.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:56:58 +00:00
Pedro Amorim f4bd0afa1b
Bug 39829: DBRev 26.06.00.019
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:56:57 +00:00
Pedro Amorim 1cf15f3c6e
Bug 39829: dbic
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:56:56 +00:00
Paul Derscheid a70df61623
Bug 39829: (QA follow-up) Fix test plan count
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:56:54 +00:00
Paul Derscheid 64aa56877a
Bug 39829: (QA follow-up) Pass server through for the title field
add_field/maybe_add need $server to apply the hide_fields and
allow_fields account options; without it the AJ field is always sent.

- Pass $server at the FID_TITLE_ID call sites in the checkout, checkin,
  item information, item status update and renew responses

Assisted-by: Fable 5 (Anthropic)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:56:53 +00:00
Nick Clemens f1ca516b60
Bug 39829: Allow custom title in SIP responses
This patch adds a custom aj_field_template to the sip accounts
This allows libraries to specify a template for the title (AJ) field in sip
messages

The template allows using item and biblio fields (item provides access to biblio. but we pass
both in order to makes the templates simpler

To test:
1 - Install patches
2 - updatedatabase, yarn build, restart_all
3 - Setup a sip account, KTD already has a sip patron, term1, you can use that
    don't add the aj_field_template yet
4 - Find or create a biblio with 245a and 245b like 245$aTitle : $bSubtitle and add item with TESTBARCODE
5 - From the command line:
    perl misc/sip_cli_emulator.pl -su term1 -sp term1 -l CPL -t CR -a localhost -p 6001 -m item_information --item TESTBARCODE
6 - Confirm AJ field shows "Title :" as expected
7 - Edit sip account, set aj_field_template to:
    [% biblio.title %] [% biblio.subtitle %] [% item.barcode %]
8 - Repeat the sip test
9 - Confirm you get the response like:
    Title : Subtitle TESTBARCODE
10 - Sign off!

Signed-off-by: Bob Bennhoff <bbennhoff@clicweb.org>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:56:52 +00:00
Jonathan Druart 6a040f8003
Bug 42975: (bug 35380 follow-up) Fix patron category filtering on Record overlay rules
Test plan:
Turn on MARCOverlayRules
Go to /cgi-bin/koha/admin/marc-overlay-rules.pl
Confirm the "Filter" column is correctly adjusted when the "source"
dropdown is modified

Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:56:50 +00:00
Tomás Cohen Arazi b31751a6cd
Bug 42565: Fix uninitialized value warning in GetIssuingCharges
GetIssuingCharges overwrites its initial $charge = 0 with the value of
itemtypes.rentalcharge, which is NULL when no rental charge is configured.
This causes 'uninitialized value' warnings in every caller that compares
$charge > 0 (automatic renewals, SIP checkout/renew, OPAC).

The fix normalizes NULL to 0 at the source. All callers already treat
the return value as numeric, so this is safe. Also removes a now-redundant
$charge && guard in AddIssue that was working around this.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/Circulation.t
   => SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Chris Mathevet <chris.mathevet@inLibro.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:56:49 +00:00
Tomás Cohen Arazi 1839189b1c
Bug 35142: (follow-up) Fix detail page crash with invalid metadata
The staff catalogue detail page called $biblio->normalized_upc,
normalized_ean, and normalized_oclc unconditionally. These methods
go through metadata_extractor which calls $biblio->metadata->record
internally, re-fetching the (corrupt) metadata from the DB and
ignoring the already-sanitized $marc_record.

When a record has invalid MARCXML, this causes an unhandled exception
instead of displaying the 'biberror' warning.

Fix: use the Extractor directly with the already-recovered
$marc_record (which was sanitized via StripNonXmlChars), and guard
the block with a check that $marc_record is defined.

Test plan:
1. Apply patch
2. Run:
   $ ktd --proxy --selenium up -d
   $ ktd --wait-ready 120
   $ ktd --shell
   k$ prove t/db_dependent/selenium/regressions.t
=> SUCCESS: Tests pass (including 'detail page must not explode')
3. Sign off :-D

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-03 10:56:48 +00:00
Pedro Amorim 481e170da1
Bug 42968: Revert "Bug 42968: Add fallback label for 505 contents notes"
This reverts commit 3bdecfb4e6.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:52:24 +00:00
Laura_Escamilla 3bdecfb4e6
Bug 42968: Add fallback label for 505 contents notes
To test:

1. Create or locate a bibliographic record with a MARC 505 field.

2. Leave the first indicator blank.

3. Add text to the 505 field.

4. View the record detail page.

5. Notice that the 505 contents display without a label.

6. Apply the patch

7. Refresh the record detail page. Notice that the 505 content displays a label now.

8. Sign off and have a lovely day! :D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:19 +00:00
Pedro Amorim 9d0b71511b
Bug 42653: Add tests
prove t/db_dependent/Koha/ILL/Request.t
prove t/db_dependent/Koha/Plugins/ILL/Backends.t

NOTE: These must be run without having any ILL backend plugin installed

Signed-off-by: Hannah Prince <helm.consortium.nhs.net>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:18 +00:00
Pedro Amorim 0e95900335
Bug 42653: get_backend_plugin should cache results per backend name
The plugin cache used a single slot, so the first backend looked up
poisoned it for all subsequent calls. This caused 500 errors when
viewing ILL requests that had been migrated between backends.

Test plan:
1) Download and install 2 additional plugin backends that implement
their own templates:
https://github.com/openfifth/koha-ill-libkey-lending-tool/releases/download/v2.6.1/koha-ill-libkey-lending-tool-v2.6.1.kpz
https://github.com/openfifth/koha-ill-reprintsdesk/releases/download/v4.2.0/koha-ill-reprintsdesk-v4.2.0.kpz

2) Enable ILLModule and run the following SQL:

INSERT INTO illrequests (borrowernumber, branchcode, status, backend, placed)
SELECT b.borrowernumber, br.branchcode, 'COMP', 'ReprintsDesk', NOW()
FROM borrowers b, branches br
LIMIT 1;

INSERT INTO action_logs (timestamp, user, module, action, object, info, interface)
VALUES
 (NOW() - INTERVAL 2 HOUR, 1, 'ILL', 'INCDOCS_REQUEST_PLACED', 1,
  '{"log_origin":"IncDocs","response":1}', 'intranet'),
 (NOW() - INTERVAL 1 HOUR, 1, 'ILL', 'REPRINTS_DESK_REQUEST_ORDER_UPDATED', 1,
  '{"log_origin":"ReprintsDesk","response":null}', 'commandline');

3) Attempt to open the 'Manage request' page for this newly created
request:
http://localhost:8081/cgi-bin/koha/ill/ill-requests.pl?op=illview&illrequest_id=1

4) Notice you get a 500 error:
Template process failed: file error - : not found

5) Apply all patches, restart plack, refresh. Notice the 'Manage
request' page now renders correctly.

6) Click the 'ILL request log' toolbar button. Confirm that both the
IncDocs and ReprintsDesk entries are shown.

Signed-off-by: Hannah Prince <helm.consortium.nhs.net>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:17 +00:00
Martin Renvoize 6d8ea60630
Bug 42993: EDI: log transport failures via Koha::Logger instead of carp
Koha::Edifact::Transport reported every transport failure (no transport
configured, connect failure, directory-change failure, file-listing
failure and per-file download/upload failure) with carp. Under
edi_cron.pl these go to STDERR, so on a normal cron run the messages are
lost and EDI simply goes quiet with nothing in editrace.log to explain
why.

Route these failures through Koha::Logger on the 'edi' interface (the
same interface edi_cron.pl already uses) so they are recorded in the EDI
log. The two ingest()-level data warnings (duplicate filename, unreadable
file) are intentionally left as carp: one is asserted by the existing
unit test and neither is a transport failure.

No change to the transfer logic.

Test plan:
1) prove t/db_dependent/Koha/Edifact/Transport.t
2) Point an EDI account at an unreachable transport and run edi_cron.pl;
   confirm the failure is now written to the edi log rather than only to
   STDERR.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:15 +00:00
Jonathan Druart 02491b08d3
Bug 43394: DT - Cast pageLength to Integer
In "Edit patron card batch" the pageLength is coming from the syspref,
the value is a string.
But DataTable's needs an integer.
Let's fix this in datatables.js, in the kohaTable's constructor

Test plan:
Got to cgi-bin/koha/patroncards/edit-batch.pl
Enter more than 20 borrowernumbers and "Add patron(s)"
Click "next" and notice that with this patch the pagination works.
Without this patch the JS error is
"Uncaught TypeError: can't access property "nTr", p is undefined"

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:14 +00:00
Pedro Amorim eb81e1cc5e
Bug 43072: DBRev 26.06.00.018
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:13 +00:00
Emily Lamancusa dcba10c45d
Bug 43072: (QA follow-up) Use say_info in dbrev
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:12 +00:00
Andrew Fuerste Henry 17c1c87583
Bug 43072: standardize atomicupdate formatting
Signed-off-by: Lawrence O'Regan-Lloyd <loreganlloyd@clamsnet.org>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:11 +00:00
Nick Clemens 241abf5545
Bug 43072: (Bug 41267 follow-up) Add default rules when not set
Bug 41267 defined the new rule, but didn't set the default.

For consistencies sake we should create these in the DB

To test:
1 - Create 'Default holds and bookings policies by item type' for two itemtypes
    Set 'Fill other record level holds on record at checkout' to yes for one and no for the other
2 - Apply this patch
3 - updatedatabase
4 - Verify the rules are not altered
5 - Chekc the DB:
    SELECT * FROM circulation_rules WHERE branchcode IS NULL and categorycode IS NULL;
6 - Note the number of rules/ids
7 - Run the DB update and confirm nothing changes
8 - DELETE from circulation_rules WHERE rule_name='fill_other_biblio_holds_policy';
9 - Run the DB update
10 - Confirm rules are replaced, both as yes
11 - Delete a single one of the rules now:
    DELETE from circulation_rules WHERE rule_name='fill_other_biblio_holds_policy' AND id=#;
12 - Run the DB update
13 - Confirm only that rule is replaced
14 - DELETE from circulation_rules WHERE branchcode IS NULL and categorycode IS NULL;
15 - Run the DB update
16 - No new rules are created

 follo#	Koha/.Patrons.pm.swp

Signed-off-by: Lawrence O'Regan-Lloyd <loreganlloyd@clamsnet.org>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:10 +00:00
Andrew Fuerste Henry 0c86df4755
Bug 43072: Show Yes when no rule exists for fill_other_biblio_holds_policy
To test
1 - have three test biblios. Bib A has just one item, a Book. Bib B has two items, a Book and a Computer File. Bib C has one item, a Computer File
2 - have KTD default circ rules (holds allowed for all itemtypes in the circ rules table, no values defined in "Default checkout, hold and return policy" or "Default holds and bookings policies by item type
")
3 - set "Default holds and bookings policies by item type" for Computer Files to Hold Policy = "No holds allowed."
4 - observe that "Fill other record level holds on record at checkout" defaults to "Yes" when a value is set to Hold Policy
5 - confirm you can place a holds on Bib A and Bib B but not Bib C
6 - confirm your hold on Bib B is filled when checking out the Computer File item from Bib B
7 - update "Default holds and bookings policies by item type" so Computer Files has Hold Policy = "No holds allowed" and "Fill other record level holds on record at checkout" = No
8 - confirm you're still able to place a hold on Bib B
9 - confirm your hold on Bib B is now *not* filled by checking out the Computer Files item
10 - open the commandline SQL interface (sudo koha-mysql kohadev on KTD)
11 - run query "select * from circulation_rules where rule_name = 'fill_other_biblio_holds_policy'"; confirm an entry for Computer Files with rule_value=0
12 - run command "delete from circulation_rules where rule_name = 'fill_other_biblio_holds_policy'"
13 - reload your circ rules page, confirm the interface still shows "Fill other record level holds on record at checkout" = No for comoputer files
14 - confirm your hold on Bib B can again be filled by checking out the Computer File item

 APPLY PATCH, restart_all

15 - reload circ rule page, confirm it now shows "Fill other record level holds on record at checkout" = Yes for Computer Files
16 - run query "select * from circulation_rules where rule_name = 'fill_other_biblio_holds_policy'"; confirm there is still no entry for Computer Files
17 - place a new hold on Bib B, confirm it fills by checking out the Computer File (not having any rule defined is enforced the same as a "Yes" and displays a "Yes")
18 - in circ rules, set "Fill other record level holds on record at checkout" = Yes for computer files
19 - run query "select * from circulation_rules where rule_name = 'fill_other_biblio_holds_policy'"; confirm there is an entry for Computer Files with rule_value=1
20 - place a new hold on Bib B, confirm it fills by checking out the Computer File (having an explicit Yes rule defined is enforced correctly and displays correctly)
21 - in circ rules, set "Fill other record level holds on record at checkout" = No for computer files
22 - run query "select * from circulation_rules where rule_name = 'fill_other_biblio_holds_policy'"; confirm there is an entry for Computer Files with rule_value=0
23 - place a new hold on Bib B, confirm it does not fill by checking out the Computer File (having an explicit No rule defined is enforced correctly and displays correctly)

Signed-off-by: Lawrence O'Regan-Lloyd <loreganlloyd@clamsnet.org>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:09 +00:00
David Cook ddd790e4c5
Bug 42924: Restore default MaxAge for CSRF token
This patch changes the default MaxAge from 8 hours to
168 hours or 7 days due to usability issues encountered from
using a short-lived token.

See OWASP for more information on why timestamps with expiry
are not necessary for CSRF tokens. The MaxAge is a requirement
of the WWW::CSRF token used by Koha, so let's just use a longer
expiry in lieu of no expiry.

Test plan:
0. apply patch
1. prove t/Token.t

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:08 +00:00
Martin Renvoize bfc6866200
Bug 39589: Auto-skip to the next page and note hidden results when a page is fully filtered
When every result on a page is hidden (OpacHiddenItemsHidesRecord), rather
than leaving the patron on a dead-end "not available" page, look ahead a
bounded number of pages (3) for one with visible results and redirect
there directly. The lookahead is capped so a library with many
consecutive hidden pages doesn't turn a single search into a long chain
of extra search-engine queries; RSS/atom/opensearchdescription requests
are left alone since redirecting a feed reader makes no sense.

Whenever a hidden page was skipped over (whether we found a visible page
to land on, or exhausted the lookahead and fell back to the "not
available" message), show a caveat next to the results count so the
"Your search returned N results" figure doesn't read as contradicting
what the patron can actually see - this was raised as a sign-off concern
on the original patch (comment 4).

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:07 +00:00
Martin Renvoize a8523740b0
Bug 39589: (QA follow-up) Revert unrelated star rating markup change
The read-only star rating widget in search results was changed from
<span> to <a href="#"> elements, unrelated to this bug's fix. That
activated a pre-existing but previously dead click handler
($(".br-readonly a") in this same template) and turned every star icon
into a focusable, non-functional link, adding up to 5 dead tab stops
per search result. Restore the <span> markup used everywhere else this
read-only widget appears.

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:06 +00:00
Martin Renvoize 84831855d2
Bug 39589: (QA follow-up) Don't zero hits when a page has no visible results
Zeroing $hits alongside the no_page_results flag also zeroed $total
(since $total accumulates $hits, and the final template 'total' param
is set from $hits/$total). Because the top "No results found!" banner
and the entire search-results section are both gated on 'total' being
true, this meant the new no_page_results branch could never actually
render: whenever it was true, 'total' was false, so the classic
"No results found!" page won instead - recreating the exact bug this
patch is meant to fix.

Verified against the bug's own test plan (OPACnumSearchResults=5,
OpacHiddenItemsHidesRecord, OpacHiddenItems: withdrawn:[1], 6 bibs with
the first 5 withdrawn/hidden): before this change, page 1 still showed
"No results found!" with no pagination; after this change, it shows
"Your search returned 6 results" / "Search results on this page are
not available", with a working pagination link to the page containing
the visible record.

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:05 +00:00
Chris Mathevet ff26e80dc8
Bug 39589: (follow-up) Applied suggested changes
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:04 +00:00
Hammat Wele 38e0b734c9
Bug 39589: OPAC search returns 'No results found' despite existing matching records
This issue occurs when the records on the first page of results are not displayed. The message "No results found" is shown, and it is not possible to access records on other pages.

To test

    1. Set the following system preferences:
        OPACnumSearchResults to 5
        OpacHiddenItemsHidesRecord to Hide
        OpacHiddenItems to withdrawn: [1]
    2. Create 6 bibliographic records with the following titles:
        "notice test 1"
        "notice test 2"
        "notice test 3"
        "notice test 4"
        "notice test 5"
        "notice test 6"
    3. For the first 5 records, add an item with Withdrawn status.
    4. For "notice test 6", add an item with Available status.
    5. In the OPAC interface, search for "notice test".
    6. Apply the patch
    7. Repeat step 4
       ---> The results table is now shown with the message: "Search results on this page are not available."
       ---> You can now access the second page and see "notice test 6"

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:03 +00:00
Jonathan Druart 3934a2bcc0
Bug 43117: Restore use Test::NoWarnings
From
  commit 6cc683b518
  Bug 34776: Unit tests

-use Test::NoWarnings;
-use Test::More tests => 2;
+use Test::More tests => 1;
+use Test::MockModule;
+#use Test::NoWarnings;

Test plan:
Apply the first patch
 prove xt/use-test-no-warnings.t
=> FAIL
Apply this patch
 prove xt/use-test-no-warnings.t
=> PASS
 prove t/Form_MessagingPreferences.t
=> PASS

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:02 +00:00
Jonathan Druart e2c90b5750
Bug 43117: Adjust xt test to catch commented use Test::NoWarnings
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:01 +00:00
Andrii Nugged fd6a616428
Bug 43354: Fix ILL request table search
Bug 33544 combined the ILL request detail fields into one column. The
combined column kept an empty data property without the related-object
search metadata. A global table search therefore generated a query against
"me.", which the REST endpoint rejected with a 500 error.

Restore the extended attribute search mapping for the fields displayed in
the combined Request details column. Add Cypress coverage for both the REST
query and the matching result.

Test plan:
Before applying the patch:
1. Make sure the ILLModule system preference is enabled.
2. Open a patron record.
3. Click "ILL requests history" in the menu on the left.
4. Type any text in the Search box above the table.
5. Confirm that Koha shows a 500 Internal Server Error message.

Apply the patch.

After applying the patch:
1. Repeat steps 2 to 4 above.
2. Confirm that the table refreshes without an error. It may show no
   matching requests.
3. If the patron has an ILL request, search for part of a title or author
   shown under Request details.
4. Confirm that the matching request remains in the table.

Co-Authored-By: Loontik <loontik@nugged.team>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:42:00 +00:00
Jonathan Druart 274432cd68
Bug 43259: Fix add/remove basket to/from group
Caused by
  commit e9b8004d19
  Bug 38255: Replace dataTable constructor with kohaTable - Staff

grouped and ungrouped should be DataTable objects.

Test plan:
Create a basket, close it
Create a basket group and test the "Add to group" and "Remove" button on
the basket group edit view

Without this patch there was a JS error:
"Uncaught TypeError: grouped.row is not a function"

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-09-01 15:41:56 +00:00
Pedro Amorim 8870a456dc
Bug 30144: dbic
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:46 +00:00
Jonathan Druart e42b4df37b
Bug 42661: Remove redundant library's name if HidePatronName
If HidePatronName is turned on and a hold is being placed on a hold for
a patron with a different library, there is an alert the duplicate the
library's name:
Pickup library is different. Patron: A patron from library Springfield Patron's home library: (Springfield / SPL)

With this patch:
Pickup library is different. Patron's home library: Springfield / SPL
Pickup library is different. Patron: Keith Hendrix Patron's home library: Springfield / SPL

Test plan:
Go to /cgi-bin/koha/reserve/request.pl?biblionumber=117&borrowernumber=26
And confirm that the changes make sense

Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:45 +00:00
Jonathan Druart 6e162e4c10
Bug 42727: Sort using the order from the staff syspref
The main goal of the previous patch was to prevent the randomness,
the languages were displayed by code.

The languages in the footer are the languages enabled for the staff interface.

This patch suggests to use the order of the syspref, and append the
languages not enabled at the end of the list.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:44 +00:00
Jonathan Druart a098d97b5a
Bug 42727: List languages in a determinist order
The list of languages are displayed in a random order in:
* language picker at the bottom of the page
* language tab when editing a notice template or an additional content

This patch fixes it to display them in a determinist order

Test plan:
Have several languages installed, and some of them enabled, change the
order of the languages in the syspref
Reload the mainpage several times and notice that the languages are
always displayed in the same order.
Same when editing a notice template or an additional content.

Note that I have not investigated yet why on the notice template and
additinal content edit views they are not listed in the same order.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:43 +00:00
Jonathan Druart 0f7145e4af
Bug 42727: Add tests
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:42 +00:00
Owen Leonard 0a5919f108
Bug 43257: Remove event attributes from circulation.tt
This patch updates circulation.tt in order to remove event attributes in
favor of defining event handlers in the relevant JS file.

To test, apply the patch and clear your cache if necessary.

- If necessary, enable the SpecifyDueDate system preference.
- Check out to a patron.
- Click the checkout settings icon inside the barcode field.
- Click the "Specify due date" field's "Remember for session" checkbox
  - The focus should move to the barcode field.

- Enable recalls if necessary
- Locate a checked out item which can be recalled.
- Log in to the OPAC and recall the item.
- In the staff interface, try to check that item out to another patron.
  - You should get a "Please confirm checkout" message.
  - Click the "Don't check out and print slip" button.
  - A popup should open for printing a recall slip.
  - The checkout page should reload  without checking out the item.

- If necessary, place a hold on an item for delivery to your logged-in
  library, check in the item, and confirm the hold.
- Try to check out the item to another patron.
  - You should get a "Please confirm checkout" message.
  - Click "Don't check out, confirm hold, and print slip"
  - A popup should open for printing a hold slip.
  - The checkout page should reload  without checking out the item.

- If necessary, place a hold on an item but do not check it in.
- Try to check out the item to a patron.
  - You should get a "Please confirm checkout" message.
  - Click "Cancel checkout and place hold for..."
  - You should be redirected to the holds page, placing a hold for that
    patron and that item.
  - The item should not have gotten checked out in the process.

Sponsored-by: Athens County Public Libraries
Signed-off-by: Olivia Reynolds <olivia.reynolds@openfifth.co.uk>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:41 +00:00
Jonathan Druart b1905338eb
Bug 43373: Holdings table - hide the shelving location column if empty
The shelving location cell is always built with data: an empty HTML tag
is added even if there is no location.
So the "Shelving location" column is always displayed, it should only be
displayed if there are items with data.

Test plan:
Have a biblio with all items without shelving location (or without
several pages and at least one page without data).

If all rows have empty shelving loc, the column should be hidden

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:40 +00:00
Jonathan Druart d80ca5f4e8
Bug 42875: Correct failure message when adding an item to a bundle
If bc_1 is checked out and I try to add bc_2 I get:
"Failure: Item 'bc_2' belongs to another bundle"

It should be:
"Failure: Bundle is currently checked out"

Test plan (using KTD):
1. Pick a record with items, for example: The definitive guide to Catalyst (146)
2. Edit the record and change 000 (the leader):
   - Click the tag editor
   - Change "7- Bibliographic level" to "C- Collection"
3. In the holdings table there is now a "Manage bundle (0|0)" option
   in the far right column for each item.
4. Check an item out for the record out, for example
   I checked out 39999000006117 to Mary Burton.
5. In the holdings table for the record, click on "Manage bundle" for the item
   you checked out (39999000006117).
6. In the expanded text area below the item row, click "+ Add to bundle".
7. In pop-up window, enter 39999000006131 "Item barcode" field (this is one
   of the other items for the record that is not checked out).
8. Click Submit.
9. You get an error:
     Failure: Item '39999000006131' belongs to another bundle
10. Apply the patch.
11. Restart everything: restart all
12. Hard refresh the page for the record.
13. Repeat steps 5 to 8.
14. You now get the correct error:
      "Failure: Bundle is currently checked out"

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:39 +00:00
Jonathan Druart 0c952e9e09
Bug 43241: Add tests for timezone support in RFC3339 q= queries
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:38 +00:00
Tomás Cohen Arazi fd87c5f195
Bug 43241: (follow-up) Use form => for q= params in tests
The + character in RFC3339 timezone offsets (e.g. +00:00) is decoded
as a space when interpolated directly into a URL query string. Use
Test::Mojo form => { q => ... } which properly URL-encodes the
parameter value.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:37 +00:00
Tomás Cohen Arazi 450e6bf178
Bug 43241: Fix datetime/boolean conversion in q= queries with DBIC operators
_parse_dbic_query maps field names but did not convert values. When
datetime or boolean filters are nested inside DBIC operators (-and,
-or) in the q= parameter, attributes_from_api (which normally handles
the conversion via _recursive_fixup) cannot reach them because it only
iterates top-level keys.

This patch fixes the issue in two places:

1. _parse_dbic_query (Query.pm): When a key maps to a known column
   (on the primary table or a related object), call _recursive_fixup
   to convert RFC3339 timestamps to MySQL format and booleans to 0/1.
   Related object instances are cached on the result_set to avoid
   repeated prefetch_whitelist lookups during recursive parsing.

2. Objects.pm: Move attributes_from_api to process named query params
   before merging with q= params. This avoids double-processing since
   q= values are already fixed up by _parse_dbic_query.

Test plan:
1. Apply the regression tests from the previous commit
2. Run:
   $ prove t/Koha/REST/Plugin/Query.t
   $ prove t/db_dependent/Koha/REST/Plugin/Objects.t
=> FAIL: Tests fail!
3. Apply this patch
4. Repeat step 2
=> SUCCESS: Tests pass!
5. Sign off :-D

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:36 +00:00
Tomás Cohen Arazi 811e57c441
Bug 43241: Add tests for datetime/boolean fixup inside DBIC operators
This patch adds regression tests proving that RFC3339 datetime values
and boolean values nested inside DBIC operators (-and, -or, -between)
in q= queries are not converted to their DB-native format.

Unit tests (Query.t):
- Datetime: >=, <=, range, -or, -between, nested -and/-or, me. prefix
- Boolean: true/false inside -and, -or, nested -and within -or
- Non-datetime values pass through unchanged

Integration tests (Objects.t):
- Full API stack tests using action_logs with controlled data
- Same operator/nesting coverage as unit tests

Test plan:
1. Apply patch
2. Run:
   $ prove t/Koha/REST/Plugin/Query.t
   $ prove t/db_dependent/Koha/REST/Plugin/Objects.t
=> FAIL: Tests fail! Values inside -and are not converted
3. Sign off :-D

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:35 +00:00
Martin Renvoize 537a64a01b
Bug 42634: Force JWT cookie deletion on self-checkout logout
The web-based self-checkout (opac/sco/sco-main.pl) builds the
response JWT cookie with -expires => $jwt ? '+1d' : ''. On logout
$jwt is undef, so the cookie is sent with an empty value and no
Expires attribute - i.e. a session cookie with an empty value
rather than a deletion. Some browsers/proxies retain the previous
valid JWT in this scenario, which presents as the Finish/close
button "doing nothing" or working only intermittently: the patron
appears to remain logged in despite clicking Finish.

Use a past Expires ('-1d') when no JWT is present so the browser
deletes the cookie entirely. The subsequent ?op=logout render
cannot then rediscover the patron from a stale cookie.

Test plan:
1. Enable web-based self-checkout (SelfCheckoutByLogin or
   SelfCheckAllowByIPRanges).
2. Log in as a patron at /cgi-bin/koha/sco/sco-main.pl.
3. Open DevTools -> Application -> Cookies and note the JWT cookie.
4. Click the green "Finish" button.
5. Without the patch: Set-Cookie on the ?op=logout response shows
   JWT= with no Expires; the cookie persists as an empty session
   cookie.
6. With the patch: Set-Cookie shows JWT=; expires=<past date>; the
   browser removes the cookie entirely.
7. Confirm the kiosk reliably returns to the login screen and
   subsequent page loads do not re-authenticate from a stale JWT.

Sponsored-by: OpenFifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Emmi Takkinen <emmi.takkinen@koha-suomi.fi>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:34 +00:00
Andrii Nugged b719532f24
Bug 42605: Add automated tests
Store scalar preference values when saving the acquisition preferences
and clean up the invoice explicitly.

Exercise the rendered receiving behavior for a basket that inherits
AcqCreateItem, one that explicitly overrides it with "ordering", and a
basket that creates items on receiving. Keep the explicit "ordering"
case as a control for behavior that already works without the production
fix.

This changes tests only.

Automated tests:

yarn cypress run --spec \
  t/cypress/integration/Acquisitions/OrderReceivePreferences_spec.ts

Co-Authored-By: Loontik <loontik@nugged.team>
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:33 +00:00
Andrii Nugged 757b347bed
Bug 42605: Preserve acquisition preference string values
Bug 41563 converted AcqCreateItem and UniqueItemFields to booleans when
adding them to Koha.prefs. Both preferences are strings: AcqCreateItem
controls where items are created, while UniqueItemFields contains a
pipe-separated list.

When a basket inherits AcqCreateItem=ordering, the boolean value matches
none of the receiving page branches and existing items are hidden. When
items are created on receiving, the numeric UniqueItemFields value also
fails when the page calls split().

Preserve both string values in Koha.prefs. Add a Cypress regression test
which renders orderreceive.pl and verifies the values exposed to
JavaScript.

Test plan:

Before applying the patch:

1. Set AcqCreateItem to "placing an order" and UniqueItemFields to
   "barcode".
2. Create a basket that inherits the system preference. Add an order with one
   item, close the basket, and start receiving it.
3. Confirm that the Items table has no existing item row and no Receive
   checkbox.
4. Set AcqCreateItem to "receiving an order" and open the same order for
   receiving.
5. Confirm that the Items section is empty: no item-entry fields and no Add
   item button are shown.

Apply the patch.

After applying the patch:

1. Set AcqCreateItem back to "placing an order" and open the order for
   receiving again.
2. Confirm that its existing item is listed and has a Receive checkbox.
3. Set AcqCreateItem to "receiving an order" and open the same order again.
4. Confirm that the Items section contains item-entry fields and an Add item
   button.

Co-Authored-By: Loontik <loontik@nugged.team>
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:32 +00:00
Tomás Cohen Arazi 3dc90004ca
Bug 39769: Flush L1 cache in es_indexer_daemon.pl before each batch
The daemon runs as a long-lived process and never clears its L1 cache.
System preference changes (e.g. IncludeSeeFromInSearches) are not
picked up until the daemon is manually restarted.

Flush L1 caches at the start of each commit() call so that preference
changes take effect on the next indexing batch without requiring a
daemon restart.

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:31 +00:00
Pedro Amorim 279448539c
Bug 30144: DBRev 26.06.00.017
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:29 +00:00
Martin Renvoize 08a2e0475b
Bug 30144: (QA follow-up) Exclude servicing_instruction from order API representation
The servicing_instruction column was not mapped in
Koha::Acquisition::Order->to_api_mapping, so it was exposed verbatim in
the REST API order representation. Since api/v1/swagger/definitions/order.yaml
does not declare this property and sets additionalProperties: false, any
request touching the acquisitions/orders endpoint (e.g. the "Receive
shipments" page) failed with a 500 error: "Properties not allowed:
servicing_instruction".

Exclude it from the API representation, consistent with the other
EDIFACT-only columns (line_item_id, suppliers_reference_number, etc.)
that are already excluded the same way.

Test plan:
1) Go to a vendor page, create a basket, add an order line, and close
   the basket.
2) From the vendor page, click "Receive shipments".
3) Without this patch, the orders table fails to load with a 500 error
   mentioning "Properties not allowed: servicing_instruction".
4) With this patch applied, the orders table loads correctly.
5) prove t/db_dependent/api/v1/acquisitions_orders.t

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:28 +00:00
Martin Renvoize 2e1f8b8660
Bug 30144: (QA follow-up) Make servicing instruction UI strings translatable
The servicing instructions widget added new user-facing strings (alerts,
button labels, hints) built entirely in JS via string concatenation, none
of which were wrapped for translation - unlike the pre-existing JS in the
same file, which already uses _() throughout. Wrap them the same way,
using _("...").format(...) for the strings that interpolate a value,
matching the existing pattern used elsewhere in this file.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:27 +00:00
Martin Renvoize bbb7f406d2
Bug 30144: (QA follow-up) Fix stored XSS in servicing instructions
The RM's review found two stored XSS vectors in the servicing
instruction free-text (LVT) handling:

1. neworderempty.tt embedded the servicing instruction JSON (and the
   EDIFACT_SI authorised values list) directly into an inline <script>
   block via JSON.parse('[% ... | $raw %]'). TT's html filter does not
   escape single quotes, so a free-text value like
   test' + alert(1) + ' broke out of the JS string literal and
   executed on the next page load of the edit form.

2. basket.pl built the "Servicing instruction" column display by
   concatenating raw LVC/LVT values with '<br>' and rendering the
   result with | $raw in basket.tt, so HTML/script content in a
   free-text value executed directly on the basket page.

Fix the first by moving the JSON into existing/new hidden input
value="" attributes (escaped with the html filter, which is safe for
double-quoted HTML attributes) and reading them back via jQuery's
.val() before JSON.parse(), instead of interpolating untrusted data
into a JS string literal.

Fix the second by passing the display parts to the template as a list
and escaping each with the html filter at render time, instead of
building a pre-escaped raw HTML string in Perl.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:26 +00:00
Martin Renvoize 42ae1e0bf0
Bug 30144: (QA follow-up) Add EDIFACT_SI authorised values to installer data
Tests were failing on upgrade route because they tried to create
EDIFACT_SI authorised values that already existed from the atomicupdate
script, causing unique constraint violations.

This patch:
- Removes test code that created EDIFACT_SI values (tests should assume
  database defaults exist)
- Adds EDIFACT_SI category to auth_val_cat.sql for fresh installs
- Adds all 32 EDItEUR List 3B servicing instruction codes to
  auth_values.yml for fresh installs

Now the authorised values exist in both installation paths:
- Upgrade route: Created by atomicupdate script
- Fresh install: Created by installer SQL/YAML files

Test plan:
1. prove t/db_dependent/Koha/EDI.t
2. prove t/db_dependent/Koha/Edifact/Order.t
3. Verify both tests pass without warnings

Sponsored-by: Martin Renvoize <martin.renvoize@gmail.com>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:25 +00:00
Martin Renvoize 711bb2dd76
Bug 30144: Add tests for EDIFACT servicing instruction handling
This commit adds comprehensive tests for servicing instruction
processing in both incoming quotes and outgoing orders:

1. Integration tests for incoming quote processing (Koha/EDI.pm)
   - Tests LVC (coded) and LVT (freetext) extraction
   - Tests multiple GIR occurrences with different servicing instructions
   - Tests all combinations: LVC only, LVT only, and LVC+LVT together

2. Test EDIFACT quote file (QUOTES_SERVICING.CEQ)
   - Contains 3 line items with 4 total orders
   - Demonstrates different servicing instruction patterns

3. Fix for multi-occurrence servicing instruction extraction
   - Modified quote_item() to extract servicing instructions
     per GIR occurrence instead of across all occurrences
   - Ensures each order gets only its own servicing instructions
   - Aligns with existing multi-item processing for funds and stock rotation

Test coverage:
- t/db_dependent/Koha/EDI.t: 29 tests for incoming quote processing
- t/db_dependent/Koha/Edifact/Order.t: 18 tests for outgoing order generation
  (previously added)

All tests pass successfully.

Sponsored-by: Martin Renvoize <martin.renvoize@gmail.com>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:24 +00:00
Martin Renvoize b02cd5e1fe
Bug 30144: Add unit tests for servicing instructions
This patch adds comprehensive unit tests for the servicing instruction
functionality in t/db_dependent/Koha/Edifact/Order.t.

Changes:
- Updated test count from 6 to 7
- Added servicing_instruction => undef to test orders to prevent
  TestBuilder from generating random invalid JSON
- Added servicing instruction subtest with 18 tests covering:
  * Single LVC instruction
  * Single LVT instruction
  * Multiple LVC in one group (tests GIR segment overflow)
  * Mixed LVC/LVT in one group
  * Multiple groups
  * Empty servicing instruction
  * Invalid JSON handling (logged to EDI log)
- Tests correctly handle GIR segment 5-element limit and overflow
  to additional segments

Test plan:
1. Run: prove t/db_dependent/Koha/Edifact/Order.t
2. Verify all tests pass

Sponsored-by: Martin Renvoize <martin.renvoize@gmail.com>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:23 +00:00
Martin Renvoize c215f2dd43
Bug 30144: Add UI for servicing instructions
This patch adds the user interface for managing and displaying
EDIFACT servicing instructions in the acquisitions module.

Changes to acqui/neworderempty.pl:
- Parses servicing_instruction JSON for editing existing orders
- Fetches EDIFACT_SI authorized values for dropdown
- Passes data to template in both array and JSON formats

Changes to acqui/addorder.pl:
- Accepts servicing_instruction_groups_json from form submission
- Validates and stores JSON in database

Changes to acqui/basket.pl:
- Fetches EDIFACT_SI authorized values
- Formats servicing instructions for display in get_order_infos()
- Creates human-readable display strings with:
  * LVC codes replaced with descriptions
  * Groups separated by <br> tags
  * Format: "Description" or "Description: free text"

Changes to neworderempty.tt:
- Dynamic UI for adding/removing servicing instruction groups
- Each group can contain:
  * One LVC (coded) instruction only, OR
  * One LVT (free-text) instruction only, OR
  * One LVC + one LVT combination
- Validation prevents:
  * More than 2 instructions per group
  * Duplicate LVC or LVT in same group
- LVC instructions use dropdown populated from EDIFACT_SI authorized values
- LVT instructions use free-text input
- Groups serialized to JSON hidden field for form submission
- Smart button labels show what can be added to each group

Changes to basket.tt:
- Displays servicing instructions in basket table
- Pre-formatted in Perl (no JavaScript parsing needed)
- Shows LVC descriptions instead of codes
- Multiple groups displayed on separate lines

Changes to columns_settings.yml:
- Adds servicing_instruction column to basket table configuration

Display format examples:
- LVC only: "Barcode labelling"
- LVT only: "Handle with care"
- Combined: "Binding: Apply to spine"
- Multiple groups (on separate lines):
  Barcode labelling
  Binding: Special instructions

Test plan:
1. Create/edit order
2. Add servicing instruction groups
3. Try to add more than 2 instructions to a group (should prevent)
4. Try to add duplicate LVC or LVT to group (should prevent)
5. Save order
6. View basket and verify instructions display correctly
7. Edit order and verify instructions load into form

Sponsored-by: Martin Renvoize <martin.renvoize@gmail.com>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:22 +00:00
Martin Renvoize 4f2829c639
Bug 30144: Process and send servicing instructions in EDI messages
This patch updates the EDIFACT processing to handle multiple servicing
instruction groups in both incoming quotes and outgoing orders.

Changes to Koha/EDI.pm (quote_item):
- Reads both LVC (coded) and LVT (freetext) instructions from incoming quotes
- Validates LVC codes against EDIFACT_SI authorized values
- Stores instructions as JSON array structure
- Groups instructions that work together

Changes to Koha/Edifact/Order.pm (gir_segments):
- Parses JSON servicing instruction data from order
- Generates multiple GIR segments for outgoing orders
- Each instruction becomes a separate GIR element
- Logs JSON parse errors to EDI log using Koha::Logger
- Fixed element counter reset bug when segments exceed 5 elements

Structure allows vendors to specify:
- Single coded instruction: [[{type:"LVC",value:"BB"}]]
- Single text instruction: [[{type:"LVT",value:"Handle with care"}]]
- Combined instructions: [[{type:"LVC",value:"BB"},{type:"LVT",value:"Apply on spine"}]]
- Multiple services: [[{type:"LVC",value:"BB"}],[{type:"LVC",value:"BI"}]]

Test plan:
1. Receive EDIFACT quote with servicing instructions
2. Verify instructions are stored correctly in aqorders
3. Create EDI order from basket
4. Verify GIR segments are generated correctly in outgoing message
5. Check EDI log for any JSON parse errors

Sponsored-by: Martin Renvoize <martin.renvoize@gmail.com>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:21 +00:00
Martin Renvoize 8d1e208a7b
Bug 30144: Update DBIC schema
Sponsored-by: Martin Renvoize <martin.renvoize@gmail.com>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:20 +00:00
Martin Renvoize b78a308e60
Bug 30144: Add servicing_instruction field to aqorders table
This patch adds a new TEXT field 'servicing_instruction' to the aqorders
table to store EDIFACT servicing instructions (GIR LVT/LVC segments) as JSON.

The field stores an array of instruction groups, where each group is an
array of instruction objects with 'type' (LVC or LVT) and 'value' fields.

Structure: [[{type:"LVC",value:"BB"},{type:"LVT",value:"text"}],[...]]

This allows for:
- Multiple servicing instruction groups per order line
- Combinations of coded (LVC) and free-text (LVT) instructions
- Instructions that work together in groups

Also adds EDIFACT_SI authorized value category with EDItEUR List 3B
servicing instruction codes for coded (LVC) instructions.

Test plan:
1. Run database update
2. Verify aqorders.servicing_instruction field exists
3. Verify EDIFACT_SI authorized value category is created
4. Verify authorized values include codes like BB, BI, etc.

Sponsored-by: Martin Renvoize <martin.renvoize@gmail.com>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-28 16:36:19 +00:00
Amit Gupta 561ef2f025
Bug 42904: Prevent XSS in patron restriction comments
Test plan

1. Search for a patron and open the patron details page.
2. Under "Patron restrictions", add a manual restriction with the following comment:
   <img src=x onerror="alert(document.domain)">
3. Save the restriction.
4. Navigate to the patron checkouts page:
   /cgi-bin/koha/circ/circulation.pl?borrowernumber=xxx
   Verify that the JavaScript payload executes.
5. Edit the patron record:
   /cgi-bin/koha/members/memberentry.pl?op=edit_form&destination=circ&borrowernumber=xxx
   Verify that the JavaScript payload executes.
6. For OPAC - Go to OPAC Verify that the JavaScript payload executes - /cgi-bin/koha/opac-user.pl
7. Apply the patch.
8. Repeat steps 4, 5 and 6.
9. Verify that:
   1. No JavaScript is executed.
   2. The page loads normally without any alert dialog. Applicable for both the staff and opac.

Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-26 09:23:34 +00:00
Martin Renvoize d32ae7f064
Bug 42800: (QA follow-up) Fix backslash breaking the shelf list script block
erm.tt built the logged_in_user_lists JS array by manually
interpolating each shelf's id/name into a JS string literal, escaped
with the TT html filter:

    id: "[% shelf.shelfnumber | html %]",
    name: "[% shelf.shelfname | html %]",

The html filter escapes <, >, &, and " -- enough to stop the
<script>-tag and </script>-breakout scenario from the original bug --
but it does not escape backslashes. A list name ending in a backslash
(e.g. "foo\") escapes the closing quote of the JS string literal,
unbalancing it and throwing a JS syntax error that breaks the whole
inline <script> block on every ERM page (including the unrelated
`const logged_in_user = ...` a few lines below), reproducing the
"broken UI" half of the original bug via a different character.

Build the id/name pairs as an actual array of hashes and JSON-encode
them with To.json(), after escaping each value with the html
vmethod. JSON encoding (via the JSON module) correctly escapes
backslashes and quotes for use inside a JS string, while keeping the
values HTML-escaped so they stay inert if ever written into the DOM
via innerHTML (as the DataTables render callback in
EHoldingsLocalTitlesFormImport.vue does).

Test plan:
1. Enable the ERMModule system preference.
2. Create a list named: backslash test \
3. Go to /cgi-bin/koha/erm/eholdings/local/titles/import (or any
   other ERM page).
   => Without this patch: the page's inline script throws a syntax
      error ("Invalid or unexpected token") and
      "logged_in_user_lists is not defined", breaking the Vue
      component.
   => With this patch: the page loads without errors and the list is
      shown correctly in the table.
4. Also verify the original bug's script-tag scenario still works:
   create a list named <script>alert("booh!");</script> and confirm
   it displays as inert text, not executing, on the same pages.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-26 09:23:33 +00:00
Jonathan Druart 22a6ab9729
Bug 42800: Fix XSS in shelf list in the ERM module
const logged_in_user_lists = [% To.json(logged_in_user.virtualshelves.unblessed) | $raw %];

If you have a script tag in a list name the UI will be broken, and there
is potentially a XSS vulnerability

Test plan:
Create a new list with name="<script>alert("booh!");</script>"
Go to /cgi-bin/koha/erm/eholdings/local/titles/import
or any ERM views
=> Without this patch the UI is broken
=> With this patch applied it's not and the table is correctly populated
with the lists

Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-26 09:23:32 +00:00
Sanjar Tulkinov Anvar o'g'li 074e0b7261
Bug 42736: Prevent SQL injection in reports/cat_issues_top.pl
The "Most-circulated items" report built its SQL by concatenating the
Criteria and Filter request parameters directly into the query, with no
bind placeholders and no whitelist on the Criteria identifier. An
authenticated staff user with the reports permission could inject
arbitrary SQL and read any data reachable by the Koha database user.

This validates the Criteria parameter against a fixed allow-list of the
report's own column options (matching reports/cat_issues_top.tt) so it
is safe to use as a SQL identifier, and binds every Filter value with a
placeholder in both the column-header query and the main calculation
query. The Limit parameter is validated as a plain integer.

This mirrors the fix already applied to the sibling
reports/bor_issues_top.pl (bug 42369).

Test plan:
0. Apply the patch
1. koha-plack --restart kohadev
2. Go to Reports > Most-circulated items (reports/cat_issues_top.pl)
3. Run the report with various Criteria and Filter values, confirm the
   output is correct and no errors appear
4. Before the patch, a Filter value or the Criteria containing SQL (a
   single quote followed by a subquery) broke out of the query. After
   the patch, confirm such input is treated as a literal value: you get
   normal output or an empty result, with no SQL error, no delay, and no
   data leak

Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Aleisha Amohia <aleisha@catalyst.net.nz>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-26 09:23:29 +00:00
Jonathan Druart e8137a2c94
Bug 42322: Add Cypress tests
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:46 +00:00
Owen Leonard fc8f56b88f
Bug 42322: (follow-up) Correct markup of ILL sidebar menu
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:45 +00:00
Owen Leonard 71d8564789
Bug 42322: (follow-up) Switch to mobile-first handling of sidebar toggle
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:44 +00:00
Jonathan Druart c407f73309
Bug 42322: Hide collapsed sidebar on small screens
When the sidebar is collapsed and the screen is resized to <= 1000px,
'#sidebar-container' should be hidden

Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:43 +00:00
Jonathan Druart 7fb0345f7f
Bug 42322: Correctly toggle aria-expanded
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:42 +00:00
Jonathan Druart e8c7901788
Bug 42322: Add missing raw filter for aside_block
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:41 +00:00
Jonathan Druart 4d87ae3424
Bug 42322: Hide .patroninfo, .browse-controls and #search-facets when collapsed
Fix sidebar when collapsed on:
 * /members/moremember.pl?borrowernumber=42
 * /catalogue/search.pl?advsearch=1&idx=kw&q=d&idx=kw&q=&idx=kw&q=&weight_search_submitted=1&weight_search=1&sort_by=relevance
 * /catalogue/detail.pl?biblionumber=438&searchid=scs_1776086577193
   (with BrowseResultSelection ON)

Signed-off-by: Clemens Tubach <clemens.tubach@kit.edu>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:40 +00:00
Owen Leonard 34644298e4
Bug 42322: (follow-up) A version with just the button
Signed-off-by: Clemens Tubach <clemens.tubach@kit.edu>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:39 +00:00
Owen Leonard f9dc282e26
Bug 42322: (follow-up) Proposed alternative style
This version keeps a visual indicator of the collapsed sidebar.

I've also added handling of the "aria-expanded" attribute.

Signed-off-by: Clemens Tubach <clemens.tubach@kit.edu>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:38 +00:00
Jonathan Druart 4784d952e7
Bug 42322: Vue apps
Signed-off-by: Clemens Tubach <clemens.tubach@kit.edu>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:37 +00:00
Owen Leonard b804854865
Bug 42322: Auto tidy
To ease rebase!

Re-run.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:36 +00:00
Jonathan Druart be9a2c7c5f
Bug 42322: Use main-container.inc from reports
Pass the aside block from the report module

Signed-off-by: Clemens Tubach <clemens.tubach@kit.edu>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:35 +00:00
Jonathan Druart add690d0f4
Bug 42322: Pass the aside to main-container
Improve main-container.inc so we can pass a specific aside block

Signed-off-by: Clemens Tubach <clemens.tubach@kit.edu>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:34 +00:00
Jonathan Druart ab7602b0f3
Bug 42322: Add a 'collapse sidebar' button
On bug 42190 we discuss the possibility to collapse the sidebar in order to enlarge the screen and specifically the table in the main div.

This patch adds the button to main-container.inc that is supposed to be
used from everywhere.

Signed-off-by: Clemens Tubach <clemens.tubach@kit.edu>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:33 +00:00
Matt Blenkinsop ce3643073a
Bug 39959: Add a total column to the invoices table
Test plan:
1. Create an order and receive it to create an invoice
2. In the invoices search, look for your invoice
3. There will be no total against the invoice
4. Apply patch and restart_all
5. Refresh the page, the total column should now be present

Signed-off-by: Coleen Lilly <clilly@aacpl.net>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:32 +00:00
Matt Blenkinsop 2ce97a58e2
Bug 12278: (follow-up) Use invoice quantity to sum total
Prevents additional orders from subscriptions from being swallowed into the invoice total calculation

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:31 +00:00
Matt Blenkinsop f7bbdd4976
Bug 12278: Add unit tests
Signed-off-by: Coleen Lilly <clilly@aacpl.net>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Sponsored-by: Anne Arundel County Public Library <https://www.aacpl.net/>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:30 +00:00
Matt Blenkinsop b114eb709f
Bug 12278: Add a total column to the table in parcels.pl
Test plan:
1. Create an order with multiple items and receive the order in full
2. From the vendor record, click Receive shipments
3. You should be in parcels.pl and the table should show your shipments
4. There will be no column displaying the total
5. Apply patch and restart_all
6. Refresh the page, the column should now be visible showing the total

Signed-off-by: Coleen Lilly <clilly@aacpl.net>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:29 +00:00
Olivia Reynolds e886c3a7e0
Bug 39337: (follow-up) Default to not suppressing OAI records
In the case when a biblio is deleted, we should not suppress its
orphaned records. Therefore, invert the conditions in the WHERE clause
to not suppress the record if no biblio could be found.

Test plan:
1.  Enable the system preferences "OAI-PMH" and "OpacSuppression".
    Ensure "OpacSuppressionByIPRange" is blank.
2.  Navigate to
    /cgi-bin/koha/oai.pl?verb=ListRecords&metadataPrefix=oai_dc
3.  Select a displayed record, and delete its corresponding biblio.
4.  Refresh the OAI page, verify the record has vanished.
5.  Apply patch.
6.  Refresh the OAI page, verify the record now appears and is marked as
    deleted. It may have moved to the top of the list.

Sponsored-by: Open Fifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:28 +00:00
Olivia Reynolds a03c119ccd
Bug 39337: (follow-up) Explicitly name tables
As we're aliasing the primary tables as `main`, we need to use that name
to refer to their columns.

Test plan:
1.  Apply patch.
2.  Prove t/db_dependent/OAI/Server.t and verify no tests fail.

Sponsored-by: Open Fifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:27 +00:00
Olivia Reynolds 091f0d7a89
Bug 39337: Filter out suppressed OAI records in SQL
Rather than fetching each biblio before choosing to suppress it, this
patch performs this filtering in the initial SQL query that fetches the
biblionumbers.

Note: for queries including items, this additional filtering is
performed in each of the three subqueries, necessitating three
quasi-joins against biblio. If we were to hoist this filtering to the
outer layer, we would also need to remove the LIMIT from each subquery
so as to allow each of them to be fairly weighted. As the preceding
comment implies keeping the subqueries separate has been beneficial for
performance, I've elected not to hoist the WHERE.

Test plan (specific URLs given for KTD, with thanks to David Cook):

1.  Apply patch.
2.  Navigate to http://localhost:8081/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=2
3.  Set 942$n to "Yes" and save the record.
4.  Disable the system preference "OpacSuppression", and ensure
    "OpacSuppressionByIPRange" is blank.

5a. Verify you can successfully access
    http://localhost:8080/cgi-bin/koha/oai.pl?verb=GetRecord&metadataPrefix=oai_dc&identifier=KOHA-OAI-TEST:2
5b. Verify KOHA-OAI-TEST-2 appears on
    http://localhost:8080/cgi-bin/koha/oai.pl?verb=ListRecords&metadataPrefix=oai_dc

6.  Enable the system preference "OpacSuppression", and ensure
    "OpacSuppressionByIPRange" is blank.
7.  Repeat steps 5a-5b, verifying that:
    - the OAI GetRecord page returns "There is no biblio record with
      this identifier".
    - the OAI ListRecords page does not show KOHA-OAI-TEST-2.

Sponsored-by: Open Fifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:26 +00:00
Olivia Reynolds 4dd0bfc107
Bug 39337: Add is_opac_suppressed helper to C4::Context
Previously, OPAC suppression was applied inconsistently, with only some
callers taking OpacSuppressionByIPRange into account. This patch adds a
new helper to C4::Context to determine whether OPAC suppression should
be active for this request, and switches to using it everywhere that was
previously manually checking the system preferences.

Test plan (specific URLs given for KTD, with thanks to David Cook):

1.  Apply patch.
2.  Navigate to http://localhost:8081/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=2
3.  Set 942$n to "Yes" and save the record.
4.  Disable the system preference "OpacSuppression", and ensure
    "OpacSuppressionByIPRange" is blank.

5a. Verify you can successfully access
    http://localhost:8080/cgi-bin/koha/opac-detail.pl?biblionumber=2
5b. Verify you can successfully access
    http://localhost:8080/cgi-bin/koha/opac-MARCdetail.pl?biblionumber=2
5c. Verify you can successfully access
    http://localhost:8080/cgi-bin/koha/opac-ISBDdetail.pl?biblionumber=2
5d. Verify you can successfully access
    http://localhost:8080/cgi-bin/koha/opac-search.pl?idx=&q=The+ideas+behind&weight_search=1
5e. Verify you can successfully access
    http://localhost:8080/api/v1/public/biblios/2
5f. Verify you can successfully access
    http://localhost:8080/cgi-bin/koha/oai.pl?verb=GetRecord&metadataPrefix=oai_dc&identifier=KOHA-OAI-TEST:2
5g. Verify KOHA-OAI-TEST-2 appears on
    http://localhost:8080/cgi-bin/koha/oai.pl?verb=ListRecords&metadataPrefix=oai_dc

6.  Enable the system preference "OpacSuppression", and ensure
    "OpacSuppressionByIPRange" is blank.
7.  Repeat steps 5a-5g, verifying that:
    - the OPAC details pages return "Record blocked".
    - the OPAC search page returns "No results found".
    - the API returns "Bibliographic record not found".
    - the OAI GetRecord and ListRecords show "Record Status: deleted"
      for KOHA-OAI-TEST-2.
8.  Keep "OpacSuppression" enabled, but set "OpacSuppressionByIPRange"
    to "." (to allow all IPs to bypass suppression).
9.  Repeat steps 5a-5g, verifying that every page is successful again.
10. Run `prove t/Context.t`, and verify all tests are successful.

Sponsored-by: Open Fifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:25 +00:00
Jonathan Druart 8316c01bac
Bug 41706: Add a test
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

Amended:
-    $marc->append_fields( MARC::Field->new( '100', ' ', ' ', a => 'Aristotle.' ) );
-    marc2cites($marc);
+    $marc_record->append_fields( MARC::Field->new( '100', ' ', ' ', a => 'Aristotle.' ) );
+    marc2cites($marc_record);

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:24 +00:00
Roman Dolny b60b7c3a3f
Bug 41706: "Use of uninitialized value..." warning in C4/Record.pm
The warning:
[WARN] Use of uninitialized value $2 in split
    at /kohadevbox/koha/C4/Record.pm line 924.
appears in the plack-opac-error.log file.
It happens during the preparation of data for the "Cite" feature
in OPAC for some of author's fields.

To test:
========
1. Observe plack-opac-error.log.
2. In OPAC go to record with one word author like Aristophanes,
   Aristotle or Xenophon (biblionumbers: 64, 176, 254).
3. Show it in any of "detail" view (Normal, MARC, ISBD).
4. Warning appears in plack-opac-error.log.
5. Apply the patch; restart_all.
6. Repeat 2-3. Warning doesn't appear.

Sponsored-by: Ignatianum University in Cracow
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-25 08:56:20 +00:00
Paul Derscheid 138d2b64ed
Bug 42207: Allow library embed on GET /patrons/{patron_id}
- Add 'library' to the allowed x-koha-embed values for the
  GET /patrons/{patron_id} endpoint
- Add unit test verifying the library embed returns library_id
  and name

Test plan:
1. Apply patch
2. yarn api:bundle
3. prove t/db_dependent/api/v1/patrons.t
4. Verify all tests pass, including the new 'library embed'
   subtest under get() which verifies that requesting
   x-koha-embed: library returns the patron's library object

Sponsored-by: Büchereizentrale Schleswig-Holstein <https://www.bz-sh.de/>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-21 15:58:25 +00:00
Olivia Reynolds ad30c815ce
Bug 34166: Add total column to suggestions table
This patch displays the "total_price" field from the API in the
acquisition suggestions table. In line with other totals, it is
displayed unitless after currency conversion.

Test plan:
1.  Navigate to Acquisitions > Suggestions.
2.  Click "New purchase suggestion", then add a title, price, and
    number of copies.
3.  Make note of the calculated total price.
3.  Save the suggestion to return to the table.
4.  Verify there is no "Total" column on the suggestions table.
5.  Apply patch, update database, and refresh.
6.  Verify there is a "Total" column on the suggestions table that
    matches the previously displayed total price.
7.  Verify the "Total" column is listed under the "Columns" dropdown.
8.  Above the suggestions table, click "Configure".
9.  In the column settings for suggestions, check both "Is hidden by
    default" and "Cannot be toggled" for "total".
10. Underneath the column settings for suggestions, uncheck "Save
    configuration state on page change".
11. Save the column settings.
12. Return to the suggestions table.
13. Verify the "Total" column is hidden.
14. Verify the "Total" column is not listed under the "Columns"
    dropdown.

Sponsored-by: Open Fifth
Signed-off-by: Michaela <michaela.sieber@kit.edu>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-21 15:58:24 +00:00
Owen Leonard 89066b3c7b
Bug 42111: Update various admin pages to use grid layout for forms - Part 1
This patch updates several administration templates with new markup and
CSS for layout forms using CSS grids.

To test, apply the patch rebuild the staff interface CSS.

Check the following pages, confirming that the "New" and
"Edit" versions of the form look correct in each case:

- Administration -> Additional fields
- Administration -> Audio alerts
- Administration -> Cities and Towns
- Administration -> Classification configuration
  -> Classification sources
  -> Classification filing rules
  -> Classification splitting rules

On the classification splitting rules page I've reformatted the regular
expression section to fit better. To test, add or edit a splitting rule.

- Under "Splitting routine" choose "RegEx"
  - "Regular expression" and "Test the regular expressions" fields
    should appear.
- Add one or more regular expressions, e.g.

  s/\s/\n/g
  s/(\s?=)/\n=/g
  s/^(J|K)\n/$1 /

- In the "Test the regular expressions" text area enter some call
  numbers e.g.

  830 Han
  159.9 (091) Gesh
  J 3 Kin =774
  830 Hil =774 4
  830 Las=20 4

- Click "Test"
- The formatted call numbers should appear below in a grid:

+---------+------------------+--------------+----------------+--------------+
| 830 Han | 159.9 (091) Gesh | J 3 Kin =774 | 830 Hil =774 4 | 830 Las=20 4 |
+---------+------------------+--------------+----------------+--------------+
| 830     | 159.9            | J 3          | 830            | 830          |
| Han     | (091)            | Kin          | Hil            | Las          |
|         | Gesh             | =774         | =774           | =20          |
|         |                  |              | 4              | 4            |
+---------+------------------+--------------+----------------+--------------+

Sponsored-by: Athens County Public Libraries
Signed-off-by: Felicie <felicie.thiery@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-21 15:58:23 +00:00
Katrin Fischer 7de8e290b9
Bug 35061: Fix documentation for -pf|preserve-field in import_patrons.pl
There was a mismatch between the documentation and the shortcut used
in the code. This patch fixes the documentation so it matches the code:

'pf|preserve-field=s'            => \@preserve_fields,

=item B<-k|--preserve-field>

Prevent specified patron fields for existing patrons from being overwritten

So pf is the right shortcut.

Signed-off-by: Aleisha Amohia <aleisha@catalyst.net.nz>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-21 15:58:20 +00:00
Tomás Cohen Arazi d6e40f612c
Bug 20846: Filter suppressed records from public lists in OPAC
When OpacSuppression is enabled, records marked as suppressed
(biblio.opac_suppressed = 1) still appear in public lists viewed
through the OPAC. This is confusing for patrons.

This patch adds a filter_by_not_suppressed method to
Koha::Virtualshelfcontents and uses it in opac-shelves.pl to
exclude suppressed records from list display when OpacSuppression
is enabled. OpacSuppressionByIPRange is respected: users within
the allowed IP range still see all records.

Test plan:
1. Apply patch
2. Enable OpacSuppression system preference
3. Create a public list and add some titles to it
4. Suppress one of the titles (set 942$n = 1)
5. View the list in the OPAC
=> SUCCESS: Suppressed record is no longer shown in the list
6. Set OpacSuppressionByIPRange to match your IP (e.g. 127.)
7. Repeat step 5
=> SUCCESS: All records (including suppressed) are shown
8. Run:
   $ ktd --shell
   k$ prove t/db_dependent/Virtualshelves.t
=> SUCCESS: Tests pass!
9. Sign off :-D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-21 14:31:50 +00:00
Olivia Reynolds ff2b951c4d
Bug 41648: Validate plugin date metadata
If a plugin reports a date_authored or date_updated we can't parse,
remove the invalid property from the metadata hashref and emit a
warning.

To test this, introduce a separate test plugin directory and test file.
Having a plugin that produces warnings in the main test directory would
require accounting for this warning in every existing test that loads
plugins. Correspondingly, this patch also fixes an invalid date in an
existing test plugin.

Test plan:
1. Apply patch.
2. Run `prove -r t/db_dependent/Koha/Plugins/` and verify there are no
   test failures.

Sponsored-by: Open Fifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-21 14:31:48 +00:00
Olivia Reynolds fd24968eb2
Bug 41648: Handle parse errors in KohaDates
If dt_from_string throws an exception whilst trying to parse the input,
catch it and return an empty result.

Test plan:
1. Apply patch.
2. Prove t/db_dependent/Template/Plugin/KohaDates.t

Sponsored-by: Open Fifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-21 14:31:47 +00:00
Matt Blenkinsop dca7313511
Bug 43203: Correctly rollback test data
Test plan:
1) In the database, verify how many biblios exist (select count(*) from biblio;)
2) Run prove t/db_dependent/Koha/Item.t
3) Check the count again, there should now be 3 extra bib records left from the test run
4) Apply patch
5) Repeat the process, the biblio count should never grow after test runs

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-21 14:31:46 +00:00
Andrew Fuerste Henry fbfccfdbf2
Bug 43331: Add ccode class to holdings table
To test:
1 - go to a bib record with items
2 - inspect the items table, confirm there is no class on the cell
    containing the ccode
3 - Apply patch, restart_all
4 - reload your bib page
5 - inspect the table again, confirm the cell has the class "ccode"

Signed-off-by: Owen Leonard <oleonard@myacpl.org>

Signed-off-by: Michelle Spinney <mspinney@clamsnet.org>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-21 14:31:44 +00:00
Fridolin Somers 634193fc6a
Bug 43339: automatic_item_modification_by_age_configuration is not a real system preference
It should not fail the unit test t/db_dependent/check_sysprefs.t.

Test plan :
1.0) Without patch
1.1) Run : prove t/db_dependent/check_sysprefs.t
=> 'All tests successful'
1.2) Go to Cataloging > Automatic item modifications by age
1.3) Create some entries and save
1.4) Run : prove t/db_dependent/check_sysprefs.t
=>
    #   Failed test at t/db_dependent/check_sysprefs.t line 96.
    #          got: '1'
    #     expected: '0'
    # Too many sysprefs in DB: automatic_item_modification_by_age_configuration
2.0) Apply patch
2.1) Run : prove t/db_dependent/check_sysprefs.t
=> 'All tests successful'

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-21 14:31:43 +00:00
Andrew Fuerste Henry b33bb4b380
Bug 43342: Remove line due to template toolkit
To test:
1 - go to the OVERDUES_SLIP notice and confirm you are using the default content
2 - have or create a patron with an overdue item
3 - click Print and then Print overdues
4 - confirm there are two empty lines between the first and second lines of text
APPLY PATCH, restart_all
5 - reload your OVERDUES_SLIP notice page
6 - click View default
7 - confirm the FOREACH statement now starts with "[%-"
8 - click Copy to Template, save your changes
9 - print your patron's overdue slip again, confirm you have only one blank line

Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-21 14:31:40 +00:00
Hammat Wele 35b58d65c5
Bug 43211: (follow-up) fix 'barcode is not defined' error
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-20 16:10:44 +00:00
Janusz Kaczmarek a17d27f8ae
Bug 43211: Koha hangs when receiving order with AcqCreateItem set to 'receiving an order'
Koha hangs when receiving an order with AcqCreateItem set to
"receiving an order".

Test plan:

1. Create a basket and set "Create items when" to "receiving an order".
2. Add an order and close the basket.
3. Attempt to receive the order. Koha will most likely hang.
4. Apply the patch; restart_all.
5. Repeat p. 3. You should be able co proceed.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-20 16:10:38 +00:00
Tomás Cohen Arazi 3522dc2bca
Bug 42567: Remove C4::Koha::GetNormalizedEAN in favor of MetadataExtractor
Adds get_normalized_ean to the flavor-specific extractors:
- MARC21: 024$a with indicator 1 = 3
- UNIMARC: 073$a

Exposes it as $biblio->normalized_ean and migrates all callers.

Assisted-by: Sonnet 4.6 (Anthropic)

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/Koha.t \
          t/db_dependent/Koha/Biblio.t \
          t/db_dependent/Koha/Biblio/Metadata/Extractor/MARC/MARC21.t \
          t/db_dependent/Koha/Biblio/Metadata/Extractor/MARC/UNIMARC.t
=> SUCCESS: Tests pass
3. Verify EAN numbers still display in OPAC and staff detail pages
4. Sign off :-D

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-20 15:34:10 +00:00
Tomás Cohen Arazi 3d3f625b56
Bug 42566: Remove C4::Koha::GetNormalizedUPC in favor of MetadataExtractor
GetNormalizedUPC duplicated logic already available via the
MetadataExtractor (Bug 34828) and exposed as $biblio->normalized_upc.

All callers migrated:
- Scripts with $biblio available: use $biblio->normalized_upc
- C4::Search (only MARC::Record): reuses the extractor instance
  already created for OCLC

Assisted-by: Sonnet 4.6 (Anthropic)

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/Koha.t \
          t/db_dependent/Koha/Biblio.t
=> SUCCESS: Tests pass
3. Verify UPC numbers still display in OPAC and staff detail pages
   (Syndetics cover images)
4. Sign off :-D

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-20 15:34:08 +00:00
Tomás Cohen Arazi 5fb7fd5f2d
Bug 35142: Remove GetNormalizedOCLCNumber, oclc_number, strip_orgcode
All callers migrated. Removes dead code:

- C4::Koha::GetNormalizedOCLCNumber
- Koha::Util::MARC::oclc_number
- Koha::Util::MARC::strip_orgcode
- Koha::Util::MARC::OCLC_REGEX constant

Test plan:
   $ prove t/Koha/Util/MARC.t t/db_dependent/Koha.t

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Assisted-by: Sonnet 4.6 (Anthropic)
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-20 15:34:07 +00:00
Tomás Cohen Arazi 263a1bd536
Bug 35142: Migrate callers to use metadata extractor
Replaces all calls to GetNormalizedOCLCNumber() and
Koha::Util::MARC::oclc_number() with the extractor pattern.

OPAC scripts pass their already ViewPolicy-filtered $record directly
to the extractor. This preserves the existing filtering behavior
without introducing new abstraction layers:

    Koha::Biblio::Metadata::Extractor->new( { metadata => $record } )->get_normalized_oclc

Intranet scripts and the Koha::Biblio convenience method use the
raw cached extractor (035 visibility is not affected by ViewPolicy
in practice, and intranet doesn't filter):

    $biblio->normalized_oclc

Test plan:
1. Verify OCLC numbers display on OPAC detail, ISBD, MARC detail
2. Verify staff catalogue detail and search results
3. prove t/db_dependent/Koha/Biblio.t

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Assisted-by: Sonnet 4.6 (Anthropic)
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-20 15:34:06 +00:00
Tomás Cohen Arazi 6b77f28dd7
Bug 35142: Move get_normalized_oclc to base MARC extractor
Moves get_normalized_oclc from the MARC21-specific extractor to the
base ::MARC class (035$a is flavor-independent) with improved matching:

- Case-insensitive OCoLC detection (matches ocolc, OCOLC, etc.)
- Generic orgcode prefix stripping (any parenthesized prefix, not
  just literal "(OCoLC)") for resilience against cataloging typos

Test plan:
   $ prove t/db_dependent/Koha/Biblio/Metadata/Extractor/MARC/MARC21.t

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Assisted-by: Sonnet 4.6 (Anthropic)
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-20 15:34:02 +00:00
Pedro Amorim 4dae704301
Bug 41029: DBRev 26.06.00.016
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 16:07:59 +00:00
Martin Renvoize 575e90f88f
Bug 41029: (QA follow-up) Seed add_to_basket record source and tidy atomicupdate
Without a matching entry in the record_sources table, the new
"Add to basket" option never appears in the Source filter dropdown
of the marc-overlay-rules admin page for module=source, since that
dropdown is populated from Koha::RecordSources, not the template's
label switch. Seed it in both the mandatory install-time data and
the upgrade atomicupdate, matching how batchimport/z3950/etc. were
introduced.

Also drop the unused say_warning import, route success messages
through say_success instead of a bare say, and simplify the
selectrow_array calls.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 16:07:58 +00:00
Nick Clemens f473c7d451
Bug 41029: Add option for marc overlay rules to apply when adding to a basket
This patch adds a new option to allow defining overlay rules for 'Add to basket' - such that they
will be used when adding orders in acquisition. Existing 'Staged MARC Import' rules will be copied to
the new fitler in order to preserve behavior

To test:
 1 - Paste the following into MarcFieldsToOrder and save
  price: 975$p
  quantity: 975$q
  budget_code: 975$h
 2 - Paste the following into MarcItemFieldsToOrder and save
  homebranch: 949$a
  holdingbranch: 949$b
  itype: 949$y
  nonpublic_note: 949$x
  public_note: 949$z
  loc: 949$c
  ccode: 949$8
  notforloan: 949$7
  uri: 949$u
  copyno: 949$t
  price: 949$g
  replacementprice: 949$v
  itemcallnumber: 949$o
  quantity: 949$k
  budget_code: 949$l
 3 - Go to Admin -> Record overlay rules
 4 - Define 2 rules:
  Source - Staged Marc Import - 949 - Add and append (the rest of the options are set when add and append is selected
  Source - Staged Marc Import - * - Protect (the rest of the options are set when add and append is selected
 5 - Grab test file from bug 39518
 6 - Import the records, ignoring items
 7 - Edit the test file and change some values for the 949 rows
 8 - Go to Acquisitions, find a basket for a vendor, add to basket from a new file
 9 - Stage the file to find matches (already imported above) and to replace
10 - Complete the orders
11 - View the record and confirm the new 949 are added
12 - Edit the 949 in the file again
13 - Apply patch, update database, restart all
14 - Edit overlay rules and remove the 'Staged Marc Import' rules, leaving only the 'Add to basket rules'
15 - Add to basket again using the updated file and confirm new 949 are added
16 - Success!

Signed-off-by: Benjamin Daeuber <BDaeuber@fargolibrary.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 16:07:57 +00:00
John Doe 9fc9f84b8d
Bug 28740: (follow-up) Add missing C4::Log import for cronlogaction
The patch added cronlogaction() calls but did not import the function.
This caused a runtime error: Undefined subroutine &main::cronlogaction.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ perl misc/cronjobs/cleanup_database.pl --illrequests-days 30 \
       --illrequests-status COMP -v
=> SUCCESS: Dry-run works, reports count of requests that would be removed
3. Run with --confirm to actually purge
4. Sign off :-D

Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 16:07:56 +00:00
Lari Strand 5ebc4d79a1
Bug 28740: Add option for cleaning up ILL requests
This adds two new parameters:
--illrequests-status
--illrequests-days (optional, defaults to 30 days)

These can be repeated to include specific statuses for cleanup. At least one status must be specified.

To test:
0 - Apply patch
1 - Create several ILL requests with different statuses (e.g., CANCELLED, REJECTED, COMPLETED, PENDING)
2 - Set some requests to be older than 30 days (update their updated timestamp)
3 - Run cleanup_database.pl with --illrequests-status CANCELLED
4 - CANCELLED requests older than 30 days are removed, others remain
5 - Run cleanup_database.pl with --illrequests-status CANCELLED --illrequests-status REJECTED
6 - Both CANCELLED and REJECTED requests older than 30 days are removed, other statuses remain
7 - Run cleanup_database.pl with --illrequests-days 5 --illrequests-status PENDING
8 - PENDING requests older than 5 days are removed
9 - Verify dry-run mode (no --confirm) correctly counts records that would be deleted without deleting them
10 - Run cleanup_database.pl with --illrequests-days without any --illrequests-status
11 - Error message displayed requiring at least one status to be specified

Sponsored-by: Koha-Suomi Oy
Signed-off-by: John Doe <tomascohen@gmail.com>
edit: tidied the file (tcohen)
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 16:07:54 +00:00
Pedro Amorim f2003e3e52
Bug 35104: (RM follow-up) Restore StripNonXmlChars in TransformHtmlToMarc
Removed by 6726b1a254 on the assumption store() strips centrally -
but TransformHtmlToMarc never calls store(), so callers like
addbiblio.pl (FindDuplicate, duplicate redisplay) see unstripped
control characters again, the same failure Bug 34549 fixed.

repair_marcxml covers more faults but works on full MARCXML and can
throw - too big a change here; left as a follow-up.

Test plan:
prove t/db_dependent/Biblio/TransformHtmlToMarc.t

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 14:34:00 +00:00
Olivia Reynolds dd7777c8b9
Bug 43313: Fix catalogue detail shelving location display
During the tidying up of Bug 41566, av_loc was accidentally changed to
map descriptions to codes, rather than vice-versa. This broke the
formatting of item permanent locations.

Test plan (for KTD):
1.  Navigate to an catalogue detail page, for example
    http://localhost:8081/cgi-bin/koha/catalogue/detail.pl?biblionumber=262
2.  Run the following SQL, replacing 262 with the biblionumber of your
    chosen page:
      UPDATE items SET location='CHILD' SET permanent_location='GEN' WHERE biblionumber=262;
3.  Refresh the page and verify the items' shelving locations display
    "(Children's area)" only.
4.  Apply patch and force refresh.
5.  Verify the shelving location now correctly displays "General
    stacks" and "(Children's area)" on separate lines.

Sponsored-by: Open Fifth
Signed-off-by: John Vinke <jvinke@austinseminary.edu>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 14:33:59 +00:00
Martin Renvoize bad9bb47ae
Bug 43215: preferences.js: Report one save confirmation per preference, not per checkbox option
Checkbox-group system preferences (multiple_sortable, ill_backends, and
the OPAC/staff interface language selectors) render one checkbox per
option. All the checkboxes for a given preference share its 'name'
attribute, but each has its own unique 'id'.

Saving such a preference produced one "Saved preference ..."
confirmation line per checkbox instead of a single line for the
preference as a whole, because KOHA.Preferences.Success() built the
message from each modified element's 'id' rather than its 'name'.

A related issue: the checkbox 'change' handler marked every
'.preference-checkbox' on the whole page as modified, regardless of
which preference it belonged to. This meant editing one checkbox-group
preference silently re-saved (and, before this patch, also reported as
saved) any other checkbox-group preference sharing the same tab - for
example, editing OPACLanguages would also touch the unrelated
StaffInterfaceLanguages preference on the I18N/L10N tab.

This patch:
* Scopes the 'modified' marking to checkboxes sharing the same
  preference name, so unrelated checkbox-group preferences on the same
  tab are left alone.
* Groups the save confirmation message by preference name
  (deduplicated), so each preference produces exactly one "Saved
  preference" line regardless of how many options it has.

Test plan:
1. Go to Administration > System preferences and search for
   OPACAuthorIdentifiersAndInformation.
2. Select two or more identifier types and click Save.
3. Confirm the confirmation banner shows a single "Saved preference
   OPACAuthorIdentifiersAndInformation" line, not one per selected
   identifier.
4. Drag one of the selected items to reorder it, click Save again,
   confirm you still get only one confirmation line, then reload the
   page and confirm the new order was kept.
5. Go to Administration > System preferences > I18N/L10N tab.
6. Toggle one of the checkboxes under "Enable the following languages
   on the OPAC" (OPACLanguages) and click Save.
7. Confirm the confirmation only reports OPACLanguages as saved - the
   staff interface language list (StaffInterfaceLanguages) should not
   appear in the message.
8. Reload the page and confirm the staff interface language selection
   is unchanged.
9. As a sanity check, modify an ordinary non-checkbox preference (e.g.
   a free text or yes/no preference) and confirm it still reports
   exactly one "Saved preference" line, same as before this patch.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 14:33:58 +00:00
Andrew Fuerste Henry 549e0b3068
Bug 42553: (QA Follow-up) tidy preferences.pl
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 14:33:57 +00:00
Martin Renvoize eb93c2b2e7
Bug 42553: preferences.pl: Allow 'multiple_sortable' to use 'authval' source
The 'choices' renderer in admin/preferences.pl already supports a
dynamic 'authval' source: the YAML can say

    choices: authval
    source: SOMECATEGORY

and the choices are populated from the named authorised value category
at render time.

The 'multiple_sortable' renderer (the SortableJS-backed drag-and-drop
multi-select used by OPACAuthorIdentifiersAndInformation and
ArticleRequestsSupportedFormats) had no equivalent — choices had to be
hardcoded in the .pref YAML.

This patch adds the same dispatcher to 'multiple_sortable' so prefs
needing an ordered, picker-driven multi-select of authorised values
can declare:

    multiple_sortable: authval
    source: PAYMENT_TYPE

Behaviour for existing prefs (which pass a hash directly to
'multiple_sortable') is unchanged — the new branch only fires when the
value is a scalar.

Test plan:
1. Create a temporary system preference using the new pattern, e.g. by
   adding to admin/preferences/accounting.pref:
       - pref: TestSortableAV
         multiple_sortable: authval
         source: PAYMENT_TYPE
   and seeding INSERT INTO systempreferences with type='Free'.
2. Visit Administration > System preferences and find the pref.
3. Verify the widget renders as a sortable list of every value in the
   PAYMENT_TYPE authorised value category, with checkboxes and drag
   handles, matching the look of OPACAuthorIdentifiersAndInformation.
4. Pick two or three, reorder them, save.
5. Reload the page and verify the order and selection are preserved.
6. With 'source' omitted or 'multiple_sortable' set to a string other
   than 'authval', verify the page raises a clear error.
7. Existing prefs (OPACAuthorIdentifiersAndInformation,
   ArticleRequestsSupportedFormats) continue to render unchanged.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Debbie McCarthy <debbie.mccarthy@westsussex.gov.uk>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 14:33:56 +00:00
Kyle M Hall 812ac7155d
Bug 41197: Don't require a CSRF token for opac-account-pay-return.pl
This page is intended for redirect/return from external payment vendors - they are not going to have/get a CSRF token from Koha for this.
The payments are handled in a POST to the API, so this should not be a sensitive page, it just provides user confirmation.
Some vendors use only a POST, we should not require CSRF on this page.

Test Plan:
1) Apply the first patch
2) prove t/Koha/Middleware/CSRF.t
3) Note the tests fail!
4) Apply this patch
5) prove t/Koha/Middleware/CSRF.t
6) All tests pass!

Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 14:33:55 +00:00
Kyle M Hall 3e87d13e67
Bug 41197: Add unit tests
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 14:33:52 +00:00
Pedro Amorim 22fad96c0d
Bug 35104: (RM follow-up) Fix regressions.t for Metadata's new auto-repair
Koha::Biblio::Metadata->store() now repairs recoverable MARCXML faults
before saving, so this test's invalid data no longer reaches the DB
and detail.pl never hits a decoding error.

Write the metadata directly via the DBIC row instead, bypassing
store()'s repair, so the test still exercises detail.pl's own
defensive handling of corrupt metadata.

Test plan:
prove t/db_dependent/selenium/regressions.t

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 11:03:34 +00:00
Pedro Amorim 868751b221
Bug 43091: (RM follow-up) Give the OPAC self-renewal island a Pinia store
hydrate() only installs Pinia for an island when its config.stores is
non-empty. patron-self-renewal kept config: {} after islands-opac.ts
was split out, so it never got one.

http-client.js's post() calls submitting(), which needs Pinia's
useMainStore(). With none active, it throws before fetch() runs -
get() skips submitting() by default, so only the actual renewal
submission failed.

Test plan:
prove t/cypress/integration/Islands/SelfRenewal.ts

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 11:03:33 +00:00
Pedro Amorim c4e70ad745
Bug 35797: (RM follow-up): Fix xt/api.t
prove xt/api.t

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-19 11:03:30 +00:00
Pedro Amorim 0e69e8d3a1
Bug 41996: DBRev 26.06.00.015
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:39 +00:00
Kyle M Hall d9b06c79e3
Bug 41996: (QA follow-up) Fix up atomic update
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:37 +00:00
Kyle M Hall d576dbadcb
Bug 41996: (QA follow-up) Test that invoicenumber is recorded on EDI invoice errors
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:36 +00:00
Kyle M Hall 7e445f1ce6
Bug 41996: (QA follow-up) Update EDI.t for improved invoice error messages
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:35 +00:00
Martin Renvoize e9a442e933
Bug 41996: Filter EDI errors on invoice page by invoicenumber
The invoice page previously fetched all edifact_errors for the EDI
message, causing errors from unrelated invoice lines to appear on every
invoice generated from the same EDI file. For example, one unmatched
ordernumber in a file with 20 invoices would flag all 20 as having
errors.

Now only errors where invoicenumber matches the current invoice (or
where invoicenumber is NULL, indicating a message-level error) are
displayed.

Signed-off-by: Ray Delahunty <lib-systems@arts.ac.uk>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:34 +00:00
Martin Renvoize a3889a7d90
Bug 41996: Thread invoicenumber through EDI invoice error recording
All errors generated during process_invoice and receipt_items now
store the invoicenumber of the specific invoice being processed. This
allows errors to be displayed only on the relevant invoice page rather
than appearing on every invoice generated from the same EDI file.

Also fixes two pre-existing spacing bugs in error message strings
("itemnumberis missing" and "barcode"for invoice line").

Signed-off-by: Ray Delahunty <lib-systems@arts.ac.uk>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:33 +00:00
Martin Renvoize 51e4567ca6
Bug 41996: Schema Updates
Signed-off-by: Ray Delahunty <lib-systems@arts.ac.uk>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:32 +00:00
Martin Renvoize 60cde3718a
Bug 41996: Add invoicenumber column to edifact_errors table
Adds an optional invoicenumber column to edifact_errors so that errors
generated during invoice processing can be associated with the specific
invoice they relate to, rather than only the EDI message as a whole.

Message-level errors (e.g. unmatched vendor EAN) leave invoicenumber
NULL, indicating they apply to the whole file.

Signed-off-by: Ray Delahunty <lib-systems@arts.ac.uk>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:31 +00:00
Martin Renvoize 52d3cd3835
Bug 41996: Improve EDI invoice error messages for branch mismatches
When EDI invoice processing fails to match an item to a branch, the
error message now includes:
- The ordernumber referenced in the invoice (RFF+LI segment)
- The branches that order actually has items at
- A hint that the ordernumber in the invoice may be incorrect

Also adds a warning when an invoice line references an order that is
already marked as complete, which can indicate the supplier sent the
wrong ordernumber.

These improvements help acquisitions staff and vendors quickly diagnose
whether the problem is a supplier error (wrong RFF+LI value) rather
than a Koha data issue.

Signed-off-by: Ray Delahunty <lib-systems@arts.ac.uk>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:30 +00:00
Kyle M Hall 9e8075eaaf
Bug 42789: (QA follow-up) Add test for cancelling an order with a held item
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:29 +00:00
Kyle M Hall 220b09c4e0
Bug 42789: (QA follow-up) Log item deletion failures during EDI cancellation
This patch logs any object messages returned by cancel() to the EDI log,
including the itemnumber if available.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:28 +00:00
Martin Renvoize 45a60d487c
Bug 42789: Add tests for process_ordrsp() cancellation handling
Tests verify that EDI ORDRSP cancellations now use
Koha::Acquisition::Order->cancel() rather than a raw ModOrder call:

- Order status, cancellation reason and date are set correctly
- On-order items linked via aqorders_items are deleted
- Purchase suggestions linked to the biblio are reverted from
  ORDERED back to ACCEPTED
- Non-cancellation actions update suppliers_report only
- Missing ordernumbers are handled gracefully without dying

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Sharon Dugdale <sharon.dugdale@cumberland.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:27 +00:00
Martin Renvoize 70f5d1d3f1
Bug 42789: Add missing POD for _validate_location_code and _validate_collection_code
These private subs were added without pod_coverage documentation,
causing a QA failure. Add minimal =head3 blocks to resolve it.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Sharon Dugdale <sharon.dugdale@cumberland.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:26 +00:00
Martin Renvoize 6c55a4e7d4
Bug 42789: EDI ORDRSP cancellation should use Koha::Acquisition::Order->cancel()
The process_ordrsp() function was calling C4::Acquisition::ModOrder()
directly when processing a supplier cancellation, which only updated
the order status fields and left associated items and purchase
suggestions untouched.

Replace with Koha::Acquisition::Order->cancel(), which mirrors the
behaviour of manual cancellation via the acquisitions UI:
- Deletes on-order items via safe_delete() with error capture
- Reverts any linked purchase suggestion from ORDERED back to ACCEPTED
- Updates orderstatus, cancellationreason and datecancellationprinted

The delete_biblio option is intentionally omitted (defaults to false)
as silent biblio deletion is not appropriate for an automated EDI flow.

ModOrder is retained for the non-cancellation branch which only needs
to update suppliers_report.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Sharon Dugdale <sharon.dugdale@cumberland.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:25 +00:00
Kyle M Hall 1031a1ba73
Bug 42700: Remove dead $flags->{WAITING} code from C4::Members::patronflags
After the previous patch, $flags->{WAITING} from
C4::Members::patronflags is no longer read anywhere in Koha.

Test Plan:
1) Apply this patch
2) prove t/db_dependent/SIP/ t/db_dependent/Members.t
3) Note all tests pass!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Aleisha Amohia <aleisha@catalyst.net.nz>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:24 +00:00
Kyle M Hall 69d9cc4e59
Bug 42700: Avoid duplicate holds query query when building SIP patron object
This patch fetches the patron's holds once into a list of
Koha::Hold objects and derives both hold_items and unavail_holds
from that single fetch.

Test Plan:
1) Apply this patch
2) prove t/db_dependent/SIP/
3) Note all tests pass!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Aleisha Amohia <aleisha@catalyst.net.nz>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:23 +00:00
Kyle M Hall eb57856091
Bug 42700: hold_items in SIP patron info should contain all holds, not just waiting
Some 3rd party SIP clients calculate the number of holds ready
for pickup as:

  holds ready for pickup = hold_items - unavail_holds

Right now this can produce a negative number since hold_items
contains only waiting holds.

This patch makes hold_items contain all of the patron's holds,
so hold_items - unavail_holds correctly gives the count of holds
ready to be picked up.

Test Plan:
1) Apply the first patch only
2) prove t/db_dependent/SIP/Patron.t
3) Note the new "hold_items counts all holds" subtest fails
4) Apply this patch
5) prove t/db_dependent/SIP/Patron.t
6) Note the tests pass!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Aleisha Amohia <aleisha@catalyst.net.nz>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:22 +00:00
Kyle M Hall d8cf6da3a2
Bug 42700: Add unit tests
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Aleisha Amohia <aleisha@catalyst.net.nz>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:21 +00:00
Pedro Amorim b33dae39ad
Bug 41649: DBIC
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-18 11:21:17 +00:00
Matt Blenkinsop 72b8e1d9ea
Bug 42018: Prevent null values from breaking patron duplicate matching
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:37 +00:00
Matt Blenkinsop dbd680caec
Bug 42018: Add unit test
Test plan:
1) Apply just the unit test patch
2) Run t/db_dependent/Koha/Patrons.t
3) Test should fail
4) Apply remaining patch
5) Repeat step 3
6) Test should pass

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:36 +00:00
Aleisha Amohia 37ef59a9c8
Bug 35729: Normalize ISBNs as ISBN-13 if cannot fallback to ISBN-10
ISBNs beginning with 979 do not have an ISBN-10 version, and need to be normalized to an ISBN-13 format. All others, if valid, can safely be normalized to ISBN-10 format (current behaviour).

To set up

1. Download the attached sample records with 979 ISBNs
2. Log into the staff interface and go to Cataloguing
3. Go to Stage MARC records for import and upload the sample MARC file
4. Stage and import the biblio records
5. Once imported, open the detail pages for some of the records
6. In another tab, do a catalogue search and open the detail pages for ISBN-10 records, for example biblionumbers 6 and 54

To test

1. Apply the patch and restart services
2. Log into the staff interface and go to Koha Administration, Global system preferences, Enhanced Content tab
3. Cycle through each cover image service, enabled by the system preferences
4. With each cover image service is enabled, refresh the record detail pages you have open. The ISBN-10 records should CONTINUE to show their cover images. The newly imported ISBN-13 records should NOW show their cover images.
5. Confirm tests pass: t/db_dependent/Koha.t

testing with ISBN 9798885795692 should return this image from Amazon: https://images-na.ssl-images-amazon.com/images/P/9798885795692.01.LZZZZZZZ.jpg
testing with ISBN 9798876536914 should return this image from Amazon: https://images-na.ssl-images-amazon.com/images/P/9798876536914.01.LZZZZZZZ.jpg

Sponsored-by: Horowhenua Libraries Trust
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:35 +00:00
Andrew Fuerste Henry d3d4a6ca96
Bug 40799: (QA Follow-up) tidy template file
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:34 +00:00
Hammat Wele be7436b429
Bug 40799: Loading spinner not removed because .cover-slider is now used instead of .cover-slides
After Bug 32642, the spinner on cover images is hidden by verify_cover_images() which removes the .cover-slides class from the cover container.

However, Bug 33848 modified the SCSS so that the spinner is now triggered by .cover-slider instead of .cover-slides.

As a result, the spinner is never removed, because verify_cover_images() only removes .cover-slides.

Test plan

1- Follow the test plan from Bug 32642.
   ==> The spinner is displayed and never disappears.
2- Apply the patch
3- Reload the page.
   ==> The spinner disappears once the cover image is loaded.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:33 +00:00
Owen Leonard 813fb3d143
Bug 43260: (follow-up) Move debug menu JS into js_includes.inc
The _() function isn't available to JS in intranet-bottom.inc.

To test, apply the patch and test that the correct strings are
translatable. In this example I'm testing fr-FR:

- Update a translation:

  > gulp po:update
  > cd misc/translator
  > perl translate update fr-FR

- Open the corresponding .po file for the strings pulled from
  the template e.g.  misc/translator/po/fr-FR-staff-prog.po
- Locate strings pulled from includes/js_includes.inc for
  translation, e.g.:

  #: koha-tmpl/intranet-tmpl/prog/en/includes/js_includes.inc:105
  #, fuzzy
  msgid "Disable custom CSS"
  msgstr "Désactiver "

- Remove the "fuzzy" line.
- Edit the "msgstr" string however you want (it's just for testing)
- Install the updated translation:

  > perl translate install fr-FR

Log in to the staff interface as a user with debug permission. Test the
debug menu in the header, confirming that the strings look correct and
that the toggles work correctly.

Switch to your translated templates and test again.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:32 +00:00
Owen Leonard b567a5589f
Bug 43260: Debug options in navigation bar untranslatable
These patches update the templates which control the display of the
debug menu item in the staff interface header. The strings need to be
wrapped in the _() i18n function.

The patch also corrects capitalization and uses the term "custom"
instead of "user."

A follow-up patch will move this JS into js_includes.inc so that the _()
is available. This patch is separate to make it easier to view the diff.

Testing requires the follow-up.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:31 +00:00
Baptiste Wojtkowski 3d41c25c08
Bug 43245: Redirect user if no email was provided
TO TEST:
0 - Apply patch
1 - Make sure OPACResetPassword is activated
2 - Go to that good old Henry Acevedo's page and reset their password
3 - Notice you have been redirected with an error message telling you
have no email

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:30 +00:00
Pedro Amorim 1a5be8e287
Bug 42112: DBRev 26.06.00.014
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:29 +00:00
Lucas Gass 74130f08df
Bug 42112: Add option when changing a patron's category: allow choosing what to do for the expiry date and messaging preferences
To test:
1. APPLY PATCH, updatedatabase, restart_all
2. Find 2 new system preferences UpdateDateExpiryOnCategoryChange and UpdateMessagingPrefsOnCategoryChange
3. Each was the same options:
    -Never update
    -Always ask, default to no
    -Always ask, default to yes
    -Always update without asking

4. Go to  Administration > Patron categories
5. Edit or create some categories with different Enrollment periods and messaaging preferences
6. Try each of the options for BOTH system preferences, making sure they work as described.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:28 +00:00
Jonathan Druart e1072702f8
Bug 41896: Remove C4::Context from Koha::Cache
Signed-off-by: Julian Maurice <julian.maurice@biblibre.com>

Amended-by: Jonathan Druart
Restore ||= op
-    $self->{namespace} = Koha::Config->get_instance->get('memcached_namespace') || 'koha';
+    $self->{namespace} ||= Koha::Config->get_instance->get('memcached_namespace') || 'koha';

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:27 +00:00
Andrew Fuerste Henry d154cda830
Bug 35797: (QA follow-up) Use https in API comment
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:26 +00:00
Shi Yao Wang b590f1a0a3
Bug 35797: Syspref, schema path file and tests changes
1- Corrected tests according to the comments
2- Removed unused import
3- Added syspref check of 'patronimages'
4- Tweaked swagger path schema

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:25 +00:00
Shi Yao Wang ad190dcc5d
Bug 35797: REST API Add GET route for patronimage and tests
Api changes adding GET route for patronimage

Test plan:
1- Apply the patch
2- Run `npm run api:bundle`
2- Go to administration -> sys.pref. -> set patronimages to allow
3- Log in koha using a user with "list_borrowers" or "edit_borrowers" privileges
4- Have a patron with a profile image (add an image if necessary by
searching for a patron and then clicking on the default profile image on
the top left)
5- Open a an API testing tool (such as postman), write
{intranet_url}/api/v1/patrons/{borrowernumber}/default_image
where {intranet_url} is the base intranet url of koha and {borrowernumber}
is the borrowernumber of a borrower that has an image
6- Add OAuth 2.0 credentials coming from koha (patron -> more -> manage
api keys) and generate a token
7- Send the request and notice the image of the patron is returned
8- Do step 5-7 with a borrowernumber of a patron without an image and a
borrowernumber of a patron that doesn't exist (and other edge cases
where there should be an error message displayed such as "Access
forbidden", "Authentication required", etc.)
9- Notice an appropriate error message is displayed

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: David Nind <david@davidnind.com>

Bug 35797: Rename test file from 'image.t' to 'patron_images.t'

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:24 +00:00
Martin Renvoize 3450f46d8b
Bug 40736: (QA follow-up) Avoid redundant CGISESSID cookie lookups
The cookie is already fetched once into $current_session_cookie
earlier in login(); reuse it instead of calling
$c->req->cookie('CGISESSID') again at each check site.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:23 +00:00
Lari Taskula b094346030
Bug 40736: Add test
To test:
1. prove t/db_dependent/api/v1/idp.t

Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:22 +00:00
Lari Taskula 1f86fb5616
Bug 40736: Restore commented out tests
Commented out by https://bugs.koha-community.org/bugzilla3/attachment.cgi?id=143438

(or 9c5f6e8c17 (diff-9e449131ec9eb973ff6a851c0fda9a426255591e3f579c79b047fb2763f33e80R275) )

To test:
1. prove t/db_dependent/api/v1/idp.t

Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:21 +00:00
David Cook 20bde64702
Bug 40736: Gracefully handle missing CGISESSID cookie
This change makes the REST API gracefully handle
a missing CGISESSID when trying to do an OAuth/OIDC login
by direct linking.

Test plan:
0. Apply the patch and koha-plack --restart kohadev
1. Set up an OpenID Connect client using the wiki
https://wiki.koha-community.org/wiki/Testing_SSO
2. In an incognito/private window, try directly logging in
with the following URL:
http://localhost:8080/api/v1/public/oauth/login/test/opac
3. Note that you're redirected to opac-user.pl with an auth error
message of "No user session found"
4. Close the incognito/private window
5. In an incognito/private window, try directly logging in
with the following URL:
http://localhost:8081/api/v1/oauth/login/test/staff
6. Note that you're redirected to mainpage.pl with an auth error
message of "No user session found"

Signed-off-by: Lari Taskula <lari.taskula@hypernova.fi>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:20 +00:00
Hammat Wele 6490728051
Bug 33758: Add a parameter to use the intranet_catalog_biblio_enhancements_toolbar_button Plugin hook
This patch adds a parameter to call the method intranet_catalog_biblio_enhancements_toolbar_button with a biblionumber in the page catalogue/detail.pl.

Modification of cat-toolbar.inc to display new buttons correctly.

To test :
1. Apply the patch
2. Take a plugin who implements the method intranet_catalog_biblio_enhancements_toolbar_button
3. Check if the parameter biblionumber is passed correctly at this method
4. Create the new button in HTML in the plugin method
(button must be like the ones that are in the toolbar of the page detail.pl)
5. Go to the page in browser and check if the new button is displayed

Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:19 +00:00
Martin Renvoize 1e0442b847
Bug 41105: (QA follow-up) DRY out gender label rendering
The gender-code-to-label mapping (F/M/O -> Female/Male/Other) was
duplicated between the results table cell and the edit-form select
options in modborrowers.tt. Extract it into a shared
patron-sex-label.inc so there is a single place defining the mapping,
and drop the now-unused "lib" values from the sex options in
tools/modborrowers.pl (labels are supplied by the include, keyed on
the option value, rather than by Perl).

Test plan:
1. Navigate to Patrons and click Search, to display a list of patrons.
2. Select all results and click "Batch patron modification".
3. Verify the "Gender" column displays Female/Male/Other correctly.
4. Verify the "Gender" edit dropdown still offers Female/Male/Other
   and saves correctly.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:18 +00:00
Martin Renvoize b51a3f111a
Bug 41105: (QA follow-up) Fix gender label translation
The batch patron modification "Gender" column and edit dropdown
hardcoded "Female"/"Male"/"Other" as data passed from
tools/modborrowers.pl, and the "Other" gender label was displayed as
plain text.

Values coming from Perl as template data are never picked up by the
PO extractor, so the gender select options in the edit form could
never be translated. Elsewhere in Koha (moremember.tt,
moremember-brief.tt, memberentrygen.tt, opac-memberentry.tt) the
"Other" gender label is wrapped with tp('gender', 'Other') to give
translators context, since "Other" is ambiguous out of context.

This makes the "Gender" dropdown options translatable directly from
the template, and aligns "Other" with the existing tp('gender', ...)
convention used everywhere else gender is displayed.

Test plan:
1. Navigate to Patrons and click Search, to display a list of patrons.
2. Select all results and click "Batch patron modification".
3. Verify the "Gender" column and "Gender" edit dropdown still work as
   expected.
4. Confirm (e.g. via the .pot file or misc/translator/translate) that
   "Female", "Male" and the "Other" (gender) msgctxt string are now
   extracted for translation from this template.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:17 +00:00
Olivia Reynolds 0bb26abd5a
Bug 41105: Add gender to batch patron modification
This patch adds the ability to batch-edit the gender field for patrons.

Test plan:
1.  Ensure there are a number of patrons in the database.
2.  Apply patch.
3.  Navigate to Patrons and click Search, to display a list of patrons.
4.  Select all results and click "Batch patron modification".
5.  Verify the "Gender" column is displayed.
6.  Verify the "Gender" column may be toggled under the "Columns"
    dropdown.
7.  Select a subset of the patrons for modification.
8.  Verify there is a "Gender" option beneath the table.
9.  Select "Female" as the gender, and click "Save".
10. Verify the gender of all selected patrons was set to "Female".
11. Repeat with "Male" and "Other" as the gender.
12. Repeat with clearing the gender field by ticking the checkbox next
    to the dropdown and saving. Verify the gender field is blank for
    each selected patron.

Sponsored-by: Open Fifth
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Amit Gupta <amit.gupta@informaticsglobal.ai>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-14 12:09:15 +00:00
Pedro Amorim 662020e21b
Bug 42714: RM follow-up: Fix tests
prove t/00-load.t
prove xt/author/pod_coverage.t

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-13 11:28:40 +00:00
Owen Leonard 55bc6f01e0
Bug 41842: (follow-up) Ensure that tidy doesn't trim wanted whitespace
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 16:08:01 +00:00
Owen Leonard 1b00485b27
Bug 41842: (follow-up) Make sure this space doesn't get tidied
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 16:07:59 +00:00
Martin Renvoize 3cd648d52f
Bug 41842: Use space-dash-space separator between course name and section
The staff interface templates were using a bare hyphen with no spaces
when displaying course name and section together, while the OPAC templates
(Bug 42654) use ' - ' (space-dash-space). This brings the staff interface
into alignment with the OPAC convention for consistency and readability.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 16:07:58 +00:00
Owen Leonard 303ba57409
Bug 41842: Improve translation of course reserves headings
This patch adds some use of I18N functions to course reserves templates
in order to improve the process of translation by adding context and
placeholders. The patch also rewords some headings for clarity and
consistency.

To test, apply the patch and go to course reserves.

- Add a course if necessary.
- Click the course to view it.
- On the course details page, confirm that the page title, breadcrumbs,
  and heading look correct.
- Click "Edit" in the toolbar.
- Check the page title, breadcrumbs, and heading again.
- Return to the course details and click "Add reserves" and check that
  page.
- Submit a barcode or biblionumber and check that page.
- Test the "Batch add reserves" page.

To test the improved I18N, apply the patch and test that the correct
strings are translatable. In this example I'm testing fr-FR:

- Update a translation:

    > gulp po:update
    > cd misc/translator
    > perl translate update fr-FR

- Open the corresponding .po file for the modified strings, e.g.
  fr-FR-messages.po.
- Locate strings pulled from course reserves templates for
  translation, e.g.:

    #: koha-tmpl/intranet-tmpl/prog/en/modules/course_reserves/course.tt:10
    #: koha-tmpl/intranet-tmpl/prog/en/modules/course_reserves/course.tt:42
    #: koha-tmpl/intranet-tmpl/prog/en/modules/course_reserves/course.tt:56
    #, fuzzy, perl-brace-format
    msgid "Edit course {course_name}"
    msgstr "Modifier {course_name}"

- Remove the "fuzzy,"
- Edit the "msgstr" string however you want (it's just for testing)
- Install the updated translation:

  > perl translate install fr-FR

Switch to the translation you updated. Walk through the process of
editing a course and adding course reserves to confirm that your translated strings show up.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 16:07:55 +00:00
Pedro Amorim 4960d6987d
Bug 42973: (RM follow-up) Update tests number
Because of bug 42866
prove t/db_dependent/Koha/AdditionalContents.t

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:49:52 +00:00
Laura_Escamilla 89f6fcc4cd
Bug 42973: Unit test
Added a unit test to confirm that search_for_display orders manually positioned news first, followed by unnumbered news by publication date descending.

Test passes:

prove t/db_dependent/Koha/AdditionalContents.t

Signed-off-by: Gretchen Maxeiner <MaxeinerGL@gcc.edu>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:49:51 +00:00
Laura_Escamilla 70caff963a
Bug 42973: Restore manual ordering for news
To test:
1. Create four OPAC news items:

   - "Pinned news item" with "Appear in position" = 1 and an older publication date.

   - "Newest unnumbered news item" with no position and the newest publication date.

   - "Middle unnumbered news item" with no position and an intermediate publication date.

   - "Oldest unnumbered news item" with no position and the oldest publication date.

2. View the OPAC main page.

3. Confirm the news items are ordered by publication date, with the newest item first.

Apply the patch.

4. Reload the OPAC main page.

5. Confirm the manually positioned news item appears first.

6. Confirm the remaining news items are ordered by publication date descending.

7. Confirm news items without an "Appear in position" value continue to display newest first.

8. Sign off and have a wonderful day!

Signed-off-by: Gretchen Maxeiner <MaxeinerGL@gcc.edu>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:49:51 +00:00
Katrin Fischer bb53e653f3
Bug 43239: (QA follow-up) Rephrase hint on subscription form
This is to clarify that the item's current item type will
not be changed, no matter what settings have been used in
the subscription.

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:49:50 +00:00
David Nind 5005019b5d
Bug 43239: (follow-up) Update wording for hint
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:49:50 +00:00
Emmi Takkinen 02515ebcc7
Bug 43239: Preserve item item type if previousitemtype column is undefined
It is possible to create serial item with wrong itemtype if syspreference
makePreviousSerialAvailable is enabled and field "item type for older issues"
isn't filled when creating a subscription for biblio record. This happens
when multiple items are created. In installations where itemtypes differ
between items and biblios, this causes data inconsistencies and leads to
errors in biblios Holdings table.

This patch changes behaviour of serials-edit.pl so that if
makePreviousSerialAvailable is enabled and subscriptions previousitemtype
column is NULL, previous serial items itype column is preserved. It also
adds a hint text to subscription form about this change.

To test:
1. Enable systempreference makePreviousSerialAvailable.
2. Create new biblio record.
3. Create a new subscription for biblio record you made, select option
"Create an item record", select different item type from "Item type"
selection than biblio record has but leave selection "item type for older
issues" empty.
4. Receive a serial item for subscription. Check from biblio records
Holdings table that its item type matches item type selected in step 3.
5. Now receive 3 serial items. After creating them check Holdings table again.
=> Note that first item created now has same item type as biblio record.
6. Apply this patch, restart services.
7. Create new subscription, fill needed fields as in step 3.
=> Note that there is a new hint text after field "item type for older issues".
8. Repeat steps 4. and 5.
=> Note that now all items have same item type.

Sponsored-by: Koha-Suomi Oy
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:49:49 +00:00
Pedro Amorim a0ac15d8b3
Bug 35104: DBRev 26.06.00.013
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:49:49 +00:00
Martin Renvoize 1b4373eea3
Bug 35104: Update Exporter/Record.t fixture for the new store() validation
This test built a "bad" biblio by storing invalid MARCXML directly via
Koha::Biblio::Metadata->store(), which now rejects it. Simulate
legacy corrupt data already in the database by writing it straight to
the DBIC row, bypassing the new validation, instead.

Signed-off-by: Baptiste Bayche <baptiste.bayche@inlibro.com>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Sponsored-by: OpenFifth
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:45:06 +00:00
Martin Renvoize 6726b1a254
Bug 35104: Catch invalid metadata and surface repair warnings in AddBiblio/ModBiblio
AddBiblio/ModBiblio/ModBiblioMarc gain an optional warnings arrayref
option: any repair messages recorded by Koha::Biblio::Metadata->store()
(see previous commit) are appended to it via object_messages, so a
caller can show the cataloguer what was automatically fixed instead of
the fix happening invisibly.

AddBiblio also now distinguishes Koha::Exceptions::Metadata::Invalid
from other errors in its catch block and rethrows it, so a caller can
tell "this record is unrecoverable" apart from an unexpected internal
error and react accordingly (see following bugs for callers doing so).

The inline StripNonXmlChars calls in TransformHtmlToMarc are removed,
since stripping is now handled centrally and consistently by
Koha::Biblio::Metadata->store() rather than ad-hoc at every call site.

Signed-off-by: Baptiste Bayche <baptiste.bayche@inlibro.com>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Sponsored-by: OpenFifth
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:45:05 +00:00
Martin Renvoize 112712e31c
Bug 35104: Recover MARCXML that fails to decode, or throw when unrecoverable
Koha::Biblio::Metadata->store() now validates that MARCXML round-trips
through MARC::Record before saving it, rather than saving whatever was
given and letting corruption surface later as an opaque failure
somewhere downstream.

Two faults are recovered automatically, and either or both may apply
to the same record:

- non-XML characters (recorded as nonxml_stripped, recovered via
  StripNonXmlChars)
- datafields with no subfields at all, which MARC::File::XML refuses
  to parse (recorded as empty_datafield_stripped, recovered by
  removing the empty datafield)

Each recovered fault is logged, reported via object_messages for the
caller to act on, and recorded as a biblio_metadata_errors row (tag
and subfield included where identifiable) for later review - except
where an identical row already exists for the same metadata_id, so a
source record that stays dirty across repeated saves (e.g. re-harvested
from an external repository) doesn't pile up duplicate rows for the
same fault. Existing rows are otherwise left alone on a clean save,
since they're review flags requiring explicit resolution, not a cache
to invalidate.

If the MARCXML cannot be recovered at all, Koha::Exceptions::Metadata::Invalid
is thrown. The recovery logic is exposed as a repair_marcxml class
method so other callers that parse MARCXML ahead of a store() (see
following bugs) can reuse it instead of re-implementing it.

Signed-off-by: Baptiste Bayche <baptiste.bayche@inlibro.com>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Sponsored-by: OpenFifth
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:45:04 +00:00
Martin Renvoize b80a26eb51
Bug 35104: Add Koha::Biblio::Metadata::Error(s) object classes
Thin Koha::Object(s) wrappers around biblio_metadata_errors. error_type
is restricted to a known set of constants, validated on store(), so a
typo'd or unknown value fails loudly rather than writing an
unrecognised value into the database. Errors::by_type() gives callers
a named filter instead of hand-rolling the same search().

Signed-off-by: Baptiste Bayche <baptiste.bayche@inlibro.com>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Sponsored-by: OpenFifth
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:45:02 +00:00
Martin Renvoize 50b0ba401b
Bug 35104: Guard Koha::Exceptions::Metadata::Invalid::full_message against undef fields
full_message() interpolated id/biblionumber/format/schema/decoding_error
directly into a sprintf without checking definedness, producing Perl
warnings whenever one of them is unset (e.g. a new, not-yet-stored
metadata object with no id yet).

Signed-off-by: Baptiste Bayche <baptiste.bayche@inlibro.com>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Sponsored-by: OpenFifth
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:45:01 +00:00
Martin Renvoize 0490204f7b
Bug 35104: Add koha_object_class to BiblioMetadataError schema result
Signed-off-by: Baptiste Bayche <baptiste.bayche@inlibro.com>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Sponsored-by: OpenFifth
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:45:00 +00:00
Martin Renvoize 7d9562c1b6
Bug 35104: Automated Schema Update
Signed-off-by: Baptiste Bayche <baptiste.bayche@inlibro.com>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Sponsored-by: OpenFifth
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:44:59 +00:00
Martin Renvoize 22b4acf8d2
Bug 35104: Add biblio_metadata_errors table for per-fault error tracking
Adds biblio_metadata_errors to record individual faults (non-XML
characters, empty datafields, ...) found and recovered when saving a
bibliographic record's MARCXML, so they can be surfaced to
cataloguers and explicitly resolved once reviewed.

Signed-off-by: Baptiste Bayche <baptiste.bayche@inlibro.com>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Sponsored-by: OpenFifth
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-12 15:44:58 +00:00
Laura_Escamilla 1ffbc32ad2
Bug 43019: Prevent access to pages belonging to another library (staff side)
To test:
1. Create two patron accounts:
    * One patron whose home library matches the library assigned to the page.
    * One patron whose home library is different from the library assigned to the page.
2. Create a new page with the following settings:
    * Display location = OPAC
    * Library = a specific library
    * Publication date = Today
    * Title and Content = minimally necessary content
3. Save the page.
4. Right-click the OPAC link and copy the link address.
5. Open the link in an incognito or private browser window without signing in.
    * Confirm that the library-specific page is not displayed.
    * Confirm that a 404 page is shown.
6. In the incognito window, sign in using the patron account whose home library matches the page library.
7. Open the copied OPAC link again.
    * Confirm that the page is displayed.
8. Sign out and sign in using the patron account whose home library does not match the page library.
9. Open the copied OPAC link again.
    * Confirm that the page is not displayed.
    * Confirm that a 404 page is shown.
10. Edit the page and change Library to All libraries.
11. Open the OPAC link in an incognito or private browser window without signing in.
    * Confirm that the page is displayed.
12. While signed in as each patron account, open the OPAC link again.
    * Confirm that the page is displayed for patrons from both libraries.
13. Create or edit a page with the following settings:
    * Display location = Staff interface
    * Library = a specific library
    * Publication date = Today
    * Title and Content = minimally necessary content
14. Sign in to the staff interface using a staff account whose library matches the page library.
15. Open the page from the staff interface.
    * Confirm that the page is displayed.
16. Sign out and sign in using a staff account whose library does not match the page library.
17. Open the staff page link directly.
    * Confirm that the page is not displayed.
    * Confirm that a 404 page is shown.
18. Edit the staff page and change Library to All libraries.
19. Open the page while signed in with staff accounts from both libraries.
    * Confirm that the page is displayed for both accounts.
20. Repeat the OPAC and staff interface checks using Display location = Staff interface and OPAC.
    * Confirm that the page is displayed in both interfaces for users from the matching library.
    * Confirm that users from a different library receive a 404 page.
    * Confirm that pages assigned to All libraries are available in both interfaces.

Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:48 +00:00
David Cook e50267072f
Bug 43019: Fix branch comparison
This change fixes the branch comparison between the Page
and the user. Previously, it was doing a numeric "!=" and now it
does the string comparison "ne". Also, if no environmental variable
is set for the default branch, it defaults to an empty string
for anonymous (ie non-authenticated) users.

Test plan:
0. Apply the patch
1. restart_all
2. Log into the staff interface
3. Go to Tools
4. Click on "Pages"
5. Create a Page for All Libraries for the OPAC
6. Copy the "OPAC" link in the "Page URL" table cell and open it
in a private window or a separate browser
7. Note that the page appears
8. Now change the "Page" to a specific library
9. Revisit the URL in the private window
10. Note that it now gives a 404 error
11. Log into the OPAC with a user whose library matches the libary
of the Page
12. Note that now you can view the URL again
13. Change the library of the user or the Page
14. Note that you cannot see the Page now

Signed-off-by: Phil Ringnalda <phil@chetcolibrary.org>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:47 +00:00
Jonathan Druart a45638d8fc
Bug 42866: Prevent SQL injection in Koha::AddtionalContents->search_for_display
Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:46 +00:00
Martin Renvoize f3f6f15938
Bug 42847: (QA follow-up) Clear session state on all login_core exit paths
login_core only cleared leftover session state on the failed-auth
branch. The earlier 'SIP access not authorized' return (unknown
terminal id) left any account/ils/institution/policy/credentials from
a prior connection on a reused prefork worker in place.

Move the reset to the top of login_core so every non-success exit path
leaves clean state, and only a successful api_auth re-establishes the
session. This keeps the no-authenticated-state-until-auth invariant in
a single place rather than duplicating the key list per branch.

Also extend the unit test to cover the unknown-terminal path on a
reused worker.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:45 +00:00
Kyle M Hall 50a4389813
Bug 42847: Prevent previously authenticated user
After a SIP terminal logs in successfully, a later connection handled by the
same worker could bypass authentication entirely. login_core() stores the account
in the session before calling api_auth(), and left it in place when authentication
failed. The check in raw_transport() that is meant to drop unauthenticated connections
only looks at $self->{account}->{id}, which is now populated straight from the SIP config,
so the connection is treated as logged in.

Test Plan:
1) Create a SIP user with the username and password "term1"
2) Restart the SIP server
3) Apply the first patch
4) prove t/db_dependent/SIP/MsgType.t
5) Note the new test fails
6) Telnet to the SIP server port ( 6001 for ktd )
5) Send a successful login: 9300CNterm1|COterm1
6) Note you get a 941 back
7) Disconnect, then telnet to localhost 6001 again
8) Send a bad login: 9300CNterm1|COterm2
9) Note you get a 940 back, but the connection stays open
10) Send a patron information request, using a real patron cardnumber:
    6300120260615    110310          AOCPL|AA23529001000463
11) Note you get a 64 patron information response with the patron's data back,
    even though the login failed!
12) Apply the second patch
13) prove t/db_dependent/SIP/MsgType.t
14) Note the subtest now passes!
15) Restart all the things!
16) Repeat steps 4 to 10
17) Note the the patron info request returns no patron data and the connection is dropped!

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:43 +00:00
Kyle M Hall d56dc2bfa8
Bug 42847: Add unit tests
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:42 +00:00
Jonathan Druart 887b163652
Bug 42749: Prevent SQL injection from GetInvoices (order_by)
Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:41 +00:00
Martin Renvoize 5aa86845f5
Bug 42747: (QA follow-up) Parameterize query, dedupe, and log rejections
The original fix only whitelisted image_name at the point it's saved
in edit-layout.pl, leaving the actual injection sink in create-pdf.pl
unparameterized. This adds defense-in-depth by fixing the query itself,
avoids a redundant duplicate call to get_all_image_names() (already
fetched earlier in the script as $image_names), and logs rejected
image_name values instead of silently discarding them so tampering
attempts leave an audit trail.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:40 +00:00
Jonathan Druart bbbaa83c1c
Bug 42747: Prevent SQL injection via image_name in edit-layout
Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:39 +00:00
Martin Renvoize b0c86b0fcf
Bug 42746: (QA follow-up) Sanitize agefield and substitution field at the sink too
The original fix only whitelisted agefield, condition_field, and
substitution_field at the point they're saved in
tools/automatic_item_modification_by_age.pl, leaving the actual
consumer of the stored syspref, C4::Items::ToggleNewStatus, trusting
agefield and substitution field unchecked. agefield is spliced
directly into raw SQL (TO_DAYS($agefield)) and so can't be
parameterized like an ordinary value; substitution field reaches a
dynamic method call ($item->$field($value)). Any future or
alternate writer of the automatic_item_modification_by_age_configuration
syspref would have reopened both holes without touching the form at
all.

This adds the same allow-list validation directly in ToggleNewStatus
(falling back to the safe default agefield, or skipping the
substitution/condition, exactly as before for missing values), shares
the age-field allow-list between the two files via a new
GetAutomaticItemModificationAgeFields() in C4::Items so it can't drift
between them, and logs rejected values via Koha::Logger instead of
silently discarding them so tampering leaves an audit trail.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:38 +00:00
Jonathan Druart 52f4b17b0f
Bug 42746: Sanitize condition and substitution fields
Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>

Amended-by: Jonathan Druart:
Replace , with ;

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:37 +00:00
Jonathan Druart 2b09e846bc
Bug 42746: Sanitize age_field
Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:36 +00:00
Martin Renvoize 8145e26a93
Bug 42471: (follow-up) prevent XSS in suggestion status
The custom/authorized-value status branch concatenated row.status
directly into a class="..." attribute string. escape_str() only
escapes &, <, > (not quotes), so a status code containing a double
quote could still break out of the attribute.

Build the status <span> via DOM APIs instead, the same way the
reason field was fixed: fixed statuses get a hardcoded class, and
the free-form status code goes into a data-status attribute via
dataset, which the DOM escapes safely regardless of content.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:35 +00:00
David Cook 222dac6c3c
Bug 42471: (follow-up) prevent XSS in suggestion reason
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:34 +00:00
Jonathan Druart acf1184436
Bug 42471: Prevent XSS in suggestion.tt
escape_str all the things

Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:33 +00:00
Martin Renvoize da595ca8d5
Bug 30233: (follow-up) Fix POD formatting codes in _apply_substitution
The angle-bracket delimiter notation in the POD prose (e.g.
PATTERN<DELIM>) caused the POD parser to treat the trailing letter of
each word as an unknown formatting code (N<>, T<>). Replace with
C<s/PATTERN/REPLACEMENT/FLAGS> and a parenthetical note that any
non-space delimiter is accepted.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:32 +00:00
Martin Renvoize 8a06056143
Bug 30233: (follow-up) Add tests for _apply_substitution and combined modifiers
- t/C4/ClassSplitRoutine/RegEx.t (new): exercises _apply_substitution via
  the public split_callnumber interface — basic substitution, alternate
  delimiters, \n/$n backrefs, /gi combined flag, /e rejection, and malformed
  or uncompilable rules (including the new logger warn path).
- t/Koha/Regex/Replacement.t: extend the subst() helper to handle the ig/gi
  combined modifier and add a subtest that confirms case-insensitive global
  replacement and backref case preservation.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:30 +00:00
Martin Renvoize 45852aa193
Bug 30233: (follow-up) Log a warning when _apply_substitution discards a bad pattern
When eval { qr/$pattern/ } fails (e.g. the admin-configured s/// contains
an invalid regex), _apply_substitution previously returned silently with no
indication of why the rule was skipped. Add a Koha::Logger warn so the
failure appears in the log and is diagnosable without a debugger.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:29 +00:00
David Cook b9e7973649
Bug 30233: (follow-up) fix some errors reported by QA tools
Note: While the methodology in Koha::Regex::Replacement may seem
bespoke, it's similar to how Python's re.sub(pattern, repl, string)
regex substitution works.

Unit tests:
prove -v t/Koha/Regex/Replacement.t \
    t/SimpleMARC.t \
    t/db_dependent/MarcModificationTemplates.t \
    t/Labels_split_Regex.t

Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:28 +00:00
Sanjar Tulkinov Anvar o'g'li 1043dfeda7
Bug 30233: Stop running call number split regexes as code
C4::ClassSplitRoutine::RegEx::split_callnumber ran the admin-configured rule
through eval "\$cn_item =~ $regex", executing arbitrary Perl. Parse the
rule as s<DELIM>PATTERN<DELIM>REPLACEMENT<DELIM>FLAGS, reject the /e flag,
compile the pattern with qr// and expand the replacement as data via
Koha::Regex::Replacement::expand_template, so the rule is applied without
eval.

Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:27 +00:00
Sanjar Tulkinov Anvar o'g'li d7c1e19b49
Bug 30233: Stop running MARC modification replacements as code
Koha::SimpleMARC::_modify_values and Koha::Items::apply_regex wrapped the
user replacement in a string and ran it through s///ee, so an interpolation
block such as \@{[ system(...) ]} executed arbitrary Perl. Expand the
replacement as data via Koha::Regex::Replacement::expand_template under a
single /e instead, which keeps capture-group replacements working but never
runs the input as code.

Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:26 +00:00
Sanjar Tulkinov Anvar o'g'li cda91ee42c
Bug 30233: Add Koha::Regex::Replacement to expand replacements as data
Add a small single-pass helper, Koha::Regex::Replacement::expand_template,
that expands a user-supplied regex replacement string as data: numbered
backreferences ($1..$n, ${n}, the legacy \1..\9 form), named captures
($+{name}, ${name}) and the \n \r \t escapes. Every other character is
copied through literally, so \@{...}/${...} interpolation blocks end up as
text rather than being run. This is used to replace the s///ee double-eval.

The routine is fully qualified at the call sites rather than exported, and
case folding is left out to keep it small.

Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-11 12:39:23 +00:00
Owen Leonard 3b5f16adc6
Bug 40498: Improve style of list transfer form
This patch makes some style changes to the list transfer form. Since it
needs the same CSS that is used on the circulation page, the CSS block
is moved from circulation.tt to staff-global.scss.

The patch also changes the behavior of the form so that the "New owner"
line doesn't appear until after a patron has been selected.

To test, apply the patch and rebuild the staff interface CSS.

- If necessary, create a public list.
- From the list of public lists, click the "Transfer" button.
- Verify that the form looks correct. The patron search field should
  match the search field shown when you navigate directly to
  circulation.pl
- Search for a patron who has permission to manage public lists.
  - After you select a patron, a new line should appear:
    "New owner: [name]"
- Click "Transfer" and confirm that the process works correctly.

Sponsored-by: Athens County Public Libraries
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:36 +00:00
Jennifer Sutton 4110bcf3bf
Bug 30759: Add hint about the data that is sent via the Google Books API to OPACSuggestionAutoFill
Add information about the data that is sent to Google if the
OPACSuggestionAutoFill system preference is enabled.

Test plan:
1. Go to Administration > System preferences.
2. Search for the OPACSuggestionAutoFill system preference.
3. Note that there is no note about the data that is sent to
   Google if the system prference is enabled.
4. Apply the patch.
5. Refresh the page.
6. There is now a note about the data that is sent to Google.
7. Check that it reads well and makes ense.

Sponsored-by: Catalyst IT
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:36 +00:00
Martin Renvoize 9e90ca26f7
Bug 40800: SIP checkin incorrectly triggering alerts on successful transfer completion
When an item is checked in via SIP2 at the correct target branch after a
transfer, the system was incorrectly triggering an alert flag. This occurred
because the TransferArrived message was not being filtered out for accounts
with checked_in_ok="1", causing the alert logic to treat successful transfer
completion as a failure condition.

The TransferArrived message indicates successful transfer completion and is
purely informational - it should not trigger SIP alerts. This fix ensures
that TransferArrived messages are filtered out before alert evaluation,
preventing false positive alerts on successful operations.

Test Plan:
1. Set up a SIP2 account with checked_in_ok="1" in SIPconfig.xml
2. Via staff client, check out an item to a patron
3. Return the item to a different branch to trigger a transfer
4. Use misc/sip_cli_emulator.pl to check in the item at the target branch
5. Verify SIP response shows alert flag 'N' (no alert) instead of 'Y'
6. Test wrong branch checkin still shows alert flag 'Y' with CV04 type

Before: Transfer completion at correct branch incorrectly shows alert='Y'
After: Transfer completion at correct branch correctly shows alert='N'

Sponsored-by: OpenFifth <https://openfifth.co.uk>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:35 +00:00
Pedro Amorim c6e52fe82b
Bug 41649: DBRev 26.06.00.012
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:35 +00:00
Martin Renvoize 4475df36bf
Bug 41649: DBIC Schema boolean update
Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Marcus Michel <marcus.michel@rbkc.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:34 +00:00
Martin Renvoize c6798323cd
Bug 41649: Fix SIP Message tests for magnetic media
The tests for desensitize and alert flags were failing because
items were created with random itemtypes that could have
sip_magnetic=1. This caused unexpected behavior since magnetic
media items don't get desensitized and trigger alert flags.

Fix by explicitly creating itemtypes with sip_magnetic=0 in:
- test_checkout_desensitize
- test_renew_desensitize
- test_checkin_v2

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Marcus Michel <marcus.michel@rbkc.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:34 +00:00
Martin Renvoize cd728e5bb2
Bug 41649: Unit tests for magnetic media
Add unit tests for SIP magnetic media support.

Test plan
1) Run t/db_dependent/SIP/Transaction.t in ktd and confirm it passes

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Marcus Michel <marcus.michel@rbkc.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:33 +00:00
Martin Renvoize df7e600587
Bug 41649: Add option to set magnetic flag for item itemtypes
This patch adds the ability to set an item type as magentic for SIP
response purposes.

Test plan:
1) Run included database updates
2) Navigate to admin > itemtypes
3) Edit/Add an itemtype and scroll to the 'Sip media types' input
4) A new 'Sip magnetic' checkbox should be available
5) Confirm setting/unsetting it works as expected
6) The SIP magnetic flag bit should respect the setting
7) To test SIP functionality:
   a) Start the SIP server (e.g., koha-sip service)
   b) Create an item with an itemtype that has sip_magnetic checked
   c) Use misc/sip_cli_emulator.pl to send a checkout request:
      perl misc/sip_cli_emulator.pl -a localhost -p 6001 -su <sip_username> -sp <sip_password> -l <location_code> --patron <patron_cardnumber> --password <patron_password> --item <item_barcode> -m checkout
   d) Check the SIP response to ensure it handles magnetic media correctly (e.g., no desensitization flag)

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Marcus Michel <marcus.michel@rbkc.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:33 +00:00
Martin Renvoize 06bf94d455
Bug 41649: DBIC Build
Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Marcus Michel <marcus.michel@rbkc.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:32 +00:00
Martin Renvoize 5968bf0468
Bug 41649: Add sip_magnetic field to itemtypes table
This patch adds the new sip_magnetic boolean flag field to the itemtypes
table to allow flagging the itemtype as magnetic for sip responses.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Marcus Michel <marcus.michel@rbkc.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:32 +00:00
Colin Campbell 303deda8d3
Bug 41649: Set magnetic_media in SIP responses
The `magnetic_media` flag was never set, it can be set from
`sip_media_type` if it's present. `sip_media_type` is set via the
Authorized Values system.

Checkins will not be sorted correctly by Biblioteca without this flag.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Marcus Michel <marcus.michel@rbkc.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:31 +00:00
Jonathan Druart 03dd4cf862
Bug 42529: Prevent the test to fail in the future
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:31 +00:00
Tomás Cohen Arazi e89478d560
Bug 42529: Remove GetAgeRestriction from C4::Circulation
GetAgeRestriction is no longer used by any caller. This patch removes
it from the exports list and deletes the old test file.

Test plan:
1. Run tests without this patch:
   $ ktd --shell
   k$ prove t/db_dependent/Circulation.t \
            t/db_dependent/Reserves.t \
            t/Circulation/AgeRestrictionMarkers.t
=> SUCCESS: Tests pass!
2. Apply this patch
3. Run:
   k$ prove t/db_dependent/Circulation.t \
            t/db_dependent/Reserves.t \
            t/db_dependent/Koha/Policy/Biblio/AgeRestriction.t
=> SUCCESS: Tests pass!
4. Sign off :-D

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>

Amended-by: Jonathan Druart
Removed this from the commit message:
"The function body is kept for now as it is not harmful and could be
used by third-party code. It is no longer exported."

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:30 +00:00
Tomás Cohen Arazi b9868ce2d2
Bug 42529: Use Koha::Policy::Biblio::AgeRestriction for issuing and holds
This patch updates CanBookBeIssued and CanItemBeReserved to use the
new policy class instead of calling GetAgeRestriction directly.

Test plan:
1. Run tests without this patch:
   $ ktd --shell
   k$ prove t/db_dependent/Circulation.t \
            t/db_dependent/Reserves.t
=> SUCCESS: Tests pass!
2. Apply this patch
3. Repeat step 1
=> SUCCESS: Tests pass!
4. Sign off :-D

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:29 +00:00
Tomás Cohen Arazi 8ebf3436cb
Bug 42529: Add Koha::Policy::Biblio::AgeRestriction
This patch adds age restriction handling to the Koha namespace:

- Koha::Biblio->age_restriction: metadata extractor that parses the
  agerestriction field using the AgeRestrictionMarker system preference.
  Returns the numeric age or undef. Uses single return with last pattern.
- Koha::Policy::Biblio::AgeRestriction->check($biblio, $patron): policy
  class that evaluates whether a patron meets the age restriction.
  Returns a Koha::Result::Boolean (with payload on restriction).

Test plan:
1. Apply this patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/Koha/Policy/Biblio/AgeRestriction.t
=> SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:07:29 +00:00
Olivia Reynolds 51e3d5435a
Bug 40668: Don't depend on checkboxes existing in drawCallback
The checkboxes this callback listens to events on are conditionally
rendered, depending on whether the user has permission to edit each
item. When moving from a page with no checkboxes displayed to one with
checkboxes displayed, the checkbox elements may not exist at the time of
this callback, so the listener is not attached and the checkboxes are
inoperable.

This patch fixes this by using a delegated event handler on the row,
which doesn't depend on the checkbox existing at the time.

Test plan:
1.  Enable the system preference EnableItemGroups.
2.  Set up a non-super-librarian patron, with permission to:
    - view the catalogue (catalogue)
    - create update and delete item groups (manage_item_groups)
3.  Place this patron's library in a group by itself, with "limit item
    editing" enabled. The patron should only be able to edit items from
    their own library.
4.  Pick an item in the catalogue, and add at least 40 items to it from
    a library the patron cannot edit items from. Add at least one item
    from the patron's library that they can edit. The 40 items are to
    ensure there is a complete (20 item) page that the patron cannot
    edit anything on.
5.  Create an (empty) item group on this catalogue entry.
6.  Log in as the unprivileged patron and navigate to the holdings view
    for the relevant catalogue entry.
7.  Navigate through the holdings table pages until there is a page with
    no checkboxes.
8.  Navigate to a table page with a checkbox. Select one of the
    checkboxes, and verify no actions appear at the top of table, even
    though you should have permission to add this item to a group.
9.  Apply this patch, and hard refresh to load the new JavaScript.
10. Repeat steps 6-8, verifying the checkbox now appears correctly.
11. Verify you can add the item to an item group using the now-appearing
    action.

Sponsored-by: Open Fifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:28 +00:00
Pedro Amorim 60725ab4f1
Bug 43204: Fix ILL availability Source column ignoring deep link
ill-availability.js renders results from ILL availability plugins.
Each result can carry a `url` (deep link to the record) and an
`opac_url` (generic fallback). The Title column correctly prefers
`url`, but the Source column links to `opac_url` unconditionally and
never checks `url`, so results always link to the bare OPAC URL even
when a deep link is available.

Make Source prefer `url`, falling back to `opac_url`, same as Title.

To test:
1) Install the latest release of the plugin:
   https://github.com/openfifth/koha-plugin-ill-avail-z3950/releases
2) Enable ILLModule, ILLCheckAvailability.
3) Set AdditionalFieldsInZ3950ResultSearch = 856$a, 999$c, 001.
4) Add Z39.50 server: Name KTD, Hostname localhost, Port 2100,
   Database biblios, Syntax MARC21/USMARC.
5) Configure the ILL z3950 plugin: tick the 'KTD' box, set bib ID
   field 999$c, deep link
   http://localhost:8081/cgi-bin/koha/opac-detail.pl?biblionumber=source_record_id,
   OPAC URL http://localhost:8080, tick the last 3 display
   checkboxes, leave Partner ID blank.
6) Add a new ILL request: type book, ISBN 0879307447, cardnumber 42,
   destination library Centerville.
7) Go to availability search, confirm the result links to the
   correct deep-linked biblio, not a bare fallback.

Signed-off-by: Edith Speller <Edith.Speller@ukhsa.gov.uk>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:27 +00:00
Jonathan Druart c39aa5f6a7
Bug 42451: Remove duplicate success message
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:26 +00:00
Jonathan Druart fb0fb890e2
Bug 42451: Remove JS error when converting a report
Test plan:
Create a report with: SELECT marcxml from biblioitems
Hit /reports/guided_reports.pl?op=list
Notice the "Update SQL" button in the "Update" column, Click "Update"
=> JS error
"Uncaught TypeError: can't access property "backdrop", this._config is undefined" (in Firefox)
And nothing happens
Apply this patch, reload, click the button
=> Success, the report is updated

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:25 +00:00
Laura_Escamilla e9b9509461
Bug 42235: Add message type header to OPAC messaging table
The OPAC messaging table includes an empty first column header. This column
contains the message type, such as Item due, Advance notice, Hold filled, and
Auto renewal. Because the header only contains a non-breaking space, it is
flagged by accessibility tools as an empty <th> element.

Add a translated "Message type" label to the first column header so it has
meaningful accessible text.

Test plan:
1. Log in to the OPAC
2. Go to "Your messaging settings"
3. Inspect the messaging table header
4. Confirm the first column header contains no meaningful text

Apply the patch:
5. Reload the page
6. Confirm the first column header contains "Message type"
7. Confirm the messaging table still displays correctly
8. Run an accessibility checker and confirm the empty table header error is resolved
9. Sign off and have a wonderful day! :D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:24 +00:00
Laura_Escamilla 444cad89c7
Bug 42235: Add accessible label to DataTables control column in OPAC messaging
The OPAC messaging table includes a DataTables responsive control column
with no header text. This results in an empty <th> element and an empty
aria-label, which is flagged by accessibility tools.

Add a translated "Expand" label to the control column header so it has
meaningful accessible text.

Test plan:
1. Log in to the OPAC
2. Go to "Your messaging settings"
3. Inspect the messaging table header
4. Confirm the control column header contains no text

Apply the patch:
5. Reload the page
6. Confirm the control column header contains "Expand"
7. Confirm responsive row expansion still works
8. Run an accessibility checker and confirm the error is resolved
9. Sign off and have a wonderful day!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:23 +00:00
Pedro Amorim de230bc58b
Bug 42656: (QA follow-up) Use _abort_operation() for Local's 'not configured' errors too
_upload_file, _download_file, _list_files and _rename_file's 'no directory configured' checks still called add_message() directly, skipping the status persistence this bug added elsewhere in the file. Route them through _abort_operation() so all error paths in Local.pm stay consistent.

prove t/db_dependent/Koha/File/Transport/Local.t

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:22 +00:00
Martin Renvoize 4deb199163
Bug 42656: (QA follow-up) Make error status persistence consistent, with a full trace, across FTP, SFTP and Local
Two related inconsistencies remained across the three transport
backends' error-handling paths:

1) Only Koha::File::Transport::SFTP's _abort_operation() persisted an
   "errors" status snapshot to the status column on every failed
   operation. FTP never did, and Local had no _abort_operation() at
   all - so an SFTP transport's displayed status could reflect a
   single transient failure from real usage (e.g. an EDI cron job)
   while the exact same failure on FTP/Local left the stored status
   untouched (stale, possibly still showing the last explicit
   test_connection() result). This is exactly the kind of unclear
   status reporting bug 42656 is about.

2) FTP's _abort_operation() carried its extra detail under an
   operation_params key, while SFTP (and Local's plain add_message
   calls) used path. The change_directory() success payload had the
   same split: FTP used directory, SFTP and Local used path. The
   admin template's change_directory row display depended on FTP's
   mismatched key names, which meant it silently failed to show the
   tested directory for SFTP transports.

Adds a shared _record_error($operation, \%payload) to the
Koha::File::Transport base class - add_message() plus status
persistence - and routes all three backends' _abort_operation() (Local
gains one) through it. Renames FTP's operation_params/directory keys
to path throughout, matching SFTP and Local, and simplifies the
template to read operation.detail.path unconditionally instead of
ORing two differently-named keys.

_record_error() persists the full trace of operations recorded on the
transport object so far this session (every success leading up to the
failure), not just the failing operation in isolation - the same
"full trace" shape TestTransport::process() already builds for an
explicit test_connection() run. A long-lived transport used across a
multi-step operation (e.g. an EDI download loop touching several files
before one fails) will therefore show every successful step leading up
to the failure. It also logs the failure - including the built trace -
via Koha::Logger at warn level, so it's visible in the Koha logs
without needing to query the transport's status column.

Test plan:
1. prove -r t/db_dependent/Koha/File/ t/db_dependent/Koha/BackgroundJob/TestTransport.t t/db_dependent/Koha/Edifact/Transport.t t/db_dependent/api/v1/file_transports.t
   => All tests successful (48 tests total), including:
      - new "_abort_operation persists status consistently" subtests
        in FTP.t, SFTP.t and Local.t that reload the transport from
        the DB after a simulated failure and confirm the status
        column was updated with the correct operation code and path
        detail;
      - a new "_record_error() tests" subtest in Transport.t proving
        the persisted trace includes a prior success message plus the
        failure (not just the failure alone), and that a warn-level
        log line mentioning the operation and error was emitted.
2. koha-qa.pl -v 2 --more-tests
   => [PASS], no critic/pod warnings.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:21 +00:00
Martin Renvoize f8cf2aea42
Bug 42656: (QA follow-up) Fix dead status code in file_transport_test.inc
Two issues in the operation_description BLOCK's status text, both
flagged in review:

1) The IF/SET that computed the " passed."/" failed." status text sat
   between the SWITCH statement and its first CASE. Template Toolkit
   only executes CASE-labelled content inside a SWITCH block, so this
   was dead code - status was never actually set, and every operation
   row silently rendered with no passed/failed text at all. It also
   checked operation.error, a field that doesn't exist on the
   operation hash (the error message lives under operation.detail.error,
   and whether the operation failed is operation.status eq 'error');
   even if the code path were reachable it would always have taken the
   "passed" branch.

2) Relying on a leading space baked into the " passed."/" failed."
   string literals to separate them from the preceding </span> is
   fragile. Made the space explicit in the markup instead, and dropped
   the leading space from the string values.

Moves the status computation above the SWITCH (using operation.status),
and adds an explicit space before [% status | html %] on every CASE line.

Also adds t/db_dependent/Koha/BackgroundJob/TestTransport.t, which was
entirely missing: it exercises process() end-to-end (enqueue, then
process, then reload both the job and the transport) and confirms the
persisted trace records every operation in sequence - successes before
the failure that ended the run - rather than collapsing to just the
final error.

Test plan:
1. prove t/db_dependent/Koha/BackgroundJob/TestTransport.t
   => All tests successful, confirming process() persists the full
      operation sequence (order preserved, at least one success and
      one error present) on both the job and the transport's own
      status column.
2. Manually rendered the fixed operation_description BLOCK with
   Template Toolkit against mock operations (mixed success/error) and
   confirmed every row now shows "Connection passed.", "Change
   directory (/incoming) passed.", ..., "Change directory (/outgoing)
   failed.- error:No such directory" - status text present with
   correct spacing, where before the fix no status text rendered at
   all.
3. prove -r t/db_dependent/Koha/File/ t/db_dependent/Koha/Edifact/Transport.t t/db_dependent/api/v1/file_transports.t
   => All tests successful (47 tests total), no regressions.
4. koha-qa.pl -v 2 --more-tests
   => [PASS], no critic/pod warnings.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:20 +00:00
Nick Clemens 7d6dafef6d
Bug 42656: Improve file transport test display
This patch displays the directory tested in the background jobs results

It updates the language when test results are not stored for the transport and
links to the job when results are stored

To test:
1 - Setup an ftp file transport with good credentials
2 - Enter non-existent directories in incoming/outgoing
3 - Save
4 - Verify on save 'Never used' shows for status
5 - Refresh the page, status now:
     Connection ok
     Change directory ok
     Change directory failed
     Change directory ok
     Change directory failed
6 - Apply patch, restart all
7 - Edit/save the transport again, change nothing
8 - Confirm the page says 'No test results stored'
9 - Reload the page
10 - Confirm results are now:
     Connection ok
     Change directory ok
     Change directory failed
     Change directory ok
     Change directory failed
     Test job details
11 - Click on the job details link
12 - Confirm actual directory and errors are shown
13 - Edit the transport, use existing directories
14 - Save, reload
15 - Confirm the success results and link t background job

Signed-off-by: John Vinke <jvinke@austinseminary.edu>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:19 +00:00
Martin Renvoize a0433635fa
Bug 43092: Stop Local transport falling back to the Koha process's own cwd
Koha::File::Transport's three backends disagree on what
change_directory(undef) means: FTP navigates to the server root,
SFTP falls back to the SFTP session's own home directory, and Local
fell back to the configured download_directory or, if that's also
unset, silently to '.' - the Koha process's own working directory,
which has no relation to any configured transport location.

test_connection() calls change_directory(undef) unconditionally for
both the download and upload directory checks on every config save
(via the auto-enqueued TestTransport background job). For a Local
transport configured with only one of download_directory/
upload_directory, the untested direction's list_files() call would
silently list the Koha process's own cwd (e.g. the Koha install root)
instead of failing cleanly.

Add a private _working_directory($configured_field) helper that
returns the tracked change_directory() override or the given
configured field, but never falls back to '.'. _upload_file,
_download_file, _list_files and _rename_file now use it and record a
clear "not configured" error instead of silently acting on an
unrelated directory when neither is set.

FTP's root and SFTP's home-directory fallbacks are left as-is: unlike
Local's '.', they navigate to a real, meaningful location for that
specific connection rather than leaking unrelated filesystem content,
so they aren't part of this fix.

Test plan:
1. prove t/db_dependent/Koha/File/Transport/Local.t
   => All tests successful, including the new subtest proving that
      list_files()/rename_file()/download_file() fail cleanly (with a
      "not configured" error) rather than falling back to '.' when
      their respective directory isn't configured, while the
      configured direction (upload, in the test) still works.
2. prove -r t/db_dependent/Koha/File/ t/db_dependent/Koha/Edifact/Transport.t t/db_dependent/api/v1/file_transports.t
   => All tests successful (43 tests total), no regressions.
3. koha-qa.pl -v 2 --more-tests
   => [PASS], no critic/pod warnings.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:18 +00:00
Pedro Amorim 885d1f65dc
Bug 42766: (RM follow-up): Ensure ScriptLog exists in new installs
Fix:
prove t/db_dependent/check_sysprefs.t

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-06 16:05:15 +00:00
Casey Conlin c267c679a1
Bug 16631: Fix: Remove TT tags from kohaTable() initialization in guided_reports_start.tt
Sponsored-by: Ramapo Catskill Library System

Bug 16631: Update saved reports table to use use named DataTables columns.

RM amended: Fix commit title

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-04 10:20:59 +00:00
Martin Renvoize 0d0ba262fe
Bug 42915: (QA follow-up) Defer coded_values declaration until document ready
holds.js declared `coded_values` as a bare top-level global, evaluated
immediately at script-parse time, reading `all_libraries` which is only
ever defined by the including template (request.tt, or
patron-detail-tabs.inc for circulation.tt/moremember.tt). This works
today only because those templates happen to emit their `all_libraries`
script block before Asset.js("js/holds.js") in the rendered HTML - a
fragile, easy-to-break ordering dependency between separate template
files, not something holds.js itself can guarantee.

Move the assignment into the existing $(document).ready() handler,
setting it explicitly on `window` so it remains visible as a global to
datatables.js's build_query(), which reads `coded_values` unqualified.
This defers evaluation until all synchronous inline scripts (including
whichever template defined all_libraries) have already run, regardless
of future include/ordering changes.

To test:
1. Place holds on a title for two different pickup libraries
2. Confirm the holds table on reserve/request.tt, the patron's Holds
   tab (circulation.pl and moremember.pl) all load with no console
   errors
3. Confirm filtering the holds table by pickup library name/branchcode
   still works on all three pages

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-04 10:20:58 +00:00
Martin Renvoize c7d13ee7a3
Bug 40445: (follow-up) Move TT values out of kohaTable script blocks
xt/tt_kohaTable_tidy.t forbids mixing Template::Toolkit tags with
.kohaTable() calls inside the same <script> block, since such blocks
are expected to be pure JS. The cashup modal init calls in
pos/register.tt and pos/registers.tt embedded TT-derived values
(bankable amount, register float, reconciliation note settings,
cashup progress) directly alongside kohaTable() initialisation,
tripping the check.

This moves those TT expressions into the existing TT-tag-only
<script> block that precedes the kohaTable block in each template,
exposing them as a plain JS object (cashupModalData) that the
kohaTable block then reads.

Test plan:
1. Apply the patch.
2. prove xt/tt_kohaTable_tidy.t -- register.tt and registers.tt
   should no longer be listed among the failures.
3. Load the POS register and registers pages, confirm the trigger
   and confirm cashup modals still receive the correct bankable
   amount, register float, and reconciliation note settings.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-04 10:20:56 +00:00
Pedro Amorim fa6ef97625
Bug 42766: DBRev 26.06.00.011
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-04 10:20:55 +00:00
Nick Clemens 0905d6a89f
Bug 42766: (QA follow-up) Add 'SCRIPTS' to log viewer
Apply this patch and confirm you can limit to 'Scripts' and that 'Scripts' is not in
all caps in the results

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-04 10:20:54 +00:00
Nick Clemens 4b4eb136cb
Bug 42766: (QA follow-up) Broaden sensitive patterns
We are all lazy, we aren't going to type --password everytime, we should
protect 'p' and 'pass' as well

This could likely get broader too later

TO test:
1 - perl misc/devel/create_superlibrarian.pl --branchcode=CPL --categorycode=S --cardnumber 3 --userid=ted3 --p=dog
2 - Note password is logged
3 - Apply this patch
4 - perl misc/devel/create_superlibrarian.pl --branchcode=CPL --categorycode=S --cardnumber 33 --userid=ted33 --p=dog
5 - Password redacted in log
6 - perl misc/devel/create_superlibrarian.pl --branchcode=CPL --categorycode=S --cardnumber 333 --userid=ted333 --pass=dog
7 - Password still redacted

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-04 10:20:53 +00:00
Martin Renvoize 53d5112c3a
Bug 42766: Add tests for logscriptaction() and _scrub_argv()
t/db_dependent/Log.t
- Add subtest verifying logscriptaction() logs to the SCRIPTS module,
  respects the ScriptLog preference, prefixes loginfo with $0, appends
  the info parameter, and honours custom action values.

t/Koha/Script_scrub_argv.t
- New test file exercising all branches of _scrub_argv():
  --flag value form, --flag=value form, non-sensitive flags, case-
  insensitive matching, multiple sensitive patterns (token, secret,
  apikey, api_key), and a flag immediately followed by another flag.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-04 10:20:52 +00:00
Martin Renvoize 5db19647c9
Bug 42766: Add ScriptLog auto-logging and ARGV scrubbing to Koha::Script
Extends the INIT/END blocks added by Bug 42714 to also log non-cron
scripts (those using bare 'use Koha::Script') to the SCRIPTS action-log
module when the new ScriptLog system preference is enabled.

Also introduces _scrub_argv(), which redacts the value of any command-
line flag whose name contains a sensitive pattern (password, secret,
token, etc.) before the argument list is logged. Scrubbing applies to
both cron and non-cron scripts, covering both --flag value and
--flag=value forms. This prevents credentials passed on the command
line (e.g. misc/devel/create_superlibrarian.pl --password ...) from
appearing in the action log.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-04 10:20:51 +00:00
Martin Renvoize e6091dc13e
Bug 42766: Add logscriptaction() to C4::Log
Adds a logscriptaction() function parallel to cronlogaction() that logs
to the SCRIPTS action-log module rather than CRONJOBS, gated behind the
new ScriptLog system preference. Exported from C4::Log alongside the
existing cronlogaction().

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-04 10:20:49 +00:00
Martin Renvoize 57cceec6bc
Bug 42766: Add ScriptLog system preference
Adds a new YesNo system preference 'ScriptLog' in the Logging section
of system preferences, adjacent to CronjobLog. When enabled, the start
and end of non-cron Koha scripts (those using bare 'use Koha::Script'
rather than 'use Koha::Script -cron') will be recorded in the action
log under the SCRIPTS module.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-04 10:20:46 +00:00
Paul Derscheid fdebc68051
Bug 43091: Clean the OPAC Vue dist directory in js:clean
js:clean predates the OPAC islands build (bug 26355) and only removes
the intranet and Cypress dist directories, so stale chunks survive in
koha-tmpl/opac-tmpl/bootstrap/js/vue/dist/ across rebuilds.

- add the OPAC Vue dist directory to js:clean

Test plan:
1. yarn js:build, then apply this patch series and yarn js:clean
2. ls koha-tmpl/opac-tmpl/bootstrap/js/vue/dist/
   => empty, no stale staff-only chunks remain
3. yarn js:build
   => only islands.esm.js and a patron-self-renewal chunk are emitted

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-03 13:00:37 +00:00
Paul Derscheid 8ebc1d0f2b
Bug 43091: Stop emitting staff-only Vue islands into the OPAC dist
Both islands builds share modules/islands.ts, which hardcodes every
island. The OPAC compilation therefore emits staff-only lazy chunks
(acquisitions-menu, vendor-menu, admin-menu) into
koha-tmpl/opac-tmpl/bootstrap/js/vue/dist/, and the OPAC islands.esm.js
bundles the staff Pinia stores.

- split modules/islands.ts into shared machinery and per-application
  entry points (islands-intranet.ts, islands-opac.ts)
- declare each application's islands as a Map literal and feed it
  through registerIsland(), the same path plugin islands take
- pass store definitions into hydrate() instead of importing every
  store in the shared module
- keep the full plugin import surface (registerIsland, hydrate, Vue
  re-exports) on both entries via star re-exports
- point the rspack islands entries at the per-application modules

Test plan:
1. yarn build
2. ls koha-tmpl/opac-tmpl/bootstrap/js/vue/dist/
   => only islands.esm.js and a patron-self-renewal chunk are emitted
3. ls koha-tmpl/intranet-tmpl/prog/js/vue/dist/
   => acquisitions-menu, vendor-menu and admin-menu chunks are still
   present
4. In the staff interface, visit pages using the acquisitions, vendor
   and admin menu islands and verify they render
5. Enable self-renewal for a patron's category and put the patron
   inside the availability window, e.g.:
   UPDATE categories SET self_renewal_enabled = 1
     WHERE categorycode =
     (SELECT categorycode FROM borrowers WHERE userid = 'koha');
   UPDATE borrowers
     SET dateexpiry = DATE_ADD(CURDATE(), INTERVAL 7 DAY),
     debarred = NULL WHERE userid = 'koha';
6. Log into the OPAC as that patron and open 'your summary'
   => the patron-self-renewal chunk loads from
   /opac-tmpl/bootstrap/js/vue/dist/ and the self-renewal modal
   completes successfully

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-03 13:00:36 +00:00
Owen Leonard be1bc44511
Bug 43206: cn_browser plugin red background on exact match is hard to read
This patch updates the cn_browser plugin output so that table rows are
highlighted using row classes and in colors which have sufficient
contrast.

To test, apply the patch and set your 952$o tag to use the cn_browser
plugin:

- Administration -> MARC bibliographic frameworks -> BKS -> MARC
  Structure -> Tag 952 -> Edit subfields.
  - Under the "Full call number" tab (o), set the "Plugin" dropdown to
   "cn_browser.pl" and save your changes.
- Go to Cataloging and locate a bibliographic record using the
  cataloging search.
- Add or edit items. NOTE: In the testing data there is only one item
  with a callnumber and it is "FIC/kas"
  - In the "o - Full call number" field, type "FIC" and then click the
    plugin editor image.
    - You should see the record with the "FIC/kas" call number and a
      green row right above it (indicating where an item with
      the call number "FIC" would be in the results).
  - Revise your search using the form at the top, entering "FIC/kas".
    - The record row should now be highlighted in pale yellow.

Sponsored-by: Athens County Public Libraries
Signed-off-by: Gretchen <maxeinergl@gcc.edu>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-03 13:00:35 +00:00
Martin Renvoize 4db4d8be55
Bug 43088: Reuse _current_directory() instead of duplicating pwd() calls
_change_directory(), _list_files() and _is_connected() each called
$self->{connection}->pwd directly for their log payloads/connectivity
check, duplicating exactly what _current_directory() now provides as a
proper, documented internal accessor. Route them through it instead -
no behaviour change, just removing the duplication now that the
wrapper exists.

Test plan:
1. prove -r t/db_dependent/Koha/File/
   => All tests successful (32 tests), including the existing
      current_directory() and list_files()/change_directory() coverage.
2. koha-qa.pl -v 2 --more-tests
   => [PASS], no critic/pod warnings.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-03 13:00:34 +00:00
Martin Renvoize 7a750c7e83
Bug 43088: Add a consistent current_directory() accessor to Koha::File::Transport
FTP, Local and SFTP list_files() were unified to return the same flat
shape, but consumers still had no portable way to ask a transport for
its current remote directory - the underlying libraries' own methods
of that name don't agree on what "cwd" means. Net::SFTP::Foreign's
cwd() is a pure read-only accessor, but Net::FTP's cwd() is a mutator:
called with no argument it defaults to "/", issues a real CWD command,
and returns a boolean rather than a path.

The koha-plugin-transport-browser plugin discovered this the hard way:
it read $transport->{connection}->cwd directly to capture the initial
working directory, which happened to work for SFTP but silently reset
the FTP connection to root and captured "1" (a boolean) as the "path"
on every request, breaking directory navigation for FTP transports.

Add a public current_directory() method to the Koha::File::Transport
base class, following the same connect-on-demand pattern as
list_files()/upload_file()/etc, with a _current_directory() hook each
subclass implements using its own real read-only accessor: pwd() for
FTP, cwd() for SFTP, and the tracked {current_directory} (falling back
to download_directory) for Local. Consumers should use this instead of
reaching into transport internals.

Test plan:
1. prove t/db_dependent/Koha/File/Transport.t
        t/db_dependent/Koha/File/Transport/FTP.t
        t/db_dependent/Koha/File/Transport/Local.t
        t/db_dependent/Koha/File/Transport/SFTP.t
   => All tests successful, including the new current_directory()
      tests subtest in each transport test file, which mock the
      underlying libraries (Net::FTP / Net::SFTP::Foreign) to assert
      each backend reads its own real read-only accessor.
2. koha-qa.pl -v 2 --more-tests
   => [PASS] for all four changed modules, no critic/pod warnings.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-03 13:00:33 +00:00
Martin Renvoize 5ca496f1b7
Bug 43078: Exclude '.' and '..' entries from SFTP file listings
The FTP and Local transports both explicitly filter out '.' and '..'
pseudo-entries when listing a directory, but the SFTP transport never
did - it relied on the assumption that Net::SFTP::Foreign's ls() already
excludes them. It doesn't: ls() returns exactly whatever entries the
remote server includes in its SSH_FXP_READDIR response, with no default
filtering. Whether '.'/'..' show up therefore depends entirely on the
remote SFTP server implementation, producing inconsistent results between
otherwise identically configured SFTP accounts.

Test plan:
1. prove t/db_dependent/Koha/File/Transport/SFTP.t
   => fails before this patch (listing includes '.' and '..')
   => passes after this patch
2. prove -r t/db_dependent/Koha/File/Transport/ t/db_dependent/Koha/File/Transports.t
   => all still pass

Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-03 13:00:32 +00:00
Martin Renvoize 407e1fe360
Bug 43078: Include longname and perms keys in FTP's NLST fallback listing
The MLSD path, SFTP, and Local always return all six documented
list_files() keys (filename, longname, size, perms, mtime, type) - undef
where the value is unknown, but the key always present. The NLST
fallback path (servers without MLSD support) only set filename, size,
mtime and type, omitting longname and perms entirely rather than
setting them to undef.

Functionally harmless for consumers reading $file->{perms} (Perl
returns undef for a missing hash key too), but it breaks the "same flat
shape" contract this bug established, and would trip up any consumer
using exists() instead of defined() to check for a key.

Test plan:
1. prove t/db_dependent/Koha/File/Transport/FTP.t
   => All tests successful, including the new assertion that longname
      and perms keys are present (as undef) on the NLST fallback path.
2. koha-qa.pl -v 2 --more-tests
   => [PASS], no critic/pod warnings.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-03 13:00:31 +00:00
Martin Renvoize 76ff3af495
Bug 43078: Unify file transport list_files() across FTP, Local and SFTP
Net::SFTP::Foreign's ls() returned file metadata with size, permissions
and modification time nested inside a Net::SFTP::Foreign::Attributes
object, while FTP and Local returned flat hash keys and silently
dropped directories. Callers of list_files() needed backend-specific
parsing to get at file metadata or to know a directory even existed,
defeating the purpose of the shared transport API.

All three transports now return the same flat hashref shape per entry:
filename, longname, size, perms, mtime, type ('file'/'directory'/
'other'). Directories are included everywhere; '.' and '..' remain
excluded everywhere. FTP's MLSD path prefers the UNIX.mode fact for
exact Unix permissions, falling back to an approximation from the RFC
3659 perm capability fact when UNIX.mode isn't available, and parses
the modify fact into mtime; its NLST fallback (servers without MLSD)
probes each entry with SIZE/MDTM to recover size, mtime and a
best-effort type.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-03 13:00:30 +00:00
Owen Leonard 76167f6cfe
Bug 43177: Use archived link for Bridge Material Type Icon Set
This patch updates the About page in the section for the Bridge Material
Type Icon Set. The link is updated to use the last valid saved version
on the Internet Archive.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-03 13:00:29 +00:00
Martin Renvoize d2b4fd2709
Bug 42992: FTP file transport: fix list_files() corrupting filenames
Koha::File::Transport::FTP::_list_files parsed the LIST / "ls -l" output
with a non-greedy date capture that only consumed the first of the three
date tokens (the month), leaving the day and time glued to the front of
the filename. Every filename returned for an FTP transport was therefore
prefixed with the listing date, e.g.

    -rw-r--r-- 1 user group 1234 Jan 01 12:00 QUOTES_413514.CEQ
    => filename "01 12:00 QUOTES_413514.CEQ"

Callers (Koha::Edifact::Transport, marc_ordering_process.pl) then asked
the server for a file that does not exist, so no files were downloaded,
renamed or deleted over FTP. SFTP and Local transports were unaffected.

This regressed when the EDI transport was moved onto the file_transports
system (bug 38489). The previous Koha::Edifact::Transport::ftp_download
listed files with Net::FTP->ls (NLST, bare names); the refactor switched
to Net::FTP->dir (LIST) so it could return the richer hashref shape used
by the SFTP and Local transports, but the parse was faulty.

The date field in "ls -l" output is a fixed run of three whitespace
separated tokens (Mon DD HH:MM or Mon DD YYYY). Consume it explicitly so
the filename is captured cleanly, preserving spaces and still skipping
directories. The LIST-based approach and the filename/longname/size/perms
return shape are kept so the structure stays consistent across transport
classes (relied on by consumers, including out-of-tree ones).

Test plan:
1) prove t/db_dependent/Koha/File/Transport/FTP.t
2) git checkout Koha/File/Transport/FTP.pm and re-run: the list_files()
   subtest fails on the filename/size/perms assertions
3) prove t/db_dependent/Koha/Edifact/Transport.t

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-08-03 13:00:25 +00:00
Matt Blenkinsop d6d95f7d7c
Bug 42827: Restore values to the Items expected column in parcels.pl
Test plan:
1. Create an order with multiple items and receive the order in full
2. From the vendor record, click Receive shipments
3. You should be in parcels.pl and the table should show your shipments
4. The Items expected column will show 0 even though you ordered multiple items
5. Apply patch and restart_all
6. Refresh the page, the column should now show a value

Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:35 +00:00
Martin Renvoize 5799159034
Bug 43082: Fix flaky object filter test in action_logs.t
The default `_match=contains` matching builds a LIKE query even on
integer columns, so a plain `?object=42` also matches object values
like 420 or 142. The test's dynamically-assigned patron borrowernumber
(auto_increment, never reset by transaction rollback) could drift into
a value containing "42" as a substring, intermittently inflating the
match count in CI. Add `_match=exact` since the test's intent is an
exact object id match.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:34 +00:00
David Cook bbc0124ede
Bug 43159: Restore localisation of PERL5LIB and KOHA_HOME
This change localises PERL5LIB and KOHA_HOME,
which means that any changes made to PERL5LIB or KOHA_HOME
within _do_instance() in koha-plack are confined to the specific
instance being worked on in that function.

See Bugzilla for steps on how to reproduce the problem.

Test plan:
- Apply the patch
- sudo vi debian/scripts/koha-plack

Within _do_instance after PERL5LIB is set, add the following:
echo $PERL5LIB

- sudo koha-create --request-db test1
- sudo ./debian/scripts/koha-plack --restart test1 test1 test1 test1
- Note that now PERL5LIB stays constant:

/usr/share/koha/lib:/usr/share/koha/installer:/usr/share/koha/lib/installer

- git restore debian/scripts/koha-plack

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:33 +00:00
Kyle M Hall 6d30ead414
Bug 43103: Switch OR chains to INs for batch item modification and patron deletion
Three searches pass large ID lists:
* Koha::BackgroundJob::BatchUpdateItem passes every itemnumber of the
  batch, and batch item modifications can cover tens of thousands of
  items
* Koha::Items::batch_update does the same with the modified
  itemnumbers when collecting biblionumbers for reindexing
* Koha::Patrons::filter_by_safe_to_delete does it with the
  borrowernumbers found safe to delete ( unlikely to approach
  the same numbers as items/bibs, but still good to fix )

This patch switches all three to IN. The lists are still passed as
values, but the query stays a flat IN list the optimizer handles well
instead of a chain of ORs that can't be optimized.

Test Plan:
1) Apply this patch
2) prove t/db_dependent/Koha/Items/BatchUpdate.t
3) prove t/db_dependent/Koha/Patrons.t
4) Note all tests pass!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:31 +00:00
Paul Derscheid 53524b4824
Bug 43174: (QA follow-up) Restore user choices on redraw independent of search state
To test:
- cypress run --spec t/cypress/integration/KohaTable/PatronSearch_spec.ts
- cypress run --spec t/cypress/integration/KohaTable/KohaTable_spec.ts

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:30 +00:00
Jonathan Druart 684c6cb96a
Bug 43174: Restore defer loading for patron searches
On the hold request and the patron search pages, the search is not
deferred when a state exists.

Coming from
  commit 2a34413601
  Bug 41604: Save user decision per column when visibility changes
-        if (default_save_state_search) {
+        if (default_save_state || default_save_state_search) {
             $("#" + settings.nTable.id).data("loaded_from_state", true);
-        } else {
+        }

+        if (!default_save_state_search) {
             delete state.search;
             state.columns.forEach(c => delete c.search);

I think this was a leftover from another try to fix bug 41604. I don't
see how it could help in the context of saving the user decision.
In the different patches from bug 41604 we don't test loaded_from_state.

Test plan:
Go to /cgi-bin/koha/members/members-home.pl
=> The table is not displayed
Search for patrons
=> The table is displayed
Reload the page
=> Without this patch the table is displayed
=> With this patch the table is not displayed

If you have "Save search state on page change" enabled for this table,
the table should be displayed if a previous search was made.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:29 +00:00
Jonathan Druart 96f1906fcc
Bug 43174: Add a Cypress test
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:27 +00:00
Jonathan Druart 21e34086ca
Bug 43053: Simplify code, only 1 return statement
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:26 +00:00
Matt Blenkinsop 2ad5ba09cb
Bug 43053: Fix display of vendor payment methods
Test plan:
1) Add multiple vendor payment method authorised values
2) Create a vendor and add multiple payment methods
3) Click to edit, the payment method will be displaying one option of a "|" concatenated string
4) Apply patch and run yarn js:build
5) Hard refresh the browser, the payment methods should now display correctly
6) Edit the payment methods and save, the saved changes should be correctly displayed on the vendor details page
7) Add a new vendor with multiple payment methods, it should save correctly

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:25 +00:00
Jonathan Druart 29e34ba1c2
Bug 43053: Add a Cypress test
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:24 +00:00
David Nind 4b7da59db5
Bug 43173: Update the terminology for the log viewer - "cardnumber" to "card number"
Update the log viewer to change "Modify cardnumber" to
"Modify card number", as per the terminology guidelines
https://wiki.koha-community.org/wiki/Terminology#C

Test plan:
1. Edit a patron and change their card number under the library
   management section.
2. Go to Tools > Additional tools > Log viewer.
3. Note under the actions section that there is an action:
   Modify cardnumber
4. Click "Submit" to see the log viewer results.
5. Note that there is an entry for the patron card number change
   with the action: "Modify cardnumber".
6. Apply the patch.
7. Refresh the log viewer page.
8. Note that "Modify cardnumber" is now "Modify card number"
   in both places.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Gretchen <maxeinergl@gcc.edu>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:23 +00:00
Jonathan Druart 3573aa2492
Bug 43168: Add a test
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:22 +00:00
Nick Clemens c13096326f
Bug 43168: Check if there are errors, not if variable defined
$errors is an array ref returned from a blank array variable

if( $errors ){ #this will be true

if( scalar @$errors ){ #only true when array has members

To test:
1 - tail -f /var/log/koha/kohadev/intranet-error.log
2 - Stage a marc file
3 - Note the error:
==> /var/log/koha/kohadev/intranet-error.log <==
[2026/07/23 19:11:37] [WARN] The following error(s) occurred during ISO2709 record import:
ERROR:  Koha::ImportBatch::try {...}  /kohadevbox/koha/Koha/ImportBatch.pm (105)
4 - Apply patch
5 - Restart all
6 - Stage a file
7 - No error!

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:20 +00:00
Pedro Amorim d5715ed753
Bug 42988: DBRev 26.06.00.010
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:19 +00:00
Pedro Amorim d9dda87fff
Bug 42988: dbic
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:18 +00:00
Kyle M Hall 73380b410c
Bug 42988: Default holds_get_captured to yes for new SIP accounts
The account editor preselects "No" for "Holds get captured" when
creating a new SIP account, but the SIP checkin default is to capture
holds. This patch makes the editor default the option to "Yes" so a
new account behaves the same as one that never set the option.

Test Plan:
1) Apply this patch
2) yarn js:build
3) Restart all the things!
4) Create a new account in the SIP2 administration module
5) Note "Holds get captured" defaults to "Yes"!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:17 +00:00
Kyle M Hall 48c54b04b6
Bug 42988: Update database, set holds_get_captured default to 1
The migration from SIPconfig.xml to the database ( bug 37893 ) leaves
holds_get_captured NULL for any account that didn't define it in the
config file. At SIP checkin an unset holds_get_captured defaults to
on, but the REST API returns NULL boolean columns as false, so the
SIP account editor shows "No" selected. Saving the account without
touching the option then stores 0 and holds are no longer captured
at SIP checkin.

This patch changes the column default to 1 and sets existing NULL
values to 1 so the stored value matches the actual checkin behavior.

Test Plan:
1) If you have no SIP accounts, create one
2) Simulate an account migrated from a SIPconfig.xml that doesn't define holds_get_captured:
   UPDATE sip_accounts SET holds_get_captured = NULL;
3) Edit the account, note "Holds get captured" shows "No" even though
   holds *are* captured at SIP checkin while the value is unset
4) Apply this patch
5) Run updatedatabase.pl
6) SELECT login_id, holds_get_captured FROM sip_accounts;
7) Note holds_get_captured is now 1!
8) Edit the account again, note "Yes" is now selected for "Holds get captured"!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:16 +00:00
Laura_Escamilla ad1d40f0b7
Bug 43134: Add missing colons to OPAC advanced search section headings
To test :
1. Apply the patch
2. Go to the OPAC advanced search page http://kohadev.mydnsname.org:8080/cgi-bin/koha/opac-search.pl
3. Click More options
4. Notice that all section titles now use colons
   - "Search for:" = colon
   - "Limit to any of the following:" = colon
   - "Location and availability:" = colon
   - "Sort by:" = colon
   - "Publication range" = colon
   - "Language" = colon
   - "Audience" = colon
   - "Content" = colon
   - "Format" = colon
   - "Additional content types..." = colon
5. Sign off and have an incredible day! :)

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:15 +00:00
Owen Leonard 32e16ca89a
Bug 43192: Cast escape_str(obj) arg to string to use escapeHtml func
The escape_str function in the OPAC requires the argument to be a string,
and will throw a fatal error if passed a Number. This is because
String.prototype.escapeHtml is created by Koha and is not a JS standard.
This patch converts the object to a string before applying escapeHtml.

To test, apply the patch and log in to the OPAC.

- If necessary, add a list.
- Search for records in the catalog and add one or more of the results
  to your new list.
- Go to Lists -> Your lists.
- In the list of your lists, click the "Delete" link for the list you
  added records to.
  - You should see a confirmation modal, "Are you sure you want to
   delete this list?" showing the name of the list and the number of
   items on it.
  - Clicking "Yes, delete" should delete the list.

Sponsored-by: Athens County Public Libraries
Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl>
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-31 13:40:08 +00:00
Martin Renvoize 0f894cb6b1
Bug 41410: DBIx::Class schema update
Regenerated via `dbic --force` after adding the local_holdgroup_match
column to hold_fill_targets and tmp_holdsqueue in the previous patch.

Test plan:
  1. Inspect Koha/Schema/Result/HoldFillTarget.pm and TmpHoldsqueue.pm
     and confirm the new local_holdgroup_match accessor is defined
     above the `DO NOT MODIFY` marker.
  2. Confirm custom content below the marker (koha_object_class,
     +item_level_request is_boolean) is preserved.

Sponsored-by: Main Library Alliance <https://www.mainlib.org/>

Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-30 08:42:33 +00:00
Pedro Amorim cf852cdd10
Bug 41410: dbic
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:15 +00:00
Pedro Amorim 471571a17d
Bug 41410: DBRev 26.06.009
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:14 +00:00
Martin Renvoize 9b27b4d03d
Bug 41410: (follow-up) Fix local_holdgroup_match set incorrectly by LocalHoldsPriority
local_holdgroup_match was being hardcoded to 1 in the two LocalHoldsPriority
allocation paths (GiveLibrary and GiveLibraryGroup), regardless of whether the
chosen item actually satisfied the LocalHoldsExclusivity conditions.  This
caused staff to be directed to pull an item that would not trigger the hold at
checkin when the two features used different patron/item controls.

The fix removes the premature local_holdgroup_match => 1 from both LHP paths and
lets _flag_local_holdgroup_matches evaluate all allocated items using the correct
LocalHoldsExclusivity* sysprefs.

Also fixes a pre-existing missing bind-variable placeholder in the
INSERT INTO tmp_holdsqueue statement (17 columns, only 16 ?).

Test updates:
- Existing local_holdgroup_match test gains the matching LocalHoldsExclusivity
  sysprefs so the flag is set via the correct code path.
- New subtest reproduces the reported bug: LHP (HomeLibrary/homebranch) targets
  a non-local item while exclusivity (PickupLibrary/holdingbranch) should not
  flag it — confirms local_holdgroup_match is 0 after the fix.

Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Sponsored-by: Main Library Alliance <https://www.mainlib.org/>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:13 +00:00
Martin Renvoize 4b23e5c5d1
Bug 41410: (follow-up) Decouple exclusivity from LocalHoldsPriority
LocalHoldsExclusivity and LocalHoldsPriority are adjacent but distinct
features: priority alters *which patron's hold* gets filled first, while
exclusivity restricts *which items* may fill a hold during a time window.
Each should be usable independently of the other.

Changes:
- Rename LocalHoldsPriorityExclusivityPeriod to LocalHoldsExclusivityPeriod
- Add LocalHoldsExclusivityPatronControl (PickupLibrary/HomeLibrary)
- Add LocalHoldsExclusivityItemControl (homebranch/holdingbranch)
- HoldsQueue: add _flag_local_holdgroup_matches() that flags
  local_holdgroup_match on allocated items using hold-group matching
  and the new exclusivity control prefs, regardless of LocalHoldsPriority
- Reserves: CheckReserves exclusivity check now computes its own
  "is this item local" using the new prefs and hold-group siblings,
  independent of the LocalHoldsPriority matching variables
- Atomicupdate handles both fresh install and rename-from-old-pref paths
- Pref UI note clarifies independence from LocalHoldsPriority

Test plan:
1. Apply patch, run updatedatabase.pl
2. Verify three new sysprefs exist in Administration > System preferences
   > Circulation: LocalHoldsExclusivityPeriod, LocalHoldsExclusivityPatronControl,
   LocalHoldsExclusivityItemControl
3. Set LocalHoldsPriority to "Don't give" (None)
4. Create a library hold group with at least two libraries (e.g. Lib-A, Lib-B)
5. Create a third library (Lib-C) NOT in the hold group
6. Create an item at Lib-B
7. Place a hold for a patron at Lib-A (pickup at Lib-A)
8. Set LocalHoldsExclusivityPeriod to 7, PatronControl to HomeLibrary,
   ItemControl to homebranch
9. Run the holds queue builder (misc/cronjobs/holds/build_holds_queue.pl)
10. Verify hold_fill_targets.local_holdgroup_match = 1 for that hold
    (item at Lib-B is in the same hold group as patron at Lib-A)
11. Check in the item at Lib-C — it should NOT fill the hold (exclusivity
    blocks the non-local item within the 7-day window)
12. Check in the item at Lib-B — it SHOULD fill the hold (local match)
13. Reset: delete the hold, create a new one with a patron at Lib-C
    (pickup at Lib-C). Run the queue again.
14. Verify hold_fill_targets.local_holdgroup_match = 0 for this hold
15. The item at Lib-B should now fill this hold at Lib-C checkin
    (non-local patron, no exclusivity protection)
16. Set LocalHoldsExclusivityPeriod to 0 — verify non-local items can
    fill holds normally regardless of hold_fill_targets flag
17. prove t/db_dependent/Holds/LocalHoldsPriority.t — passes
18. prove t/db_dependent/HoldsQueue.t — passes
19. prove t/db_dependent/Reserves.t — passes

Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:12 +00:00
Martin Renvoize bec5c357b4
Bug 41410: Unit tests
Covers the new LocalHoldsPriorityExclusivityPeriod behaviour and keeps
existing tests working with the schema change:

 - t/db_dependent/Holds/LocalHoldsPriority.t: new subtest exercising
   all four cases -- pref disabled, pref active on a fresh hold, pref
   active past the window, and pref active with no hold_fill_targets
   row (queue hasn't run).
 - t/db_dependent/HoldsQueue.t: assert that after CreateQueue() with a
   local-group match, both tmp_holdsqueue and hold_fill_targets rows
   carry local_holdgroup_match = 1.
 - t/db_dependent/Reserves.t, t/db_dependent/Koha/Hold.t: qualify the
   two direct "INSERT INTO hold_fill_targets VALUES (...)" statements
   with explicit column names so they continue to work now that
   hold_fill_targets has one extra column.

Test plan:
  prove t/db_dependent/Holds/LocalHoldsPriority.t \
        t/db_dependent/HoldsQueue.t \
        t/db_dependent/Reserves.t \
        t/db_dependent/Koha/Hold.t

All tests should pass.

Sponsored-by: Main Library Alliance <https://www.mainlib.org/>

Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:10 +00:00
Martin Renvoize a2ef73fe95
Bug 41410: Add local holds priority exclusivity period
When LocalHoldsPriority is enabled, the holds queue picks a local-group
item for eligible holds.  Until now there was nothing to reserve that
priority at checkin time: if a non-local item was returned first, it
would happily fill the hold and the local-priority decision was lost.

This patch adds a time-bounded exclusivity window so that, for a
configurable number of days after a hold is placed, a hold whose queue
target is a local-group item will not be filled by a non-local item.

Changes:
 - C4/HoldsQueue.pm: in MapItemsToHoldRequests, flag %item_map entries
   that matched on local library (GiveLibrary/GiveLibraryAndGroup) or
   local hold group (GiveLibraryGroup/GiveLibraryAndGroup) with
   local_holdgroup_match => 1, and propagate that into both the
   tmp_holdsqueue and hold_fill_targets INSERTs.
 - C4/Reserves.pm: in the CheckReserves loop, when
   LocalHoldsPriorityExclusivityPeriod is set, the hold is still within
   that window, and the item being evaluated is not itself a local
   match, skip the reserve if its hold_fill_targets row has
   local_holdgroup_match = 1.  Missing hold_fill_targets row means "no
   information" -- fall through so holds are never trapped between
   queue runs.
 - circulation.pref: expose the new LocalHoldsPriorityExclusivityPeriod
   preference next to the existing LocalHoldsPriority* entries.

The feature is a no-op unless both LocalHoldsPriority is set to
something other than "Don't give" AND
LocalHoldsPriorityExclusivityPeriod is > 0.

Test plan:
 1) Apply the patches and run the atomicupdate.
 2) Set up a library group flagged as a local hold group containing
    two libraries (call them A and B).  Add a third library C that is
    NOT in the group.
 3) Add one item to each library (itemA, itemB, itemC) on the same
    biblio.
 4) Administration -> System preferences -> Circulation:
    - Set LocalHoldsPriority to "Give library group".
    - Set LocalHoldsPriorityPatronControl to "home library".
    - Set LocalHoldsPriorityItemControl to "home library".
    - Set LocalHoldsPriorityExclusivityPeriod to 7.
 5) As a patron whose home library is A, place a title-level hold with
    pickup at A.
 6) Run the holds queue (misc/cronjobs/holds/build_holds_queue.pl).
    -> The queue should target itemA (or itemB) for this hold.
    -> Confirm in the database:
         SELECT reserve_id, itemnumber, local_holdgroup_match
           FROM hold_fill_targets;
       The targeted row must have local_holdgroup_match = 1.
    -> Holds to pull / holds queue report should also show the flag
       (tmp_holdsqueue.local_holdgroup_match = 1).
 7) At library C, check in itemC (the non-local item).
    -> The hold must NOT be captured -- itemC simply goes back on the
       shelf.
 8) At library A, check in itemA (the local item).
    -> The hold IS captured for the patron.
 9) Cancel and re-place the hold, run the queue again.  Manually age
    the hold past the window:
         UPDATE reserves
            SET reservedate = DATE_SUB(CURRENT_DATE, INTERVAL 10 DAY)
          WHERE reserve_id = <id>;
    Now check in itemC again.
    -> The hold IS captured (exclusivity window has expired).
10) Re-place the hold but do NOT run the queue.  With no
    hold_fill_targets row for the reserve, check in itemC.
    -> The hold IS captured (we never trap holds between queue runs).
11) Flip LocalHoldsPriorityExclusivityPeriod back to 0 and repeat
    step 7.
    -> The hold IS captured; the feature is fully disabled.
12) Run:
      prove t/db_dependent/Holds/LocalHoldsPriority.t \
            t/db_dependent/HoldsQueue.t \
            t/db_dependent/Reserves.t \
            t/db_dependent/Koha/Hold.t
    -> All tests pass.

Sponsored-by: Main Library Alliance <https://www.mainlib.org/>

Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:09 +00:00
Martin Renvoize 753c5fe349
Bug 41410: Add local_holdgroup_match column and exclusivity period syspref
Adds groundwork for a time-bounded local holds exclusivity feature:

- Adds a local_holdgroup_match flag (tinyint, default 0) to both
  hold_fill_targets (consulted by CheckReserves/_Findgroupreserve at
  checkin) and tmp_holdsqueue (so queue/pull-list reports can display
  it). The flag will be set by the queue builder when a local-group
  match is made.
- Adds a new system preference LocalHoldsPriorityExclusivityPeriod
  (Integer, default 0 = disabled) specifying the number of days from
  hold creation during which only locally-targeted items may fill a
  hold with a local queue target.
- Provides an atomicupdate that guards both ALTERs with column_exists
  and INSERT IGNOREs the new syspref.

Test plan:
  1. Apply atomicupdate: perl installer/data/mysql/updatedatabase.pl
  2. Verify both columns exist:
       SHOW COLUMNS FROM hold_fill_targets LIKE 'local%';
       SHOW COLUMNS FROM tmp_holdsqueue LIKE 'local%';
  3. Verify syspref row:
       SELECT variable, value FROM systempreferences
         WHERE variable = 'LocalHoldsPriorityExclusivityPeriod';
  4. Confirm default value is 0 and type is Integer.

Sponsored-by: Main Library Alliance <https://www.mainlib.org/>

Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:08 +00:00
Jan Kissig 2fe11906a7
Bug 42292: (QA follow-up) replace remaining OD_password_required with Koha.prefs.OverDrivePasswordRequired
As in opac-user.tt OD_password_required was replaced by Koha.prefs.OverDrivePasswordRequired. The script overdrive.js checks already for the Koha.prefs variable.

1) Set sysprefs: OPACOverDrive, OverDriveCirculation, OverDrivePasswordRequired
= Enable/Yes; OverDriveClientKey, OverDriveClientSecret, OverDriveLibraryID =
any non-empty value (e.g. 123).
2) Go to <opac_url>/cgi-bin/koha/opac-user.pl, click "Log in to your OverDrive
account" - password modal appears (correct).
3) Go to <opac_url>/cgi-bin/koha/opac-overdrive-search.pl?q=music, click the
same link, it fails with "No password provided" instead, no modal.

4) apply patch and repeat 3) The modal appears now.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:07 +00:00
Jan Kissig c2514018b4
Bug 42292: (QA follow-up) remove unused reference OverDriveEnabled in opac-shelves
As OverDriveEnabled is declared via SET in the template and not a system preference and after all not used in that script it is removed.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:06 +00:00
Jan Kissig 87d4eac98e
Bug 42292: (QA follow-up) Fix evaluation of OpacHighlightedWords to be numeric in opac-results
OpacHighlightedWords was set in quotation marks and resulted in either "1" or "0".
As "0" and "1" always evaluates to true in JS the following error was thrown when OpacHighlightedWords was set to Don't highlight: Uncaught TypeError: $(...).highlight is not a function

To test:
a) Set system preference OpacHighlightedWords to Don't highlight
b) search a title in the opac with open dev console and check that no error (like above) is thrown.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:05 +00:00
Paul Derscheid 71b121bb6f
Bug 42292: (follow-up) Fix OverDrive/OpenLibrary references in opac-results.tt
Three issues introduced by the OPAC port of opac-results.tt:

- The tt-allowed block sets OverDriveEnabled via
  `Koha.Preference("OverDriveEnabled")`, but OverDriveEnabled is a
  TT-side SET variable (computed from OverDriveLibraryID +
  OverDriveClientKey + OverDriveClientSecret), not a system
  preference, so Koha.prefs.OverDriveEnabled is always "".
  The code at line 621 then reads an unprefixed `OverDriveEnabled`
  identifier which is not declared -- the OverDrive result block
  never fires.
- The neighbouring block `if (OpenLibrarySearch) { ... }` reads an
  unprefixed identifier for a value that is only exposed on
  Koha.prefs, so the OpenLibrary result block also never fires.
- Both blocks build a throbber <img> with
  `src=\`${interface}/${theme}/images/spinner-small.gif\`` inside a
  single-quoted JS string; the backticks are literal characters
  there, so the rendered src ends up as that literal template
  text, not a usable URL.

Replace the OverDriveEnabled addPrefs entry with a plain
`const OverDriveEnabled = [% OverDriveEnabled ? 1 : 0 | html %];`
that reads the TT SET variable, prefix OpenLibrarySearch with
Koha.prefs, and rebuild the throbber src via normal string
concatenation.

To test:
- With OverDrive configured (OPACOverDrive, OverDriveLibraryID,
  OverDriveClientKey, OverDriveClientSecret), run a search in the
  OPAC: the OverDrive results panel shows the spinner and then
  the API results.
- With OpenLibrarySearch enabled, the OpenLibrary panel does the
  same.
- In both cases the spinner image loads (no broken-image icon).
- Disable both prefs -> neither panel is rendered, no console errors.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:04 +00:00
Paul Derscheid b4035c5a0c
Bug 42292: (follow-up) Fix duplicate NovelistSelectProfile declaration
In the new tt-allowed block, the NovelistSelect fallback declarations
redeclare `NovelistSelectProfile` instead of also exposing
`NovelistSelectPassword`:

    var NovelistSelectProfile = "[% NovelistSelectProfile | html %]";
    var NovelistSelectProfile = "[% NovelistSelectPassword | html %]";

So `NovelistSelectProfile` ends up holding the password, and
`NovelistSelectPassword` is never declared. The later
`novSelect.loadContentForQuery(..., NovelistSelectProfile,
NovelistSelectPassword, ...)` call then throws ReferenceError and
NovelistSelect content never loads on biblio detail pages.

Rename the second declaration to `NovelistSelectPassword` to match
the original template behaviour.

To test:
- Configure the NovelistSelect sysprefs (NovelistSelectEnabled,
  NovelistSelectProfile, NovelistSelectPassword) in the OPAC. Real
  NovelistSelect content only loads with valid EBSCO subscription
  credentials -- with dummy values the JS path still runs but the
  tab stays empty; that is still sufficient to verify this fix.
- Open an OPAC biblio detail page with a known ISBN.
- In the browser console, run
  console.log(NovelistSelectProfile, NovelistSelectPassword)
  and confirm both values are printed (before the fix,
  NovelistSelectPassword is undefined).
- With valid subscription credentials, NovelistSelect content loads
  in its tab/panel instead of throwing a ReferenceError in the
  console.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:03 +00:00
Paul Derscheid a8da9a5f14
Bug 42292: (follow-up) Rename opac-shelves print flag to avoid shadowing window.print
The port declares `var print = [% print ? 1 : 0 | html %];` at the
top of the tt-allowed block, then later calls `window.print()` inside
`if (print) { ... }`. Declaring `var print` at top-level overwrites
the built-in `window.print` with 0 or 1, so the subsequent
`window.print()` call throws "print is not a function" and the print
view never fires its actual print dialog.

Rename the flag to `should_print` and update the single `if (print)`
check accordingly.

To test:
- Open a public list with items in the OPAC.
- Click "Print this list" (or load the page with op=print).
- The browser print dialog opens and the window closes afterwards.
- No "print is not a function" error in the console.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:02 +00:00
Paul Derscheid ed7fef7dd7
Bug 42292: (follow-up) Replace removed dateformat_pref var in OPAC calendar.inc
The OPAC port moved the dateformat pref into Koha.addPrefs and dropped
the local `var dateformat_pref = ...;` declaration, but four usages of
`dateformat_pref` remain in the same file (the debug alert, two
`switch` statements, and the get_dateformat_str call in validate_date).
At runtime the switches throw ReferenceError, so flatpickr ends up
with the default 'Y-m-d' format and date validation via validate_date
crashes before showing the invalid-date alert.

Replace all remaining occurrences with Koha.prefs.dateformat, which is
populated by the new addPrefs block at the top of the include.

To test:
- Set dateformat syspref to each of us / metric / dmydot / iso.
- Open any OPAC page using a flatpickr input (e.g. hold date, patron
  dob on the registration form).
- Confirm the widget renders dates in the expected format and
  entering an invalid string triggers the validate_date alert.
- No ReferenceError in the browser console on page load.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:01 +00:00
Jonathan Druart e53b7346c9
Bug 42292: Tidy all script tags - OPAC (auto tidy)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:50:00 +00:00
Jonathan Druart 5e822ff613
Bug 42292: Tidy all script tags - OPAC
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the OPAC module.

Non-trivial changes, we modify the whole OPAC in a single patch.
Testing this patch should focus on:
* date formatting
* the cover images on the search result and shelves views

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:49:59 +00:00
Jan Kissig 8812de46fe
Bug 42279: (QA follow-up) Fix straying TT variable, auto tidy
Removes a left over TT variable from a JS script block without data-tt-allowed="true" attribute. Auto tidy

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:49:58 +00:00
Paul Derscheid 3dbe00ccbe
Bug 42279: (follow-up) Restore numeric compare of quantityrec
The port quoted [% quantityrec | html %] into a string const, but
the value is used as `$(this).val() < quantityrec` where both sides
are now strings and JS does a lexicographic compare ("10" < "5" is
true), giving the wrong branch when the user enters a multi-digit
quantity.

Render quantityrec as a JS number (with a 0 fallback) so the
comparison stays numeric, matching the original template output.

To test:
- Modify an existing order with quantityrec, e.g. 5 items received.
- Change quantity to 10 (higher than received): "can't add new item"
  alert fires, value reverts.
- Change quantity to 2 (lower): "don't forget to delete" alert fires.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:49:57 +00:00
Jonathan Druart af445bdeba
Bug 42279: Tidy all script tags - acqui (auto tidy)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:49:56 +00:00
Jonathan Druart 3113ca088e
Bug 42279: Tidy all script tags - acqui
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the acquisition module.

The 2 main changes are in the following files:
* basket.tt
It's mostly about table settings and the visibility of columns depending
on syspref:
  - "supplier report" / EDIFACT
  - "modify" / if basket is not closed
  - "cancel" / CancelOrdersInClosedBaskets
* neworderempty.tt
trivial changes, no error or warning in the console should appear.
Confirm than createing a new order works as expected.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:49:55 +00:00
Owen Leonard 707878312b
Bug 41116: (follow-up) Fix template variable filter
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:49:54 +00:00
Owen Leonard 56928506ae
Bug 41116: (follow-up) Some more fixes for consistency
- There was a mix of "patronphone" and "patron-phone" and I fixed it to
  standardize on "patron-phone" (and patron-email) which I find easier
  to read.
- I made similar changes to the branchtransfers template
- Corrected the TT filter on mailto links from "html" to "uri"

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:49:53 +00:00
Andrew Fuerste Henry 1b2dd6ab54
Bug 41116: Standardize selectors in patron contact information in hold found modals
This patch modifies both hold-found-modal and hold-found1. It adds a
class to the patron phone number. For patron email, it changes the
existing id to a class, expands the selector to include the bullet
point in hold-found-modal, and standardizes the name (changing from
"#boreamil' to ".patronemail").

To test:
1 - have a patron with both a phone number and an email address
2 - create a hold for your patron
3 - check an item in to fill your hold, triggering the hold-found-modal
4 - inspect the patron contact info, confirm there is no class on the
    phone number
5 - confirm there is an id on the patron email but it does not include
    the bullet point and is inconsistent with the names of other classes
    in the modal
6 - click Confirm Hold
7 - check your item in again, triggering the hold-found1 modal
8 - confirm there is no class on the patron phone number
9 - confirm there is an id on the patron email with an inconsistent
    name
10 - revert the waiting status of your hold
11 - apply patch, restart_all
12 - check your item in again, triggering hold-found-modal
13 - confirm the patron phone now has a class, .patronphone
14 - confirm the patron email now has a class, .patronemail, that the
     class includes the bullet point, and that the #boremail id has been
     removed
15 - click Confirm Hold
16 - check your item in again, triggering hold-found1 modal
17 - confirm the patron phone now has a class, .patronphone
18 - confirm the patron email now has a class, .patronemail, and that
     the #boremail id has been removed

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:49:52 +00:00
Phil Ringnalda 67da9d31ec
Bug 36897: Make it possible to use select[multiple] and select[size] in a sidebar
Currently, if you put either <select multiple> or <select size="10"> in
an <aside><fieldset> sidebar, you get a height of "one fat line" rather
than the bigger box you would expect.

Test plan:
 1. Without the patch, load Reports - Use saved
 2. Open Firefox developer tools to the Inspector tab
 3. With the "Pick an element from the page" tool, click on the word
    "Filter" in the Reports page's sidebar
 4. Just above that line in the HTML display is a <fieldset
    class="brief"> - right click that and choose Edit As HTML
 5. After the fieldset element, paste

<select multiple><option>one</option><option>two</option></select><select size="10"></select>

 6. Click outside the editor to save your changes
 7. Note that both new selects are only one line high
 8. Apply patch, run yarn build, Shift+reload to clear your cache
 9. Repeat steps 3 through 6
10. Note that both selects are more than one line high
11. Set the preference EnableAdvancedCatalogingEditor to Enable
12. Cataloging - Advanced editor
13. In the left sidebar, verify that the Clipboard is still 10 lines high,
    not one fat line like it was before bug 36589

Sponsored-by: Chetco Community Public Library
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:49:51 +00:00
Jan Kissig 84948a6041
Bug 40201: Add addidtional fields to subscription search on serials home
This patch adds additional fields (table subscription, searchable) to the subscription search form on serials home.

To test (ktd):
a) create some additional fields in Admin -> Additional fields -> Serials subscriptions. Make sure the 'Searchable' checkbox is checked.
b) go to serials home: More -> Serials
c) Check that the form is missing the additional fields just defined.
d) apply patch and call restart_all
e) reload serials home and check that the search form is now extended with the additional fields

Signed-off-by: Michaela Sieber <michaela.sieber@kit.edu>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-29 15:49:44 +00:00
Owen Leonard 7dfa99f237
Bug 42118: Update various admin pages to use grid layout for forms - Part 2
This patch updates several more administration templates with new markup
and CSS for layout forms using CSS grids.

To test, apply the patch rebuild the staff interface CSS.

Check the following pages, confirming that the "New" and
"Edit" versions of the form look correct in each case:

- Administration -> Item search fields
- Administration -> OAI sets
  * Updated OAI set mappings form to add maxlength attributes to field
    and subfield inputs.
- Administration -> OAI repositories
- Administration -> SMS cellular providers
- Administration -> SMTP servers

Sponsored-by: Athens County Public Libraries
Signed-off-by: Felicie <felicie.thiery@biblibre.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:33:03 +00:00
Jan Kissig 415d1bf087
Bug 42284: (QA follow-up) Fix missing query param index
This restores the param "index" to the popup window URL.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:33:02 +00:00
Jan Kissig 848dce7c95
Bug 42284: (QA follow-up) biblionumber must not be null
Presetting biblionumber to null (when adding a new record) will lead to a query parameter biblionumber=null when performing a z39.50 search followed by  importing a result. This patch will default biblionumber to "".

Testplan:
a) go to More -> Cataloging -> New Record ->  Z39.50/SRU search
b) search for a title f.e. "Programming Perl"
c) import one of the results
d) check that the opened URL has a param biblionumber=null and the addbiblio page displays an error:  The record you are trying to edit doesn't exist.
Add a new record or do a catalog search.
e) apply patch and redo steps a) - d) and check that query param biblonumber is 0 and addbiblio loads correct carrying the imported data.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:33:01 +00:00
Jan Kissig e30141ac7c
Bug 42284: (QA follow-up) Add missing Koha.prefs
Fix call of marcflavour which is saved in Koha.prefs store.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:59 +00:00
Paul Derscheid 3e3708f65b
Bug 42284: (follow-up) Restore hide_marc polarity for marc tag visibility
The original UNLESS/ELSE block kept MARC tag links visible by
default and hid them only when the hide_marc syspref was enabled:

    [% UNLESS Koha.Preference("hide_marc") %]
        toggleMARCTagLinks(true)
    [% ELSE %]
        toggleMARCTagLinks(false);
    [% END %]

The JS port inverted the test without swapping the branches, so
when hide_marc was off the tag links were hidden and when it was
on they were shown -- the opposite of the pref's intent.

Flip the condition back (!Koha.prefs.hide_marc).

To test:
- Set hide_marc = 0: open addbiblio.pl, MARC tag numbers and
  subfield codes are visible on first load.
- Set hide_marc = 1 and clear the marctags cookie (devtools ->
  Application -> Cookies): reload addbiblio.pl, tag numbers and
  subfield codes are hidden on first load.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:58 +00:00
Paul Derscheid bdc88607e1
Bug 42284: (follow-up) Use z3950_mandatory name as query param, not marc_lib
GetMandatoryFieldZ3950 (in cataloguing/addbiblio.pl and
authorities/authorities.pl) populates subfield_loo.z3950_mandatory
with the z3950 query-field name expected by z3950_search.pl
(isbn, title, author, issn, lccn, ...). The pre-port JS copied
this value into mandatories_label and used it as the URL parameter
name in GetZ3950Terms.

The BIG_LOOP rewrite stored subfield_loo.marc_lib as `label`
instead. marc_lib is the human-readable field label ("Title
statement"), so GetZ3950Terms started emitting URLs like
&Title+statement=... which z3950_search.pl ignores, and the
z3950 lookup from the editor stopped matching any subfield.

Use subfield_loo.z3950_mandatory for the label in
z3950_mandatory_subfields on both templates.

To test:
- Open addbiblio.pl and fill the ISBN / title / author subfields.
- Click "Z39.50/SRU search".
- In the popup form (or the network request), the pre-filled
  fields should correspond to the entered values (title -> Title,
  isbn -> ISBN, ...). Before this patch they were empty.
- Same flow from authorities/authorities.pl on a matching
  authority type.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:57 +00:00
Paul Derscheid 0cd30ad0e0
Bug 42284: (follow-up) Fix important_fields push and id concatenation
The BIG_LOOP rewrite introduced two regressions in the mandatory /
important field collector at the top of addbiblio.tt:

- `important_fields.push([ tag: ..., id: ..., ... ])` wraps the
  object in brackets instead of braces. `[a: 1]` is not a valid
  array literal, so the parser aborts the whole script tag and
  AreFieldsNotOk / PopupZ3950 never run for any record whose
  framework marks fields as "important".
- The `id` interpolation joins index and random with a literal `$`
  (`"[% index %]$[% random %]"`), but the DOM ids rendered
  elsewhere in the template (and looked up by AreFieldsNotOk via
  `divid = "tag_" + tag + "_" + id`) concatenate them with no
  separator.

Use `{...}` for important_fields and drop the `$` so the collector
shape and ids match the rest of the template.

To test:
- Open addbiblio.pl for a framework that has mandatory and
  important fields (e.g. default MARC21).
- Submit a new record with mandatory subfields empty: the error
  panel lists them and focuses the first empty input.
- Submit with only important subfields empty: the warning panel
  (not blocking) lists them under "The following important
  subfields aren't filled".
- Browser console has no SyntaxError on page load.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:56 +00:00
Jonathan Druart 3560c28bd0
Bug 42284: Tidy all script tags - addbiblio/authorities (auto tidy)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:55 +00:00
Jonathan Druart 5848c185f2
Bug 42284: Tidy all script tags - addbiblio/authorities
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the edition of biblio and authority records.

We rethink how (the ugly) BIG_LOOP works:
* push all mandatory and important fields in a JS array
* use objects/hashes instead of array to store/access tag, id, index and
  tab (vs arr[0], arr[1], ...)

For testing you need to focus on the mandatory and important fields and
subfields. Confirm that there is no change in behaviour introduced by
this patch.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:54 +00:00
Jan Kissig 043396c8ae
Bug 42293: (QA follow-up) Fix hiding of additiona fields in serials-search
This fixes the hiding of additional fields in mana search by generating an array of additional field ids instead of calling To.json(additional_fields_for_subscription) which led to an Template process error.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:53 +00:00
Jonathan Druart b54bfa2670
Bug 42293: Tidy all script tags - serials (auto tidy)
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:52 +00:00
Jonathan Druart 2ecbac6060
Bug 42293: Tidy all script tags - serials
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the serials module.

The 3 main changes are in the following files:
* routing.tt
* serials-search.tt
* subscription-detail.tt

Changes are trivial.

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:50 +00:00
Hammat Wele 8d9d749c53
Bug 41988: (follow-up) Fix opac-news-rss page title
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:49 +00:00
Hammat Wele 30cc4af870
Bug 41988: OPAC News RSS links with HTML not working
The news RSS link on the OPAC appears to be invalid when the news
content contains HTML. Users cannot subscribe to the feed using
standard RSS readers or email clients.

To reproduce:

1- Go to Tools -> HTML customizations -> News
2- Click New Entry with the following settings:
   2-1- Display location: OPAC
   2-2- Publication date: today
   2-3- Title: "News test"
   2-4- Content: click on <> and paste some HTML code, for example:
        <span style="color: #ba372a; font-size: 12pt;">Le Pr&ecirc;t
        entre biblioth&egrave;ques est mis en pause.
        D&eacute;sol&eacute;s de
        l'inconv&eacute;nients.<br />Merci de votre
        compr&eacute;hension.</span>
3- Save the entry.
4- Go to the OPAC and copy the "RSS feed for system-wide library news"
   link.
5- Execute: curl -v
   http://opac/cgi-bin/koha/opac-news-rss.pl?branchcode= and copy the
   XML output to https://validator.w3.org/feed/ to check the feed.
     ==> The feed is not valid.
6- Try to create a subscription to this link in an email client (e.g.,
   Thunderbird).
     ==> It does not work, giving the error "Feed URL is not valid".
7- Apply the patch.
8- Repeat steps 5 and 6.
     ==> The feed is now a valid RSS.
9- Repeat step 7.
     ==> You can now subscribe to the RSS feed using standard readers
         and email clients.

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:48 +00:00
Jan Kissig 08a906d95b
Bug 41681: (follow-up) Fix record number count when broken records are processed
When running the script bulkmarcimport.pl, the number of MARC records reported at the end of the execution does not match the actual number of records processed from the input file.

This patch fixes 3 issues
- broken (invalid) records from batch get now counted (record_number)
- when using the number param (n) to limit the number of records to be processed valid and invalid records get counted
- when using th offset param (o) the scipt errored when broken records were part of the offset.

To test:

1. Download the MARC file with the broken record
2. Run the following import commands and compare the counted import numbers:
  a) all records
     perl misc/migration_tools/bulkmarcimport.pl -b -v -m MARCXML --file 2_records_1st_broken.xml
     1 MARC records done in 0.0421221256256104 seconds
  b) limit to 1 record in total
     perl misc/migration_tools/bulkmarcimport.pl -b -v -n 1 -m MARCXML --file 2_records_1st_broken.xml
     1 MARC records done in 0.0420489311218262 seconds
  c) limit to 1 record  offset 1 record
     perl misc/migration_tools/bulkmarcimport.pl -b -v -n 1 -o 1 -m MARCXML --file 2_records_1st_broken.xml
     :2: parser error : Opening and ending tag mismatch: leader2 line 2 and leader
  <leader2>00216nam a22001097a 4500</leader>

3. Apply the patch
4. Repeat steps in 2 and compare the results
  a) 2 MARC records done in 0.0405838489532471 seconds
  b) 1 MARC records done in 0.00948596000671387 seconds  *
  c) 1 MARC records done in 0.0294840335845947 seconds

*though this one was skipped

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:46 +00:00
Hammat Wele f5ff5269ac
Bug 41681: bulkmarcimport.pl reports an incorrect number of MARC records processed
When running the script bulkmarcimport.pl, the number of MARC records reported at the end of the execution does not match the actual number of records processed from the input file.

To test:

1. Download the joined MARC file containing 5 records.
2. Run the import command:
   ./misc/migration_tools/bulkmarcimport.pl -b --file=marc_file.mrc
3. Check the final script report
   ==> 11 MARC records done in 0.566349983215332 seconds
   Expected output should show 5 records
4. Apply the patch
5. Repeat step 2, 3
   ==> The correct number is shown

Signed-off-by: Janusz Kaczmarek <januszop@gmail.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:45 +00:00
Emily Lamancusa 0aa5820916
Bug 40492: Improvements to offline circ actions table
Update the pending offline circulation actions page to link each item
barcode directly to the item details, rather than the biblio record, and
open the links in a new tab.

Add a column to show the last seen date of the items being processed,
with a warning if the item's last seen date is more recent than the
offline circulation action date.

To test:
1. Upload a Koha Offline Circulation (koc) file
   (the file attached to this bug can be used if you are using a
   sandbox or KTD with the standard sample data)
   i. Go to Circulation > Upload offline circulation file
   ii. Upload the file
   iii. Click "Add to offline circulation queue"
2. Go to Circulation > Pending offline circulation actions
3. Click on the item barcodes in the table
--> Note that each barcode links to the biblio record, and navigates
    away from the page within the same tab.
4. Apply patch and restart_all
5. Return to (or refresh) the pending offline circulation actions page
   and click on the item barcodes again
--> The barcodes now open the item details page in a new tab
--> There is a new column for the item last seen date
6. Without processing the offline circulation actions, check in one of
   the items listed in the table
7. Return to the Pending offline circulation actions page
--> Confirm that the last seen date of the item you just checked in is
    highlighted in red with a warning icon, since its last seen date is
    newer than the offline transaction date
8. Hover over the warning icon
--> Confirm that a tooltip appears with an explanation of why the date
    is in red

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:44 +00:00
Baptiste Wojtkowski 7cda9f0648
Bug 39729: Add option -exact_days to filter_by_last_update
This patch adds an option -exact_days, to filter objects by an exact
number of days.

To test:
1 - Apply test patch
2 - Run tests -> they fail
3 - Apply patch
4 - Run tests -> they don't fail

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:43 +00:00
Baptiste Wojtkowski 501d4c5769
Bug 39729: Add tests
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:42 +00:00
Nick Clemens 2d73f3eb42
Bug 42714: (QA follow-up) Add comment with Perl doc for bash
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:40 +00:00
Martin Renvoize ad5fe0adff
Bug 42714: (QA follow-up) Add tests for cronlogaction and Koha::Script logging
t/db_dependent/Log.t
- Add subtest verifying cronlogaction() prefixes loginfo with \$0
  (the running script path), appends the info parameter, defaults the
  action to 'Run', and respects custom action values.

t/Koha/Script.t
- Assert that \$Koha::Script::_cron is false when the module is loaded
  without the -cron flag, confirming INIT/END blocks will not log.

t/Koha/Script_cron.t
- Assert that \$Koha::Script::_cron is true when the module is loaded
  with the -cron flag, confirming INIT/END blocks will log.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:39 +00:00
Martin Renvoize ece2d64e76
Bug 42714: (QA follow-up) Limit auto-logging to -cron scripts
The INIT/END blocks in Koha::Script fire for every consumer of the
module, including non-cron admin and migration tools. Logging those as
CRONJOBS when CronjobLog is enabled would pollute the action log with
entries for tools that are not scheduled jobs.

Introduce a package-level $_cron flag that is set by import() when the
-cron argument is passed (import() runs at compile time, before INIT
blocks execute, so the flag is in the correct state when INIT fires).
Guard both the INIT and END blocks behind this flag so only scripts that
explicitly declare themselves as cron scripts receive automatic logging.

Also update the import() POD to document the auto-logging behaviour.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:38 +00:00
Martin Renvoize 1652191b7f
Bug 42714: (QA follow-up) Use \$0 instead of caller() in cronlogaction
caller(1) was introduced to skip past Koha/Script.pm when cronlogaction
is called from the INIT/END blocks, but it breaks any direct caller that
invokes cronlogaction from within a Try::Tiny try/catch closure or a
callback sub - in those contexts caller(1) resolves to Try/Tiny.pm (or
the calling module) rather than the script itself.

\$0 is always the path of the running script regardless of call depth,
INIT/END phase, or enclosing closure, making it the correct choice here.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:37 +00:00
Nick Clemens 8f5c0abd09
Bug 42714: Adjust cronlogaction to use caller(1)
Since we are calling from within another module we need more caller information to
log the correct script

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:35 +00:00
Nick Clemens b9ec31fe7b
Bug 42714: Add missing use Koha::Script -cron to cron scripts
Add Koha::Script -cron to restrict_patrons_with_failed_notices.pl
and sftp_file.pl which used cronlogaction directly without importing
Koha::Script. Remove their now-redundant manual calls.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:34 +00:00
Nick Clemens 996aefd280
Bug 42714: Remove redundant manual cronlogaction calls from scripts
Remove start/end cronlogaction calls and unused C4::Log imports
from scripts that use Koha::Script, as these are now handled
automatically by the INIT/END blocks.

Scripts with mid-execution informational logging (plugins_nightly,
harvest_oai, fines, process_message_queue) retain their C4::Log
import and non-start/end cronlogaction calls.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:32 +00:00
Nick Clemens ecd056b21d
Bug 42714: Add automatic cronlogaction logging to Koha::Script
Add INIT and END blocks to Koha::Script that automatically call
cronlogaction on script start (with command line options) and
completion. This removes the need for scripts to manually handle
cron log entries.

This will mean that any script importing Koha::Script will be logged,
but I think the addition is beneficial.

To test:
1 - Ensure CronjobLog is enabled
2 - perl misc/cronjobs/fines.pl --test
3 - Confirm start and end are logged in action logs:
    http://localhost:8081/cgi-bin/koha/tools/viewlog.pl?src=&user=&modules=CRONJOBS&actions=&object=&info=&interfaces=&from=&to=&output=screen&basename=Export&report_name=&do_it=1
4 - Apply patch
5 - Repeat 2
6 - Reload the logs, confirm the new logs look the same
7 - Sign off

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:31 +00:00
Pedro Amorim 47a09c9182
Bug 16631: dbic changes
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:30 +00:00
Pedro Amorim c07c0f4a1f
Bug 16631: DBRev 26.06.008
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:29 +00:00
Pedro Amorim 92134fbec3
Bug 16631: (RM follow-up): Ensure syspref default on install
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:28 +00:00
Casey Conlin ba448064e2
Bug 16631: Bring back template. Fix test syntax.
RM amended: Add bug number to commit title

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:27 +00:00
Cameron E. Tidd 67a37a07c3
Bug 16631: Institute library limits
This patch implements the library_limit concept for Report/Reports. Staff with the correct permission and superlibrarians can set libaries to which the report should be visible, if the system preference is enabled.

Test plan:
Confirm Initial Settings
1. Log in to Koha staff with username: koha and password: koha and set your library as Centerville if it's not already.
2. Visit the Saved reports page, and create and save three reports: Report 1, Report 2, and Report 3. The SQL query for each report doesn't matter, it should just be something valid.
3. Visit the Patrons page and create two staff users: StaffReportLimitUser and StaffNoLimitPermission. Set their home libraries to Centerville and Fairfield, respectively.
4. Under Allow access to the reports module, ensure both users have report management permissions for Create, Delete, Execute.

Verify Patch Settings
5. Apply the patch and restart services. If using Koha Testing Docker (KTD): from the Koha repo directory, apply the patch (e.g. `git bz apply 16631`, or check out the patched branch with `git fetch && git checkout <branch>`), then run `ktd --shell` to enter the KTD shell and run `restart_all`. If the patched branch is already checked out, just enter the KTD shell and run `restart_all`.
6. Click Patrons in the top navbar to go to the Patrons page.
7. Search for "StaffReportLimitUser" in the patron search field.
8. Click StaffReportLimitUser to edit the patron record for this user.
9. Click More -> Set permissions to access the permissions screens for the StaffReportLimitUser.
10. In the row for Allow access to the reports module (reports), click Show details to view relevant permissions.
11. Confirm Manage Report Limits (manage_report_limits) is an option under Allow access to the reports module (reports).
12. Click the checkbox for Manage Report Limits to give this user this permission, and click Save.
13. Go to the System Preferences page (Click the More dropdown from the top nav bar, click Administration, click System Preferences).
14. Verify Reports is an option in the left side navigation bar, and that when clicked, you see the LimitReportsByBranch option.
15. Enable LimitReportsByBranch as described in step 14.
16. Go back to Saved reports as in Step 2.
17. Click Edit in the row menu for Report 1.
18. At the bottom of the Report 1 edit page, you should see a Library limitation select box.

Verify the Library Limitation Settings are Persisted
19. Choose Centerville from the select box and click Update SQL.
20. Reload the page, or navigate away from the page and go back to the edit screen for Report 1.
21. Verify that Centerville is still in the Library limitation select box.
22. Add Fairfield in the Library limitation select box and click Update and run SQL.
23. Go back to editing Report 1.
24. Confirm the Library limitation select box has both Centerville and Fairfield selected.
25. Duplicate Report 1.
26. Confirm the Library limitation select box has both Centerville and Fairfield selected.
27. Click Save Report.
28. Confirm the Library limitation select box has both Centerville and Fairfield selected.

Verify Reports are Limited Based on Library Limitations
29. Edit Report 1 to add Centerville in the Library limitation select box as the only choice and click Update SQL.
30. Edit Report 2 to add Fairfield in the Library limitation select box as the only choice and click Update SQL.
31. Report 3 should have no library limitations, so it should be visible to all libraries.
32. Go to Saved reports as in Step 2.
33. Confirm you do not see Report 2, and only Report 1 and Report 3.
34. In the upper right corner, click the drop down that says koha Centerville and click Set library to change your library.
35. Set your library to Fairfield.
36. Go to Saved reports as in Step 2.
37. Confirm you do not see Report 1, and only Report 2 and Report 3.

Verify Disabling LimitReportsByBranch Preference Shows All Reports
38. Disable LimitReportsByBranch preference as in steps 13 to 15.
39. Go to Saved reports as in Step 2.
40. Confirm Report 1, Report 2, Report 3 are visible.
41. Click to edit Report 1 and verify there is no Library limitation select box.

Verify Library limitations are Persisted When EnableFilteringReports is Toggled On or Off
42. Enable LimitReportsByBranch preference as in steps 13 to 15.
43. Ensure your library is set to Fairfield as in step 35.
44. Go to Saved reports as in Step 2.
45. Confirm Report 1 is not visible, and Report 2 and Report 3 are visible.
46. Click to edit Report 2 and verify the Library limitation select box is at the bottom of the page and that it has Fairfield selected.

Verify the Show all reports option shows all reports for users with the Manage Report Limits permission and superlibrarians
47. Go to Saved reports as in Step 2 and in the Show reports dropdown, select Show all reports, and click Apply filter.
48. Confirm the page reloads with all reports visible: Report 1, Report 2, and Report 3 should be visible.
49. Select Show my branch reports from the dropdown and click Apply filter.
50. Confirm Report 1 is not visible, and Report 2 and Report 3 are visible (you should only see reports for your branch, Fairfield).

Verify Report filters return the correct results
51. Go to Saved reports as in Step 2.
52. Verify you're viewing only your branch's reports in the filter.
53. Under Filter in the left sidebar, type "2" in the Keyword field and click Apply filter.
54. Confirm only Report 2 is visible.
55. Under Filter in the left sidebar, type "1" in the Keyword field and click Apply filter.
56. Confirm there are no reports visible, and the message "No saved reports match your criteria" is displayed.

Verify Library Limits column is available to superlibrarians and users with the Manage Report Limits permission
57. Go to Saved reports as in Step 2.
58. Click the Columns dropdown and ensure that the Library limits column is available to select for display. Select it.
59. Confirm that the Library limits column is now visible in the list of reports and that it shows the correct library limitations for each report (e.g., Report 1 should show 1 library limitation and Centerville (CPL) on hover, Report 2 should show 1 library limitation and Fairfield (FFL) on hover, and Report 3 should show no limitation).

Verify the Show all reports and Limit reports options are available to users that are not superlibrarian but have the Manage Report Limits permission
60. For the StaffReportLimitUser, add the permission for Manage Report Limits as described in steps 9 to 12. This will allow this user to see the Show all reports and Limit reports options.
61. Log out of the koha admin account. In the upper right corner, click the drop down that says koha Fairfield and click Log out.
62. Log in as StaffReportLimitUser, the previously created staff user with report permissions.
63. Confirm the staff user's library is Centerville in the upper right corner of the screen.
64. Go to Saved reports as in Step 2.
65. Confirm the filter option for Show reports is visible.
66. Click to edit Report 1.
67. Verify there is a Library limitation select box and that it can be interacted with.

Verify the Show all reports and Limit reports options are not available to users that are not superlibrarian and do not have the Manage Report Limits permission
68. Log out of the StaffReportLimitUser account.
69. Log in as StaffNoLimitPermission, the previously created staff user without the Manage Report Limits permission.
70. Confirm the staff user's library is Fairfield in the upper right corner of the screen.
71. Go to Saved reports as in Step 2.
72. Click the Columns dropdown and ensure that the Library limits column is not in the list of available columns to select for display.
73. Confirm the filter option for Show reports is not available.
74. Click to edit Report 2.
75. Verify there is no Library limitation select box at the bottom of the page.

[NEW] Verify library limits are auto-assigned and preserved correctly for non-permissioned users
76. Still logged in as StaffNoLimitPermission (Fairfield) from the previous section. Go to Saved reports and click New report. Enter "Limit Auto-Assign Test" as the report name and SELECT 1 as the SQL. Confirm there is no Library limitations select box visible on the form. Click Save.
77. Go back to Saved reports. Click Edit for Report 2 (limited to Fairfield). Add `-- test` as a trailing SQL comment and click Update SQL. This simulates a non-permissioned user saving an existing report that already has limits.
78. Go back to Saved reports. Click Edit for Report 3 (no library limits). Add `-- test` as a trailing SQL comment and click Update SQL. This simulates a non-permissioned user saving an existing report that has no limits.
79. Log out and log back in as StaffReportLimitUser (Centerville, has manage_report_limits).
80. Go to Saved reports. Use Show all reports if needed. Find "Limit Auto-Assign Test" and click Edit. Confirm the Library limitations select box shows Fairfield as the only selected library — the report was automatically limited to the creator's home library on create.
81. Find Report 2 and click Edit. Confirm the Library limitations select box still shows only Fairfield — the existing limit was preserved when the non-permissioned user saved the report.
82. Find Report 3 and click Edit. Confirm the Library limitations select box is empty — no limits were introduced when the non-permissioned user saved a report that had no existing limits.

Verify limited reports are not visible to users without permissions
83. While still logged in as StaffReportLimitUser from step 79, copy the URL for the edit page of Report 1.
84. Log out of the StaffReportLimitUser account.
85. Log in as StaffNoLimitPermission, the user without the Manage Report Limits permission.
86. Paste the copied URL for the edit page of Report 1 into the browser and navigate to it.
87. Confirm that you see an error message that says "This report is restricted and cannot be accessed by the current user." and that the page does not load the report details.

Run Unit Tests
88. Using Koha Testing Docker, in the Koha home directory run the command: ktd --shell to start the ktd shell.
89. Run command: reset_all to drop and recreate the koha database.
90. Run command prove t/db_dependent/Koha/Reports.t to run the test.
91. This file contains the unit tests for the reports_branches table and the new subtest `library limits enforcement on save and update`, which covers auto-assignment of branch limits on create and preservation of limits on update for non-permissioned users.
92. All tests should pass.
93. Run command prove t/Koha/Auth/Permissions.t
94. Permissions unit tests in this file were updated.
95. All tests should pass.
96. Run command prove t/db_dependent/Reports/Guided.t
97. The Guided report tests were updated to account for the new library limitations and permissions.
98. All tests should pass.

Signoff
99. Sign off if the steps above check out.

Sponsored-by: Ramapo Catskill Library System
Co-authored-by: Casey Conlin <casey.conlin@gmail.com>
Signed-off-by: Emmi Takkinen <emmi.takkinen@koha-suomi.fi>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:25 +00:00
Chris Nighswonger 60ee7fb527
Bug 41717: Bump PDF::Reuse and PDF::Reuse::Barcode versions
PDF::Reuse 0.43 fixes a TTFont/docProxy lifecycle bug that caused
prEnd() to crash when TrueType fonts were used.

PDF::Reuse::Barcode 0.09 is the current stable release.

Testing plan:

- Start ktd, note older perl modules versions under About Koha
- Apply patch
- ktd --shell
- sudo cpan -i PDF:Reuse; sudo cpan -i PDF:Reuse:Barcode
- Let cpan auto set itself up (not sure if sudo is needed above)
- `restart_all`
- Revisit About Koha; module versions should be 0.43 & 0.09 respectively

Signed-off-by: Eric Phetteplace <ephetteplace@cca.edu>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-28 14:32:22 +00:00
Pedro Amorim cfe7440dd7
Revert "Bug 34451: Add CSRF protection for REST API (cookie-auth only)"
This reverts commit 7716fac58d.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 16:06:36 +00:00
Andrii Nugged a445bb0ab3
Bug 42845: Add ILL config endpoint for Vue app
The ILL Vue app currently reads ILLModule and ILLPartnerCode through the system preferences service. That service requires parameters/manage_sysprefs, so a staff user with ILL permissions but without system preference administration permissions cannot load the ILL module.

Add an ILL-specific /ill/config REST endpoint protected by the ill permission, and switch the ILL Vue app to load its configuration from that endpoint. This follows the module-specific configuration endpoint pattern introduced by bug 33606 for the same manage_sysprefs problem in the ERM Vue app.

Security/compatibility considerations: the endpoint returns only allow-listed, non-secret ILL configuration values needed by the ILL Vue app. It does not expose the generic system preferences API, and /api/v1/sysprefs remains restricted to parameters/manage_sysprefs.

Regression test coverage: the new API test verifies that a staff user with the ill permission can read /ill/config, that a staff user without the ill permission cannot read it, and that the same ILL-only user still cannot access /api/v1/sysprefs.

Test plan:

1. prove -I. -It/lib t/db_dependent/api/v1/ill_config.t

2. prove -I. -It/lib t/db_dependent/api/v1/ill_users.t

Signed-off-by: David Nind <david@davidnind.com>

Amended-by: Jonathan Druart
chmod +x t/db_dependent/api/v1/ill_config.t

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:48 +00:00
Emily Lamancusa 769e565890
Bug 38361: (QA follow-up) Set required variable for input validation
Built-in validation checks weren't functioning because the variable
required.sex was not getting set. Ensure that variable gets set, and
remove manual validation code that is no longer needed.

Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:47 +00:00
Bo Gustavsson 9d40cdee7f
Bug 38361: Gender field does not include a Required if the field is set to required in PatronSelfRegistrationBorrowerMandatoryField
Solves this bug and enhances the user experiance by replacing the old radio buttons with a less-confusing dropdown menu.

Test plan:

1. Open the setting "PatronSelfRegistrationBorrowerMandatoryField", find Gender(sex) and mark it as not required.
2. Go to the self registration form. No 'Required' label under gender.
3. Go back to the setting  "PatronSelfRegistrationBorrowerMandatoryField", find Gender(sex) and mark it as required.
4. Go to the self registration form. The 'Required' label is there.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:46 +00:00
Slava Shishkin ac31d8ae25
Bug 41227: Respect SuspendHoldsIntranet on patron holds page
Test plan:
1. Set SuspendHoldsIntranet to "Don't allow"
2. Place a hold for a patron
3. Open the patron holds table
4. Confirm the Suspend button is visible in the Suspend column
5. Apply the patch
6. Reload the patron holds table
7. Confirm the Suspend button is no longer visible
8. Set SuspendHoldsIntranet to "Allow"
9. Reload the patron holds table
10. Confirm the Suspend button is visible again

Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:45 +00:00
Fridolin Somers ea185b6833
Bug 43098: Typo edit_formid in guided_reports_start.tt
Typo in edit SQL when authorised values category is wrong : 'op=edit_formid='

Test plan:

1) Create a new authorised values category WRONG
2) Create a new SQL report with : SELECT * FROM biblio WHERE title LIKE <<val|WRONG>>
3) Save the report
4) Delete authorised values category WRONG
5) Run the SQL report
6) You see message 'The authorized value category (WRONG) you selected does not exist.'
7) Click on 'Edit SQL'
=> You comme back to editing the correct SQL report

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:44 +00:00
Lari Taskula d0b458af58
Bug 42298: Automatic renewals should only respect the "No automatic renewals before" rule
To test:
1. prove t/db_dependent/Circulation.t
2. Observe success

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:43 +00:00
Lari Taskula 54ac795b76
Bug 42298: Add a failing test
To test:
1. prove t/db_dependent/Circulation.t
2. Observe the following failures

t/db_dependent/Circulation.t .. 17/87
    #   Failed test 'Bug 42298: No autorenew before rule is respected'
    #   at t/db_dependent/Circulation.t line 1134.
    #          got: '0'
    #     expected: '1'

    #   Failed test 'Bug 42298: No autorenew before rule is respected'
    #   at t/db_dependent/Circulation.t line 1135.
    #          got: 'too_soon'
    #     expected: 'auto_renew'

    #   Failed test 'soonest_renew_date is not returned because this issue can be renewed'
    #   at t/db_dependent/Circulation.t line 1136.
    #          got: '2026-04-21T23:59:00'
    #     expected: undef

    #   Failed test 'No renewal before is 99, patron opted out of auto_renewal so can renew'
    #   at t/db_dependent/Circulation.t line 1141.
    #          got: '0'
    #     expected: '1'
    # Looks like you failed 4 tests of 124.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:42 +00:00
Jonathan Druart 34da25f751
Bug 42632: Fix "Manage bundle" after filters are shown/hidden
If "Show/Hide filters" link is clicked, the "Manage bundle" buttons do not longer work.

Test plan:
Edit a record, set leader 7=collection
In the holding list click "Manage bundle"
Click "Show filters"
Click one of the "Manage bundle" button
=> Without this patch nothing happens
=> With this patch applied the table displays the nested table with the
bundle details

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Anni <anni.maki-mantila@turku.fi>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:41 +00:00
Pedro Amorim d7ff63f20b
Bug 41898: DBRev 26.06.00.007
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:39 +00:00
Paul Derscheid 1f45a43f1d
Bug 41898: (QA follow-up) Skip clash detection when collecting a booking
Transitioning a booking to 'issued' no longer skipped clash detection,
and Item::check_booking treats any live checkout on the item as a
clash. Switching an on-site checkout of a booked item to a regular
checkout (SwitchOnSiteCheckouts) therefore crashed AddIssue with an
unhandled Koha::Exceptions::Booking::Clash; collection could fail the
same way whenever the biblio's bookable item pool had shrunk since the
booking was placed.

- short-circuit no-op stores before validation; circulation issues
  redundant status writes that must not re-trigger the checks
- rename _is_final_status_transition to _skip_clash_detection and also
  skip when transitioning to 'issued' from an active status, as long
  as the booked period and item are unchanged
- keep validating period/item changes and reactivations of terminal
  bookings
- cover the on-site switch in Circulation.t and the store() semantics
  in Booking.t

Test plan:
1. Enable OnSiteCheckouts and SwitchOnSiteCheckouts
2. Place a booking starting today for a patron
3. Check the booked item out to that patron from Circulation > Check
   out with 'On-site checkout' ticked. Do NOT use the bookings page's
   'Transform to checkout' button: it submits a fixed field set that
   does not carry the on-site flag (Bug 36789), so the checkout would
   silently be a regular one and step 4 could not reproduce the switch
4. The booking shows 'Issued'. Scan the same barcode for the same
   patron again to switch the on-site checkout to a regular one; this
   crashed with Koha::Exceptions::Booking::Clash before the patch and
   now succeeds with the booking still 'Issued'
5. prove t/db_dependent/Koha/Booking.t t/db_dependent/Circulation.t

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:38 +00:00
Paul Derscheid 15dfbc2f08
Bug 41898: (QA follow-up) Only list uncollected bookings on the pending list
The pending bookings page lists items that have not been collected yet,
but the status filter added when the server-side active pre-filter was
removed included 'issued' bookings, so bookings reappeared on the list
after the item had been checked out.

- restrict the status filter to 'new'

Test plan:
1. Place a booking starting today and open Circulation > Bookings to
   collect; the booking is listed
2. Check the item out to the booking patron; reload the list and
   confirm the booking is gone

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:37 +00:00
Martin Renvoize 91556ada91
Bug 41898: Load additional-filters.js on the circulation page
The rewrite of js/tables/bookings.js in this patchset builds on
AdditionalFilters, which is provided by js/additional-filters.js. The
patron details page loads that asset but the circulation page did not,
so the bookings tab on circ/circulation.pl died with 'AdditionalFilters
is not defined' and the table never initialised.

Test plan:
1) Check out to a patron with bookings and open the Bookings tab on
   the checkout page; confirm the table renders and no JS error is
   logged in the console

Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:36 +00:00
Martin Renvoize 40b9c8e29c
Bug 41898: Update tests for new booking status lifecycle
- Bookings.t: rewrite filter_by_active to verify status-based filtering;
  new/issued are active, cancelled/completed are not; past-window 'new'
  bookings (uncollected) remain active
- Booking.t: checkout relation uses 'issued'; old_checkout uses 'completed';
  clash detection covers issued transition; notice test updated
- Circulation.t: AddIssue sets 'issued'; AddReturn sets 'completed'; plan
  expanded to cover the full checkout → return lifecycle

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:35 +00:00
Martin Renvoize bf8237ae54
Bug 41898: Fix callers of bookings API after removing server-side active filter
The bookings REST controller previously applied filter_by_active (status IN
'new','issued') as a server-side pre-filter. Removing that pre-filter means
callers that relied on it must now supply an explicit status filter.

- pendingbookings.tt: add status filter to additional_filters so the
  pending-bookings-to-collect page still shows only new+issued bookings
- place_booking.js: correct status values from the obsolete 'pending','active'
  (never valid in the schema) to the correct 'new','issued'

Test plan:
1. Navigate to Circulation > Bookings to collect.
   - Confirm only bookings with status "New" or "Issued" are listed.
   - Complete a booking (check out then return the item) and confirm it
     disappears from the list.
2. On any biblio with multiple bookings, open the Place booking modal.
   - Create overlapping bookings where one is completed or cancelled.
   - Confirm the modal does not treat completed/cancelled bookings as active
     conflicts when checking availability.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:34 +00:00
Martin Renvoize c7673a9d6a
Bug 41898: Introduce 'issued' status and update booking display/filtering
Implements a 4-state booking lifecycle: new → issued (on checkout) →
completed (on return), plus cancelled at any point.

Backend changes:
- AddIssue sets booking status to 'issued' (was incorrectly 'completed')
- AddReturn transitions 'issued' → 'completed' via booking_id on the issue
- filter_by_active updated to status-based: status IN ('new', 'issued');
  previously it filtered by date range — active/inactive now reflect booking
  lifecycle rather than the current date relative to start/end dates
- _is_final_status_transition reverted to only skip clash detection for
  terminal statuses 'cancelled' and 'completed'; 'issued' is active and does
  not bypass clash detection
- GET /api/v1/bookings no longer pre-filters to active bookings server-side;
  callers must supply an explicit status filter if they need active-only

Display changes:
- Patron bookings tab: replaces the date-based expired/active toggle with
  per-status filters (completed, cancelled) using the AdditionalFilters
  framework; active bookings (new/issued) are shown by default
- Biblio bookings list: same status-based filters; removes the date-based
  expired filter that was still present
- Status column is always visible on both views (was previously hidden on
  the patron tab unless expired bookings were shown)
- Status badges: new=bg-success, issued=bg-info, completed/cancelled=bg-secondary
- Timeline isActive check corrected to ['new','issued']
- patron-detail-tabs.inc: rename expired_bookings_count → closed_bookings_count
  to reflect status-based (not date-based) counting
- Filter button colour fixed to use $link-color so both views are consistent

Test plan:
1. Enable the EnableBookings system preference.
2. Create a biblio with at least one bookable item.
3. Place a booking for a patron via the biblio's Bookings tab.
   - The booking should appear with status badge "New" (green).
4. Check out the booked item to that patron (Circulation → Check out).
   - Reload the Bookings tab; the booking should now show "Issued" (blue).
5. Return the item (Circulation → Check in).
   - Reload the Bookings tab; the booking should now show "Completed" (grey).
6. Place a second booking and cancel it from the Bookings tab Actions menu.
   - The booking should show "Cancelled" (grey), with the reason if provided.
7. On the biblio Bookings tab:
   - Completed and cancelled bookings are hidden by default.
   - Clicking "Include completed" shows completed bookings.
   - Clicking "Include cancelled" shows cancelled bookings.
   - Both can be toggled independently.
   - The Status column is always visible.
8. Navigate to the patron's detail page → Bookings tab.
   - Active bookings (new/issued) are shown by default.
   - Completed and cancelled can be toggled independently using the same
     filter buttons.
   - The Status column is always visible.
9. Confirm via the REST API that GET /api/v1/bookings (no filters) returns
   bookings of all statuses, not just active ones.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:33 +00:00
Martin Renvoize fde2bc5680
Bug 41898: Automated Schema Update
Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:32 +00:00
Martin Renvoize 4633d37931
Bug 41898: Add atomicupdate and update schema for 'issued' booking status
Extends bookings.status enum to ('new', 'cancelled', 'issued', 'completed').

Migrates existing data intelligently: rows with an active checkout in the
issues table (via booking_id) are updated to 'issued'; rows whose item has
already been returned retain 'completed', which is already correct under the
new scheme where 'completed' means the booking was fully fulfilled and the
item returned.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:31 +00:00
Brendan Lawlor 5f99bbb772
Bug 41879: (QA follow-up) Add pod for new sub _pickup_valid_for_item
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:30 +00:00
Lucas Gass ada88e3a83
Bug 41879: (follow-up) Add a test
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:29 +00:00
Lucas Gass 7d1521b943
Bug 41879: (follow-up) Bail with error code when no items on target bib
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:28 +00:00
Lucas Gass 9ca6d01f22
Bug 41879: (follow-up) Add helper function to avoid repeating code
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:27 +00:00
Lucas Gass 7f331590fa
Bug 41879: update move_hold unit tests
Signed-off-by: Eric Phetteplace <phette23@gmail.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:26 +00:00
Lucas Gass 47dd1f1844
Bug 41879: Disallow moves to invalid pickup locations for next available holds
Signed-off-by: Eric Phetteplace <phette23@gmail.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:25 +00:00
Lucas Gass 560efd018b
Bug 41879: Disallow moves to invalid pickup locations for items
To test:

1. Set ReservesControlBranch is set to 'item's home library'
2. Set the deafult 'Holds and bookings policies by item type' for the itype BK to allow holds from patrons from any library, but pickup at CPL only
3. Have a bib owned by another library ( I used MPL ), and bib owned by CPL. Both are the item type BK, and each has one item on the bib.
4. Place a hold for a patron on bib A for pickup at MPL
5. Move the hold to bib B
6. Note that the hold moves successfully, and is still set for pickup at MPL
7. Login and try to trigger the hold, nothing happens
8. APPLY PATCH, restart_all
9. Try spets 3 - 6 again. This time on step 6 you should see a message that the hold cannot be moved.

Signed-off-by: Eric Phetteplace <phette23@gmail.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:24 +00:00
Jonathan Druart 4c3e4b8bd7
Bug 42396: Adjust 'use' statements
Always include the plural version, not the singular one (the plural
imports the singular)

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:23 +00:00
Olivia Reynolds 0670379b76
Bug 42396: Remove C4::Stats::UpdateStats and delete module
This patch replaces all calls to C4::Stats::UpdateStats with the wrapped
function from the Koha namespace. As this is the last function in C4::Stats, the
module is also deleted.

Several tests depended on transitive includes to Koha::Statistics through
C4::Stats. I've identified uses of that module by searching, and added the
necessary use in each case. I've run `prove -r t xt` with this patch applied and
didn't hit any relevant errors.

Test plan:
1. Inspect the patch, and verify each changed call maintains the same parameters.
2. Apply patch.
3. Prove the following files:
   - t/db_dependent/Accounts.t
   - t/db_dependent/Circulation.t
   - t/db_dependent/Koha/Account.t
   - t/db_dependent/Koha/ILL/Request.t
   - t/db_dependent/Koha/Item.t
   - t/db_dependent/Koha/Pseudonymization.t
   - t/db_dependent/Koha/Recalls.t

Sponsored-by: Open Fifth

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:22 +00:00
Emily Lamancusa 9cad4c5c8d
Bug 20573: Make NotifyBorrowerDeparture's date math consistent with MembershipExpiryDaysNotice
To test:
1. Set both system preferences NotifyBorrowerDeparture and
   MembershipExpiryDaysNotice to 10 days
2. Create or edit a patron account with the following settings:
    - expiry date is exactly 10 days in the future
    - add an email address on the patron account
    - set messaging preferences to receive email notices for Patron
      account expiry
3. Run the membership expiry cron job to generate a notice:
   ktd --shell
   perl misc/cronjobs/membership_expiry.pl -c
4. Check notices tab on patron account
--> A membership expiry notice has been queued for the patron
5. Click back to the patron details
--> Note that there is no alert that the patron account will be expiring
    soon
6. Apply patch and restart_all
7. Refresh the patron details page
--> You should now see an alert that the account will be expiring soon

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 15:53:18 +00:00
Emmi Takkinen fdf37a2630
Bug 42699: Display Koha error message when attempting to save non-unique barcode in Edit serials
When you edit an already received serial from Serial collection page
using Edit serials and edit items buttons, and try to save a barcode
that already exists on another item, saving leads to Error 500.
There is no barcode validation for editing serials, this patch adds it.

To test:
1. Go to serials module.
2. Search for a serial that is not expired to edit and click on the title.
3. Go to Serial collection from the menu.
4. Use the checkboxes to mark an already received (eg. usually older,
arrived, not expected) serial and click Edit serials.
5. Click on Edit item.
6. Replace the barcode with a unique barcode that is already in use
and hit save.
=> Error 500 is raised.
7. Apply this patch and restart services.
8. Attempt to save serial with already used barcode again.
=> A standard Koha error message is displayed stating that barcode
must be unique.

Sponsored-by: Koha-Suomi Oy
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:13:09 +00:00
Martin Renvoize 7355dade3b
Bug 43071: Isolate holds_per_day-sensitive tests from ambient circulation rules
t/db_dependent/Holds.t, t/db_dependent/ILSDI_Services.t, and
t/db_dependent/api/v1/holds.t all fail if a site has a global
'Holds Allowed (Daily)' circulation rule configured, because
C4::Reserves::CanItemBeReserved checks holds_per_day before
holds_per_record/reservesallowed, so an ambient daily-cap rule
returns tooManyReservesToday and masks the status these tests
actually exercise (tooManyReserves, noReservesAllowed, OK, or the
API's hold_limit override). The affected subtests never set
holds_per_day themselves, so they unintentionally depend on the
database having no such rule configured.

Test plan:
1. On a fresh db, set a global 'Holds Allowed (Daily)' circulation
   rule (Administration > Circulation and fine rules) to 1.
2. prove t/db_dependent/Holds.t t/db_dependent/ILSDI_Services.t
   t/db_dependent/api/v1/holds.t
   => fails before this patch, passes after.
3. Remove the circulation rule and re-run the same tests.
   => still passes (no regression for the default/unconfigured case).

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:13:08 +00:00
Paul Derscheid fd3addf35f
Bug 38358: (QA follow-up) Return opened panel from dateChanged
Builds on the panel-detection logic by having dateChanged report which
panel it opens, so onChange can adjust that panel's "To date" picker
directly without waiting on the slide animations to settle.

- dateChanged returns "showHoliday" when editing, "newHoliday" when adding
- onChange maps that to datecancelrange/dateofrange, in place of the
  setTimeout and the :visible check

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:13:07 +00:00
Emmi Takkinen aaa3954c8f
Bug 38358: Do not allow selecting past dates when editing existing holidays "To date" input
When you are editing holidays "To date" inputs calendar doesn't follow the
"From date" date. Instead calendar offers you today's date/month and
you must click calendar forward to the right date/month. This patch
adds check to determine if user is adding new holiday or editing
existing one and adjusts the code to disable past dates counting from
selected "From date" in both cases.

To test:
1. Navigate to calendar tool (Tools->Calendar).
2. Add new holiday and select "To date" for it.
=> Note that you are unable to select dates past from "From date".
3. Edit existing holiday and select "To date" for it.
=> Note that "To date" input calendar allows selecting dates past
"From date".
=> If you chosed holiday from incoming month, note that "To date"
calendar still opens up to this month.
4. Apply this patch, restart services if needed.
5. Edit existing holiday again.
=> Note that now "To date" inputs calendar works the same way as
it is working when your adding a new holiday.

Sponsored-by: Koha-Suomi Oy
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:13:06 +00:00
Kyle M Hall 430281a59a
Bug 43101: search_for_data_inconsistencies.pl inlines every biblionumber in the catalog as an OR chain
Queries that use OR instead of IN are less efficient and can lead to
situations where the query takes a very long time to complete due to
row level scanning. We found one instance where they query had been
running for over 10 days!

This patch makes ids() return a subquery instead of a list of results,
and switches the callers to using IN. The database now resolves the set
itself and the queries stay a constant size no matter how large the
catalog is. This is super efficient because the subquery doesn't even
execute as a separate query since biblionumber is the primary key!

Test Plan:
1) Apply the unit tests patch
2) prove t/db_dependent/Koha/Database/DataInconsistency.t
3) Note the ids() subtest fails
4) Apply this patch
5) prove t/db_dependent/Koha/Database/DataInconsistency.t
6) Note all tests pass!
7) Run misc/maintenance/search_for_data_inconsistencies.pl
8) Note the output is the same as before the patch
9) Bonus: Re-run it with DBIC_TRACE=1 and note the queries now use
   IN (SELECT ...) instead of one OR clause per bib!

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:13:05 +00:00
Kyle M Hall 94695b74a1
Bug 43101: Add unit tests
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:13:04 +00:00
Matt Blenkinsop a5d12147f6
Bug 43121: Convert all groups to use explicit labels
Test plan:
1) Install another language
2) Navigate to a vendor record
3) The group names should translate correctly and display in the split screen view

Signed-off-by: Michaela <michaela.sieber@kit.edu>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:13:03 +00:00
Matt Blenkinsop 53e9d026ce
Bug 43121: Use an explicit label for group and tab names rather than a translated string
Signed-off-by: Michaela <michaela.sieber@kit.edu>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:13:02 +00:00
Saiful Amin b98a0c87fb
Bug 42861: System warning about missing encryption_key on a fresh instance
This patch updates the koha-create command to generate the random encryption key
using pwgen with the number of characters set to 32.

Test plan:
1. In ktd, create a new instance:
    - sudo koha-create --create-db test1
    - Check the /etc/koha/sites/test1/koha-conf.xml file and verify that the encryption_key
    is having the placeholder text '__ENCRYPTION_KEY__'

2. Apply the patch and copy the new koha-create command
    - git bz apply 42861
    - sudo cp /kohadevbox/koha/debian/scripts/koha-create /usr/sbin/koha-create

3. In ktd, create a new instance:
    - sudo koha-create --create-db test2
    - Check the /etc/koha/sites/test2/koha-conf.xml file and verify that the encryption_key
    is generated and has 32 characters random string.

4. Sign-off

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:13:01 +00:00
Matt Blenkinsop 7b199653ed
Bug 43119: Restore item and hold links to the log viewer
INSERT INTO action_logs (timestamp, user, module, action, object, info, interface) VALUES
    (NOW(), 5, 'CIRCULATION', 'ISSUE', 5,
     '{"issue":1,"branchcode":"CPL","itemnumber":1,"confirmations":["TOO_MANY"],"forced":["DEBT"]}', 'intranet'),
    (NOW(), 5, 'CIRCULATION', 'RETURN', 5,
     '1', 'intranet'),
    (NOW(), 5, 'HOLDS', 'CREATE', 7,
     '{"hold":7,"biblionumber":1,"itemnumber":1,"confirmations":["HIGHHOLDS"],"forced":["HOLD_POLICY_OVERRIDE"]}', 'intranet');

Test plan:
1) Without the patch applied, run the populate SQL above against the kohadev database.
2) Go to Tools > Log viewer
3) Under Modules, select only Circulation - leave the date range empty and click Submit.
4) Observe the ISSUE row's Info column shows the raw JSON payload as plain text, and the RETURN row shows the bare itemnumber "1" as plain text - neither is a clickable link to the item.
5) Go back, select only the Holds module, click Submit, and observe the CREATE row's Info column likewise shows raw JSON with no hold/item links.
6) Apply the patch.
7) Run yarn build && restart_all, then hard refresh the browser.
8) Repeat step 3.
9) Confirm the ISSUE row's Info column now shows a link reading "Item <barcode> CPL" (e.g. Item 3999900000001 CPL) - the branchcode comes from the JSON payload - and clicking it opens the item page. Override information is also displayed correctly
10) Confirm the RETURN row's Info column now shows a link reading "Item <barcode>" (no branchcode) opening the same item's detail page - verifying the bare-itemnumber payload path.
11) Repeat step 5. Confirm the CREATE row now shows "Hold 7 on biblio 1" linking to reserve/request.pl?biblionumber=1, followed by " - Item 1" linking to the item detail page. Override information is also displayed correctly

Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:13:00 +00:00
Samuel Sowanick 1254997e69
Bug 42928: Update log viewer action terminology - Return to Check-in
The log viewer results table uses "Return" for "Check-in" actions.

See the terminology list:
https://wiki.koha-community.org/wiki/Terminology#C

Test plan:
1. Check out an item to a patron, then check it back in again.
2. Go to the log view (Tools > Additional tools > Log viewer).
3. Click "Submit".
4. Note that the action column for the results shows "Return".
5. Apply the patch.
6. Refresh the log viewer page.
6. Note that the action column now correctly shows "Check-in".

Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:59 +00:00
Tomás Cohen Arazi 7716fac58d
Bug 34451: Add CSRF protection for REST API (cookie-auth only)
This patch adds Koha::REST::Plugin::CSRF which enforces CSRF token
validation on state-changing requests (POST/PUT/DELETE/PATCH) when
a CGISESSID cookie is present.

Requests using OAuth2 or HTTP Basic Auth are not affected since they
don't use session cookies and are inherently immune to CSRF.

The client-side already sends the CSRF-TOKEN header via http-client.js
(added in Bug 34478). This patch adds the server-side enforcement.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/Koha/REST/Plugin/CSRF.t
=> SUCCESS: Tests pass!
3. Run:
   k$ prove t/db_dependent/api/v1/csrf.t
=> SUCCESS: Tests pass!
4. Tests cover:
   - GET/HEAD/OPTIONS requests bypass CSRF check
   - POST/PUT/DELETE without CGISESSID bypass CSRF check
   - POST with CGISESSID but no CSRF-TOKEN returns 403
   - POST with CGISESSID and invalid CSRF-TOKEN returns 403
   - POST with CGISESSID and valid CSRF-TOKEN passes through
   - PUT and DELETE enforcement
5. Sign off :-D

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:58 +00:00
Tomás Cohen Arazi 058c476bc7
Bug 42732: Migrate catalogue/detail.tt addToBundle $.post to api-client.js
Replace direct $.post() to /api/v1/items/{id}/bundled_items with
ItemAPIClient.bundled_items.add(), which uses http-client.js and
automatically includes the CSRF token header.

The caller passes return_response: true because it needs access to
the raw response status code and body for fine-grained error handling
(different user-facing messages for 409/already_bundled, 409/checked_out,
409/reserved, 404/not_found, 400/failed_nesting). http-client.js
default behavior throws a plain Error on non-2xx responses, losing
the status code and error_code fields the UI needs.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:57 +00:00
Tomás Cohen Arazi e2b0883811
Bug 42731: Migrate modal-claims.inc $.post to api-client.js
Replace direct $.post('/api/v1/return_claims', ...) with the new
ClaimAPIClient, which uses http-client.js and automatically includes
the CSRF token header.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:56 +00:00
Tomás Cohen Arazi f5a1f41d35
Bug 42730: Migrate place_booking.js $.post to api-client.js
Replace direct $.post() and $.ajax() calls to /api/v1/bookings with
the new BookingAPIClient, which uses http-client.js and automatically
includes the CSRF token header.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:55 +00:00
Pedro Amorim f5e8cdb399
Bug 43139: Sort by ID DESC instead of ID ASC
Test plan:
1) Enable ILLModule
2) Ensure at least 2 ILL requests exist
3) Go to <staff_url>/cgi-bin/koha/ill/home
4) Confirm the most recent ILL request (highest ID) shows first

Signed-off-by: Michaela <michaela.sieber@kit.edu>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:54 +00:00
Aleisha Amohia 33f4336974
Bug 33364: Prevent Holds to Pull from including items with pending recalls
This patch checks for waiting or in_transit items allocated to recalls, and excludes them from the Holds to Pull report.

To test:

1. Enable UseRecalls and set the recalls-related circulation rules
2. Check out an item to Patron A
3. Place an item-level hold on the item for Patron B
4. Log into the OPAC as Patron C and search for the item. Place an item-level recall on the item.
5. Go back to the staff interface and check in the item. This should prompt a pop-up to confirm the recall as waiting. Confirm the recall as waiting.
6. Go to Circulation -> Holds to pull.
7. Notice that even though the item is allocated for a recall, the hold is showing. It should not be showing in this page.

8. Apply the patch, restart services and refresh the page

9. The item allocated to a recall should no longer be showing in the Holds to pull report

10. Go to the record detail page and go to the Recalls tab
11. Revert the recall from its waiting status
12. Go back to the Holds to pull page. The record will now show for the hold.
13. Go to the Circulation -> Check in page
14. Go to the menu in the top-right of the page and click Set library
15. Change your library to a different one
16. Check in the item to confirm the recall as waiting and trigger a transfer
17. Go back to the Holds to pull page
19. The item should no longer show on the Holds to pull report

20. Confirm tests pass t/db_dependent/Koha/Holds.t

Sponsored-by: Catalyst IT
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:53 +00:00
Jonathan Druart f2cca7c630
Bug 43110: (bug 28308 follow-up) Disable checkboxes on the detail view
Checkboxes must be disabled on the detail page as we cannot submit the
changes.

Test plan:
1 - Edit a patron, set a value for advance notices, do not check any transports
2 - View the details for the patron:
    http://localhost:8081/cgi-bin/koha/members/moremember.pl?borrowernumber=5
3 - Note the checkboxes are disabled

QA: The logic is a bit weird as messaging_form_inactive is not set if the
pref is off, but the table is not displayed in this case.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:52 +00:00
Lucas Gass 3fac313a53
Bug 42930: Replace title with borrowertitle
To test:
1. Have some overdues
2. Go to circ/overdue.pl
3. Notice the the 'Patron' column the name is prefixed with the biblio title
4. This is meant to be the borrowertitle
5. APPLY PATCH, restart_all
6. Now you should see only the borrower/parton name and info.
7. Edit the patron and add a " Salutation"
8. In the overdue report this should now display

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:51 +00:00
Lucas Gass 35bbb013ba
Bug 42166: Check for 008 before adding the date to it
To test:
1. Edit or create a framework that does not require the 008.
2. Make or edit a record in that framework without a 008.
3. Turn on EnableAdvancedCatalogingEditor, and make sure you are using the advanced editor.
4. Go to the record from step 2 and click "Edit as new (duplicate)"
5. The record won't load in the advanced editor.
6. APPLY PATCH
7. Try again, this time the record should load when using "Edit as new (duplicate)".

Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:50 +00:00
Jonathan Druart 96de51c53d
Bug 42981: Prevent toLowerCase to fail when comparing int and string
If stateSave is turned on and one of the option has a value with _id set
to an Integer, toLowerCase is not available and a JS error is raised:
 Uncaught (in promise) TypeError: this._id.toLowerCase is not a function

Test plan:
1.) Admin - table settings: cgi-bin/koha/admin/columns_settings.pl#eholdings|packages
Activate checkbox "Save search state on page change"

2.) Go to ERM : cgi-bin/koha/erm/eholdings/local/packages
And create two packages:
 - One package with My Vendor assigned.
 - One package without a vendor.

3.) Filter the package results list by My Vendor. --> List is filtered to one package

4.) Leave the page, for example by navigating to Agreements.
Return to the Packages list and you will see the issue  (See also attached Screenshot)

=> Without the patch
The package results list becomes unresponsive / hangs after returning to the page.

=> With the patch
The package results list should load and display the filtered results correctly.

Signed-off-by: Michaela Sieber <michaela.sieber@kit.edu>
Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:49 +00:00
Pedro Amorim 959ba68ad0
Bug 42489: DBRev 26.06.00.005
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:48 +00:00
Tomás Cohen Arazi 3edd575263
Bug 42489: Pass unallocated mode to real-time holds queue
When RealTimeHoldsQueueUnallocated is enabled, the background job
passes unallocated => 1 and delete => 0 to update_queue_for_biblio.
This skips deleting existing queue entries and only processes holds
not already allocated, matching the --unallocated behavior of
build_holds_queue.pl.

Test plan:
1. Apply patches
2. Run:
   $ ktd --shell
  k$ prove t/db_dependent/Koha/BackgroundJob/BatchUpdateBiblioHoldsQueue.t
=> SUCCESS: Tests pass!
3. Tests verify:
   - delete => 1 and no unallocated when pref is off
   - delete => 0 and unallocated => 1 when pref is on
4. Sign off :-D

Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Sponsored-by: The Main Library Alliance <https://www.mainlib.org/>
Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:47 +00:00
Tomás Cohen Arazi 5345fbf908
Bug 42489: Add RealTimeHoldsQueueUnallocated syspref
Adds a new system preference to control whether the real-time
holds queue processes only unallocated holds (faster, additive)
or rebuilds the entire queue for the affected biblio (current
behavior, more thorough).

Default is off (preserving current behavior).

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
  k$ perl installer/data/mysql/updatedatabase.pl
=> SUCCESS: New preference added
3. Verify RealTimeHoldsQueueUnallocated appears in
   Administration > System preferences > Circulation
4. Sign off :-D

Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:46 +00:00
Pedro Amorim 2431af105d
Bug 40445: DBRev 26.06.00.005
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:45 +00:00
Martin Renvoize 09642be4db
Bug 40445: (follow-up) Add default cud- op value to cashup selected form
The two-phase cashup workflow added a 'Cashup selected registers' modal
whose form's op hidden input had an empty value, populated at runtime by
JavaScript. xt/find-missing-op-in-forms.t parses templates statically and
flagged the empty op value as not starting with 'cud-' (see bug 34478).

Set a sensible default of 'cud-cashup_start'; the JS click handlers still
overwrite the value before submission, so behaviour is unchanged.

Test plan:
1. prove xt/find-missing-op-in-forms.t => now passes

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:44 +00:00
Martin Renvoize a4ba11af06
Bug 40445: (follow-up) Fix TT in Tag issue
Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:43 +00:00
Pedro Amorim f1766a206b
Bug 40445: (RM follow-up): Fix POD copy paste
Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:42 +00:00
Martin Renvoize 53d498e145
Bug 40445: (QA follow-up) Show cashup summary modal when completing from registers.pl
When a single-register cashup is completed from the Cash summary page
(registers.pl), pos/register.pl redirected back to registers.pl without
the '#cashup-<id>' hash, so the hash-watcher in registers.tt never
triggered the cashup summary modal. Completing the same cashup from the
Transaction history page (register.pl) worked because that branch
already appended the hash.

Append '#cashup-<id>' to the registers.pl redirect URL too, so both the
'Record cashup' (quick) and 'Complete cashup' (two-phase) actions on the
Cash summary page open the summary modal on return.

Test plan:
1. Have at least one register with cash transactions.
2. From Cash summary (registers.pl), click 'Record cashup' on a register
   with no cashup in progress; complete it.
   => On return, cashup summary modal opens automatically.
3. Start a two-phase cashup; from Cash summary click 'Complete cashup'
   and confirm.
   => On return, cashup summary modal opens automatically.
4. From Transaction history (register.pl), complete a cashup as before.
   => Modal still opens (regression check).

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:41 +00:00
Martin Renvoize c51f4b7cca
Bug 40445: (QA follow-up) Add missing [% USE raw %] in cashup_summary.inc
The cashup summary modal include uses the \$raw filter on
Koha.CSPNonce, but never declared [% USE raw %]. The QA template
tester (which processes templates through a fresh Template::Toolkit
instance) reports "valid_template: filter not found" because the
\$raw plugin is not loaded.

Add [% USE raw %] at the top of the include, matching the convention
used by other includes that emit nonce attributes (e.g.
includes/str/cataloging_additem.inc).

Test plan:
1. Run qa-test-tools against the bug branch -- cashup_summary.inc
   passes valid_template.
2. Open the cashup summary modal -- the <style> tag still receives
   a CSP nonce and the embedded styles render correctly.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:40 +00:00
Martin Renvoize 7798332412
Bug 40445: (follow-up) Show actual cash removed alongside expected on discrepancy
The cashup summary modal labelled the per-payment-type cash row "Cash
removed from register", but the value came from the session
transactions (with CASHUP_SURPLUS/DEFICIT lines excluded), so it
represented the *expected* cash, not the amount actually counted at
cashup. When a deficit or surplus existed, the reconciliation row
appeared as the difference between two figures the user could not
both see.

Render two rows when surplus_total or deficit_total is set on a
completed cashup: "Expected cash total" (from total_grouped) and
"Cash removed from register" (from cash_register_actions.amount,
already exposed as data.amount). The reconciliation row below now
visibly equals the difference. Balanced cashups and the in-progress
preview keep the original single-row display, and the negative-cashup
branch flips the labels to "Expected cash to add to register" /
"Cash added to register".

Test plan:
1. Configure a cash register and record a few cash payments.
2. Trigger a cashup, enter the exact expected amount; open the
   summary modal -- a single "Cash removed from register" row is
   shown matching the expected total.
3. Repeat with an amount lower than expected -- "Expected cash
   total" and "Cash removed from register" both appear, and the
   "Cashup deficit" row is the difference.
4. Repeat with an amount higher than expected -- the actual row
   exceeds the expected row and "Cashup surplus" matches the gap.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:39 +00:00
Martin Renvoize 86d034e628
Bug 40445: (follow-up) Clarify cashup summary cash row wording and sign
A cashup is the process of removing cash from the till to send it
to the bank, so labelling the headline cash line "Cash added to
register" was confusing. Re-word it to "Cash removed from register"
in the typical case and "Cash added to register" in the edge case
where the cashier has had to top up the till to cover refunds.

While doing this, align the sign convention of total_grouped with
that of the overall total field on the summary: positive means cash
collected (i.e. removed from the register at cashup time), negative
means cash had to be added back into the register. Previously
total_grouped used the raw accounting sign (credits negative, debits
positive), so a typical $2 cash session would display "Total: 2.00"
and "Cash added to register: -2.00", which was internally
inconsistent. The conditions on the cash and other-payment-type rows
in cashup_modal.js are swapped to match the new convention.

Test plan:
1. Apply the patch and rebuild JS (yarn js:build) inside KTD.
2. As a staff user with permission to manage cash registers, take
   payments via several payment types (some cash, some not).
3. Trigger a cashup and view the cashup summary modal.
4. Confirm the "Total" line and the per-payment-type lines all
   display as positive amounts in the typical "money was collected"
   scenario, with the cash row labelled "Cash removed from
   register".
5. Issue some refunds large enough to exceed cash collected, then
   cash up again.
6. Confirm the cash row is labelled "Cash added to register" with a
   negative amount and the per-non-cash-type rows are labelled
   "{type} to add" where applicable.
7. Run prove t/db_dependent/Koha/Cash/Register/Cashup.t and prove
   t/db_dependent/Koha/Cash/Register.t inside KTD; both should
   pass.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:38 +00:00
Martin Renvoize 463b92df52
Bug 40445: (QA follow-up) Add nonce, POD, and test isolation
This patch resolves three QA findings on the cashup reconciliation
work:

* Add a CSP nonce to the inline <style> block in cashup_summary.inc
  so it is not flagged by the forbidden_patterns check.
* Document the private _get_session_end method on
  Koha::Cash::Register::Cashup so pod_coverage passes.
* Mock CashupReconciliationNoteRequired to 0 in
  t/db_dependent/Koha/Cash/Register/Cashup.t so the test does not
  depend on the site preference value (it previously failed when
  the syspref was set to "Require" because surpluses and deficits
  are created without a reconciliation note in this test).

Test plan:
1. Apply the patch.
2. Set CashupReconciliationNoteRequired to "Require".
3. Run prove t/db_dependent/Koha/Cash/Register/Cashup.t -- it should
   pass.
4. Set CashupReconciliationNoteRequired to "Don't require" and
   re-run -- it should still pass.
5. Run koha-qa.pl on the bug branch -- pod_coverage on
   Koha/Cash/Register/Cashup.pm and forbidden_patterns on
   cashup_summary.inc should now pass.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:37 +00:00
Martin Renvoize b7a68f1f49
Bug 40445: Add configuration options and UI enhancements
This patch adds configuration options for cashup reconciliation and
enhances the user interface with preview functionality and improved
interaction patterns.

Configuration Options:

1. CashupReconciliationNoteRequired (System Preference):
   - Optional validation requiring reconciliation notes when discrepancies exist
   - Ensures documentation of surplus/deficit causes when needed
   - Can be disabled for less stringent workflows
   - Applied at cashup completion time

2. CashupReconciliationNoteAuthorisedValue (System Preference):
   - Allows use of authorized value dropdown for reconciliation notes
   - Provides standardized note options (e.g., "Till count error", "Cash removed")
   - Falls back to free text textarea when not configured
   - Improves consistency and reporting capabilities

UI Enhancements:

1. Cashup Summary Preview:
   - Preview cashup summary for in-progress cashups
   - Shows current transaction totals before completion
   - Clearly indicates preview vs. final summary status
   - Helps staff verify amounts before committing

2. Improved User Experience:
   - Conditional note fields based on configuration
   - Authorized value dropdowns when configured
   - Clear validation messages
   - Responsive UI updates

Backend changes:
- Enhanced Koha::REST::V1::CashRegisters::Cashups with preview support
- Database atomicupdate script for system preferences
- Validation logic for required notes

Frontend changes:
- Preview modal functionality in cashup_modal.js
- Conditional rendering of note input fields
- Authorized value dropdown integration
- Enhanced summary display logic

Test plan:
1. Apply patches and run database update
2. Configure CashupReconciliationNoteRequired:
   - Enable preference
   - Perform cashup with discrepancy without note - should fail
   - Add note and retry - should succeed
   - Disable preference - verify note optional
3. Configure CashupReconciliationNoteAuthorisedValue:
   - Create AV category (e.g., CASHUP_NOTE) with values
   - Set preference to category name
   - Verify dropdown appears in cashup modal
   - Clear preference - verify textarea shown
4. Test cashup preview:
   - Start cashup on register
   - Click "Preview cashup summary"
   - Verify current totals displayed
   - Add transaction and re-preview - verify updated
   - Complete cashup - verify final summary

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:36 +00:00
Martin Renvoize 9db47a47c1
Bug 40445: Implement optional two-phase cashup workflow
This patch implements an optional two-phase cashup workflow for point
of sale operations, along with improved error handling and validation.

Two-Phase Cashup Workflow:
- Staff can initiate cashup (CASHUP_START) and complete later
- Prevents new transactions on registers with cashups in progress
- Tracks cashup sessions with start/end timestamps
- Supports both quick cashup (immediate) and staged cashup workflows
- Updates batch cashup workflow operations across multiple registers

Error Handling & Validation:
- Centralized exception handling for cashup operations
- Clear validation messages for missing parameters
- Prevents cashup when no cash transactions exist
- Improved handling of zero/negative amounts
- Informative error messages for user guidance

Negative Amount Support:
- Supports negative cashup amounts for cashup deficits
- Automatic detection when actual cash is less than expected amount
- Creates appropriate CASHUP_DEFICIT records
- UI handles both positive and negative reconciliation amounts
- Updated calculations and display logic throughout

Modal Refactoring:
- Eliminates code duplication between register.tt and registers.tt
- Centralized cashup modal functionality in cashup_modals.js
- Shared confirm_cashup.inc and trigger_cashup.inc templates
- Consistent user experience across all cashup workflows
- Improved maintainability and code organization

Backend changes (Koha::Cash::Register):
- start_cashup(): Creates CASHUP_START action to begin session
- cashup_in_progress(): Detects active cashup sessions
- add_cashup(): Enhanced with two-phase completion support
- outstanding_accountlines(): Respects cashup session boundaries

Frontend changes:
- Interactive modals for starting and completing cashups
- Real-time calculation displays
- Support for reconciliation with actual amounts
- Batch operations UI for multiple registers
- Responsive error messaging and validation feedback

Test plan:
1. Apply patches and restart services
2. Run prove t/db_dependent/Koha/Cash/Register.t
3. Run prove t/db_dependent/Koha/Cash/Register/Cashup.t
4. Test single register workflows:
   - Start cashup, add transactions, verify blocked
   - Complete cashup with reconciliation amounts
   - Test with positive, negative, and zero amounts
5. Test multi-register workflows:
   - Select multiple registers on registers page
   - Perform batch cashup operations
   - Verify each register processes correctly
6. Test error conditions:
   - Attempt cashup with no transactions
   - Attempt cashup with invalid amounts
   - Verify clear error messages displayed

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:35 +00:00
Martin Renvoize 6d5c2f463f
Bug 40445: Add cashup reconciliation functionality
Implements cashup reconciliation allowing staff to record actual cash
amounts and track surplus/deficit discrepancies.

Backend changes:
- Enhanced add_cashup() to accept actual_amount and optional notes
- Automatic CASHUP_SURPLUS/DEFICIT accountline creation for discrepancies
- Transaction handling ensures atomicity of cashup and reconciliation
- outstanding_accountlines() excludes reconciliation entries

Frontend changes:
- Interactive modal requiring actual amount entry
- Real-time surplus/deficit calculation
- Conditional note field for discrepancies (1000 char limit)
- Enhanced summary modal displays reconciliation details
- Full audit trail with timestamps and manager links

Test plan:
1. Apply all patches and restart services
2. Run prove t/db_dependent/Koha/Cash/Register.t
3. Create test transactions on a register
4. Perform cashup with balanced amount - verify no reconciliation accountlines
5. Perform cashup with surplus - verify CASHUP_SURPLUS credit created
6. Perform cashup with deficit - verify CASHUP_DEFICIT debit created
7. Add notes to reconciliation - verify stored correctly
8. Start new cashup - verify previous reconciliation excluded from outstanding
9. View cashup summary - verify reconciliation displayed prominently

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:34 +00:00
Martin Renvoize 6a38111462
Bug 40445: Add CASHUP_SURPLUS and CASHUP_DEFICIT account types
This patch adds two new system account types to support cashup reconciliation:

- CASHUP_SURPLUS (credit type): Used when actual cash found exceeds expected amount
- CASHUP_DEFICIT (debit type): Used when actual cash found is less than expected amount

Both types are system-managed and cannot be manually added by staff. They will be
automatically created during cashup processes when discrepancies are detected.

Changes include:
- Atomicupdate script for existing installations
- Mandatory YAML files for fresh installations

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Jackie Usher <jackie.usher@westsussex.gov.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:33 +00:00
Tomás Cohen Arazi 86d54c2c4a
Bug 42391: (follow-up) Add missing warning assertions for Object::store
The previous follow-up restored warn in Object::store but missed
several test files that trigger DB exceptions via ->store(). These
tests now fail Test::NoWarnings because the warning is emitted but
not expected.

This patch adds warning_like wrappers to 15 additional test files,
asserting that the expected DB exception warning is emitted.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/api/v1/biblios.t \
       t/db_dependent/Koha/Holds.t \
       t/db_dependent/api/v1/return_claims.t \
       t/db_dependent/api/v1/libraries.t \
       t/db_dependent/Koha/Booking.t \
       t/db_dependent/api/v1/patrons.t \
       t/db_dependent/SIP/Message.t \
       t/db_dependent/Koha/Patron.t \
       t/db_dependent/Koha/Acquisition/Fund.t \
       t/db_dependent/api/v1/acquisitions_orders.t \
       t/db_dependent/Koha/ILL/Requests.t \
       t/db_dependent/Koha/Patron/Relationship.t \
       t/db_dependent/Koha/Hold.t \
       t/db_dependent/Circulation/Returns.t \
       t/db_dependent/api/v1/bookings.t
=> SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-27 12:12:29 +00:00
Tomás Cohen Arazi 4189d42ee6
Bug 42391: (follow-up) Restore DB exception warnings in Object::store
Bug 42391 removed the warn from Koha::Object::store when it moved
exception translation to the exception_action hook. This lost useful
debugging information from the Plack error logs.

This patch restores the warning by adding a try/catch in store() that
stringifies and warns the exception before rethrowing. The warning now
contains the translated Koha exception message (e.g. 'Broken FK
constraint') rather than the raw DBI error, since translation happens
before the exception reaches store().

Note: delete() does not warn. Ideally the logging would live in the
ExceptionMapper itself (via an injected logger), covering all code paths
uniformly. That is a larger refactor tracked separately.

Tests that assert on these warnings are updated to match the new
message format.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/Koha/Object.t \
       t/db_dependent/Koha/Objects.t \
       t/db_dependent/Koha/Schema/ExceptionMapper.t \
       t/db_dependent/Koha/Item.t \
       t/db_dependent/Koha/Suggestions.t \
       t/db_dependent/Koha/Account.t \
       t/db_dependent/Koha/ApiKey.t \
       t/db_dependent/Koha/Checkouts/ReturnClaim.t \
       t/db_dependent/Circulation/MarkIssueReturned.t \
       t/db_dependent/Suggestions.t \
       t/db_dependent/Biblio.t
=> SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-21 15:40:23 +00:00
Pedro Amorim 880f33a4bb
Bug 41604: (follow-up) Wait for the page to be ready before searching in patrons_search.t
Bug 41604 made every page do a bit more work when it loads. This
Selenium test didn't wait for that work to finish before typing into
the search box, so sometimes it searched too early and got the wrong
results.

This adds a short wait after each page load so the test only searches
once the page is actually ready.

Introduced by: Bug 41604: Save user decision per column when visibility changes

Test plan:
prove t/db_dependent/selenium/patrons_search.t

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-21 15:40:19 +00:00
Pedro Amorim 037d38b00c
Bug 42772: Adjust yarn.lock
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:45:02 +00:00
Jonathan Druart f89e68c220
Bug 42772: Cypress.env => cy.env
Warning: The allowCypressEnv configuration option is enabled. This allows any browser code to read values from Cypress.env(). This is insecure and will be removed in a future major version.

1. Replace Cypress.env() calls with cy.env() (for sensitive values) or Cypress.expose() (for public configuration)
2. Set allowCypressEnv: false in your Cypress configuration to disable Cypress.env()

Learn more: https://on.cypress.io/cypress-env-migration

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:45:01 +00:00
Jonathan Druart 7b307694fa
Bug 42772: Remove experimentalStudio from config
The experimentalStudio option was removed in Cypress version 15.4.0.

Cypress Studio is now available for all users.

You can safely remove this option from your config.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:45:00 +00:00
Jonathan Druart f357dd4103
Bug 42772: Upgrade Cypress to 15.16.0
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:59 +00:00
Kyle M Hall 54fc07f408
Bug 42391: (QA follow-up): Fix unit tests
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:58 +00:00
Kyle M Hall 22d988e8b4
Bug 42391: (QA follow-up): Clarify exception_action ref-passthrough comment
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:57 +00:00
Kyle M Hall 9c04c7bbb4
Bug 42391: (QA follow-up): Remove unused Try::Tiny from Koha::Item
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:56 +00:00
Tomás Cohen Arazi 62e003b88f
Bug 42391: (follow-up) Rename ExceptionTranslator to ExceptionMapper
The Koha::Schema::Util:: namespace adds unnecessary nesting for a
single-purpose class. This follow-up moves it to
Koha::Schema::ExceptionMapper, which sits alongside Koha::Schema
itself — a better fit given it's tightly coupled to the Schema's
exception_action hook.

Changes:
- Koha::Schema::Util::ExceptionTranslator → Koha::Schema::ExceptionMapper
- Update Koha::Schema to use the new module
- Move test file accordingly
- Remove now-empty Koha/Schema/Util/ directory

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:55 +00:00
Tomás Cohen Arazi 033cfd772c
Bug 42391: (follow-up) Add NotNull, NotInStorage and UnhandledDBError translations
Extend the ExceptionTranslator to handle two additional MySQL error
patterns:

- NOT NULL violations ('Column X cannot be null' and 'X doesn't have
  a default value') → Koha::Exceptions::Object::NotNull
- Not in database errors → Koha::Exceptions::Object::NotInStorage

Add Koha::Exceptions::Object::NotNull exception class with a property
field identifying the offending column.

Add Koha::Exceptions::Object::UnhandledDBError as a catch-all for DBI
errors that don't match any known pattern. This closes the gap where
unmatched MySQL errors could leak as DBIx::Class::Exception to callers
(e.g. templates that no longer catch that type).

The exception_action handler now distinguishes three cases:
- Koha exception objects (from txn_do re-throws): passed through via
  'die $msg' to preserve the original exception type. Without this,
  txn_do's rollback-and-rethrow would stringify the Koha exception
  and wrap it in UnhandledDBError, losing the specific type.
- DBI errors (prefixed 'DBI Exception:'): translated if possible,
  otherwise wrapped in UnhandledDBError.
- DBIC internal errors (e.g. 'Not in database'): translated if
  possible, otherwise thrown as DBIx::Class::Exception.

Note: NotInStorage translation works for update operations but not for
delete-on-detached-Row where DBIC bypasses the Schema's exception_action.
Those cases remain DBIx::Class::Exception.

Update tests:
- OAIServers.t, Z3950Servers.t: assert NotNull with property check
- Hold.t: assert NotNull, remove warn suppression hack
- Objects.t: update-not-in-storage cases assert NotInStorage
- Upload.t: delete-not-in-storage stays DBIx::Class::Exception

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:54 +00:00
Tomás Cohen Arazi face3c96d0
Bug 42391: (follow-up) Fix Objects.t case 3 to test FK deletion properly
The 'Delete a patron that cannot be deleted' test (case 3) was using a
stale $r_us object from case 2 (not in storage), so it was not actually
testing FK constraint deletion. The assertion on DBIx::Class::Exception
passed by accident.

This follow-up:
- Fetches a fresh Koha::Patron object for case 3
- Clears $e_us/$e_them before the new test
- Asserts Koha::Exceptions::Object::FKConstraintDeletion, which is what
  exception_action now produces for FK deletion errors

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:53 +00:00
Tomás Cohen Arazi 66b738f1eb
Bug 42391: Use exception_action to translate DBIC exceptions at the source
DBIx::Class provides an exception_action hook that intercepts every
throw_exception call before it becomes a DBIx::Class::Exception. By
installing this hook on Koha::Schema via a connection() override, all
database exceptions are automatically translated into Koha exceptions
at the source.

This eliminates the need for translate_exception catch blocks in every
caller:
- Remove try/catch from Koha::Object::store
- Remove try/catch from Koha::Object::delete
- Remove try/catch from Koha::Item::add_to_bundle
- Remove translate_exception convenience method from Koha::Schema
- Remove safe_do (already removed in previous follow-up)

ExceptionTranslator is refactored to work on plain error message strings
instead of DBIx::Class::Exception objects, since exception_action
receives the message before wrapping.

Unmatched errors fall through to DBIx::Class::Exception->throw() as
the default, preserving backward compatibility.

The warn $_->{msg} that was in Object::store is no longer needed since
exceptions are translated before they propagate. Tests that asserted on
that warning (via warning_like) are updated to use throws_ok directly.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
  k$ prove t/db_dependent/Koha/Schema/Util/ExceptionTranslator.t
=> SUCCESS: Tests pass!
  k$ prove t/db_dependent/Koha/Object.t
=> SUCCESS: Tests pass!
  k$ prove t/db_dependent/Koha/Item.t
=> SUCCESS: Tests pass!
  k$ prove t/db_dependent/Koha/Patron/Modifications.t
=> SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:52 +00:00
Owen Leonard 8bc90cbb5e
Bug 42440: Remove event attributes from labeled MARC detail template
This patch removes a redundant "onchange" event attribute from the
framework selection dropdown. This event is already registered in the JS
block.

The patch also does a little bit of cleanup to the JS: Combining two
<script> tags which don't need to be separate and moving a function
outside of the $(document).ready block.

To test, apply the patch and search for a bibliographic record in the
catalog.

- From the detail page, click the "Labeled MARC" tag in the sidebar (you
  may need to enable the "viewLabeledMARC" preference).
- Try changing the "With framework" selection. The page should reload
  with the selected framework.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:51 +00:00
Andrew Fuerste Henry 22621e24cd
Bug 36762: [QA Follow-up]: Tidy branchtransfers.pl
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:50 +00:00
Emmi Takkinen ed992358bf
Bug 36762: Allow to ignore hold when creating a transfer
When one tries to transfer an item that could fill a pending hold
to a library that is not the holds pickup library, Koha gives only
a option to transfer item to pickup library. This patch adds
new option to ignore hold and transfer item to the library user
selected in transfer tool.

To test:
1. Log into Library A.
2. Find or create a biblio with 1 holdable item at Library A.
3. Place a hold for the biblio with pickup at Library B.
4. Navigate to Circulation > Transfers.
5. Attempt to transfer item to Library C.
=> Note that Koha only allows to transfer item to Library B,
cancel hold and attempt transfer or return to the transfer tool.
6. Apply this patch.
7. Attempt to transfer item again.
=> Note that there is now an option to ignore the hold and transfer
item to Library C.
=> When option to transfer item to the Library C is selected, confirm
that Koha tells that transfer was made to Library C and a new transfer
was made to database.

Sponsored-by: Koha-Suomi Oy
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:49 +00:00
Emmi Takkinen 2994d2c484
Bug 36762: Fix setting transfer for hold via branchtransfers.pl
When one tries to transfer an item that could fill a pending hold
to a library that is not the holds pickup library, Koha gives only
a option to transfer item to pickup library. When this option
is chosen, Koha tells that item is being transfered to selected
library not to pickup library but fails to create the transfer.
This patch fixes this by setting op variable as "cud-transfer"
and tobranchcd as holds pickup library so that transfer is actually
made and target library is displaued correctly.

To test:
1. Log into Library A.
2. Find or create a biblio with 1 holdable item at Library A.
3. Place a hold for pickup at Library B.
4. Navigate to Circulation > Transfers.
5. Attempt to send your item to Library C.
6. Koha gives you three options. Pick the first one:
"Transfer to Library B."
7. Koha tells you it has transferred the item to Library C.
8. Go to detail.pl for your title, confirm the holdings table
says "there is an item level hold" on your item.
9. Click to the Holds tab for your title, confirm the hold claims
to be in transit.
10. Query the branchtransfers table
SELECT * FROM branchtransfers WHERE itemnumber = [your item]
=> Note that no new transfer was made.
11. Query reserves SELECT * FROM reserves WHERE biblionumber = [your biblio]
=> Confirm your hold has found=T, itemnumber=[your item], and item_level_hold=0
12. Revert trasfer from biblios holds page.
13. Apply this patch.
14. Attempt the transfer again.
=> Koha should now tell that transfer was made to Libary B.
15. Check branchtransfers table again.
=> New transfer should have been made to Library B.

Sponsored-by: Koha-Suomi Oy
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:48 +00:00
Martin Renvoize 3abe3f1485
Bug 40206: (QA follow-up) Fix JS error and misleading message on disabled curbside pickups page
The early-exit added for the CurbsidePickup syspref check left
policy/curbside_pickups unset, but intranet-bottom.inc's jsinclude
still referenced them unconditionally, causing a client-side
TypeError on every render of the disabled state. Guard the whole
jsinclude block with disabled_for_branch.

Also distinguish the two reasons the page can be disabled: the
CurbsidePickup syspref being off globally vs. this branch simply
not having an enabled curbside pickup policy configured. The
message and link now point staff to the correct admin page for
each case, instead of always suggesting the syspref (which may
already be on).

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:47 +00:00
Tomas Cohen Arazi c68bacc5c2
Bug 40206: Fix wrong layout in curbside pickups when 'CircSidebar' enabled
This patch solves the following issues:

* Template flow issue: the template called STOP which prevented the
  WRAPPER to complete and affected how things were rendered.
* Redirect to 404 if feature disabled. This is common practice in other
  modules. The fact things can be executed even with the feature
  disabled is not ok (syspref).
* Shortcircuit earlier. Similar to the above, if the feature is
  disabled for the current branch, no DB queries and stuff should take
  place. Notice the lack of checks actually makes the controller perform
  the actions. It is just that it is not displayed afterwards!

To test:
1. Enable the `CurbsidePickup` syspref.
2. 'Activate' the `CircSidebar` syspref.
3. Be on a branch that doesn't have them enabled
4. Go to Circulation > Check in
   => SUCCESS: A nice left-hand sidebar is displayed with access to
      Circulation actions
5. Click on 'Curbside pickups'
   => FAIL: The sidebar doesn't display. boo!
6. Apply this patch
7. Restart plack
   $ ktd --shell
   $ koha-plack --restart kohadev
8. Repeat 4
   => SUCCESS: It renders much better!
9. Sign off :-D

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 12:44:43 +00:00
Pedro Amorim 2f227048ae
Bug 42280: (RM follow-up) Tidy categories.tt
Leftover untidy JS from bug 26355 (multi-line jQuery .on() calls,
missing semicolon) in the clone/clear message handlers.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:16:04 +00:00
Jan Kissig a4a53ea03c
Bug 42280: (QA follow-up) Split JS in uage_statistics to TT and non TT block. Auto tidy
This splits remaining JS into a block with and without TT interpolation. Auto tidy

Small changes on add_to_map() call:

To test:
a) go to Administration -> Shar eyour usage statistics
b) in dev console check Koha.prefs.UsageStatsGeolocation is [] when no coordinates are set
c) add some coordinates to main library (first Geolocation input field) and others
d) check that Main and other libraries are drwan on the map when reentering usage statistics page
e) No console errors should be shown

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:16:03 +00:00
Jan Kissig 8de6874de5
Bug 42280: (QA follow-up) Split JS in transfer_limits to TT and non TT block. Auto tidy
This splits remaining JS into a block with and without TT interpolation. Auto tidy

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:16:02 +00:00
Jan Kissig 5ce434644c
Bug 42280: (QA follow-up) Split JS in restrictions to TT and non TT block. Auto tidy
This splits remaining JS into a block with and without TT interpolation. Auto tidy

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:16:01 +00:00
Jan Kissig 1987d0a930
Bug 42280: (QA follow-up) Split JS in preferences to TT and non TT block. Auto tidy
This splits remaining JS into a block with and without TT interpolation. Auto tidy

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:16:00 +00:00
Jan Kissig 1153112951
Bug 42280: (QA follow-up) Split JS in matching-rules to TT and non TT block. Auto tidy
This splits remaining JS into a block with and without TT interpolation. Auto tidy

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:15:59 +00:00
Jan Kissig c2f1576a3d
Bug 42280: (QA follow-up) Split JS in curbside_pickup to TT and non TT block. Auto tidy
This splits remaining JS into a block with and without TT interpolation. Auto tidy

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:15:58 +00:00
Jonathan Druart f52abf6d79
Bug 42280: Tidy all script tags - admin (auto tidy)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:15:57 +00:00
Jonathan Druart 70bb50988b
Bug 42280: Tidy all script tags - admin
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the administration module.

The 2 main changes are in the following files:
* aqplan.tt
"0" should be correctly formatted
* categories.tt
trivial change about table settings

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:15:56 +00:00
Jonathan Druart 0de23cde12
Bug 42291: Prevent XSS
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:15:54 +00:00
Jan Kissig d194215dfe
Bug 42291: (follow-up) Move remaining JS to non TT script block
This removes all TT interpolations from the build_dropdown function and moves it to the non TT script block.

To test:
a) create several private or public lists via /cgi-bin/koha/virtualshelves/shelves.pl
b) either the number of private or public list should be > 10 to
c) add some items to a list
d) on this lists content site click 'Add to' to open drop down menu
e) the menu should offer up to 10 private or 10 public list options and a [ more lists ] option

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:15:53 +00:00
Jonathan Druart 56036ac098
Bug 42291: Tidy all script tags - shelves (auto tidy)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:15:52 +00:00
Jonathan Druart f73b0db9ae
Bug 42291: Tidy all script tags - shelves
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the virtual shelves module.

Trivial changes, the existing JS variables are moved in a separate
script tag.
We could have gone further with the list of shelves (the build_dropdown
function) , but that's for another day...

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:15:51 +00:00
Jonathan Druart 9ac2f291b3
Bug 41061: Remove unused calendar JS functions
Since the introduction of Maskito by bug 33703 we do no longer need to
valid dates when the flatpickr widget is closed.

This patch removes the following JS function from staff and OPAC:
* is_valid_date
* validate_date
* get_dateformat_str

Test plan:
0. Do not apply this patch and confirm that the date string received by
   those functions has already been correctly formatted
1. Apply the patch and git grep to make sure no occurrence exists in the
   code
2. Confirm that you are still able to select dates with flatpickr

Signed-off-by: Emmi Takkinen <emmi.takkinen@koha-suomi.fi>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:15:50 +00:00
Pedro Amorim c798a0c49b
Bug 41084: (RM follow-up): Remove atomicupdate file
Should have been part of e5aa0c0

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-20 09:15:49 +00:00
Lucas Gass 1265f90523
Bug 43005: Remove messaging preferences check
1. Go to Patron record -> Details
2. Under "Library use" click 'Edit'
3. From here change the patron category, no pop-up
4. APPLY PATCH and try again
5. Now you should see the pop-up modal

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:20 +00:00
Kyle M Hall 506f6ed2a2
Bug 43034: Fix t/db_dependent/Koha/EDI.t failure when SearchEngine is Elasticsearch
Bug 21820 made the Zebra queue update conditional on the new
ElasticsearchEnableZebraQueue system preference, which defaults to off.
The 'basic_quote_processing' subtest in t/db_dependent/Koha/EDI.t counts
the log lines process_quote emits, and two of those are the
"specialUpdate biblioserver" traces that ModZebra logs only when the
Zebra queue path runs. When the SearchEngine preference is set to
Elasticsearch, that path no longer runs by default, so the subtest now
records 6 log lines instead of 8 and fails:

    #   Failed test '8 log lines recorded for passed quote file'
    #          got: '6'
    #     expected: '8'
    #   Failed test 'Trace recorded biblioserver index'

Test Plan:
1) Set the SearchEngine system preference to 'Elasticsearch'
2) prove t/db_dependent/Koha/EDI.t
3) Note the basic_quote_processing subtest fails with "got: '6', expected: '8'"
4) Apply this patch
5) prove t/db_dependent/Koha/EDI.t
6) Note the tests pass!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:19 +00:00
Martin Renvoize 77fc25f184
Bug 42915: (QA follow-up) Remove unused filters_options variable
filters_options was introduced with the coded_values library filtering
patch but never referenced anywhere in the file.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:18 +00:00
Martin Renvoize 1cf8f9ffc5
Bug 42915: (QA follow-up) Restore fallback message for holds with no patron data
The "Add patron names to data in column def in holds table" patch
dropped the null-patron handling in the patron column's render
function, so a hold row whose patron embed comes back null (e.g. a
user without permission to view the patron on that hold, per Bug
23269's test plan) now renders a blank cell instead of a useful
message.

Restore the fallback, sourced from `all_libraries` (populated by this
same table's include) rather than the separate `libraries` global,
and check `row.patron` directly instead of the now-composite `data`
column key.

To test:
1. Have holds on a bib from patrons the logged in user cannot see
   (e.g. IndependentBranches, or a reduced permission set)
2. View the record's Holds tab
3. Confirm rows with no visible patron show "A patron from library
   <library>" instead of a blank Patron cell

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:17 +00:00
Martin Renvoize e2685f88c9
Bug 42915: (QA follow-up) Declare all_libraries once, unconditionally, in request.tt
holds_table.inc is INCLUDEd in a loop from reserve/request.tt whenever
HoldsSplitQueue splits the holds queue by branch/itemtype, or when
holds for more than one biblio are shown at once (multi_hold). Each
inclusion appended its own <script> block declaring `all_libraries`,
and since top-level `let`/`const` in separate inline <script> tags
share the page's global lexical scope, the second and subsequent
occurrences threw "Identifier 'all_libraries' has already been
declared", breaking those script blocks and the pickup-library
filtering that depends on `all_libraries` being set.

Worse, that script block is only emitted when a biblio's holds are
actually rendered (biblioloo.total_holds > 0), but holds.js is loaded
unconditionally by reserve/request.tt and reads `all_libraries` at
top-level parse time. Placing the very first hold on a title (0
existing holds) therefore threw:

    ReferenceError: all_libraries is not defined

breaking every hold-management handler defined below that line in
holds.js.

Fix both by declaring `all_libraries` exactly once, unconditionally,
in request.tt's existing page-global script block (alongside the
other unconditional globals such as HidePatronName and
HoldsSplitQueue), before Asset.js("js/holds.js") loads, instead of
duplicating it inside the per-title/per-split holds_table.inc
include.

To test:
1. Search for a title with no existing holds and start placing one
2. Without this patch: browser console shows "ReferenceError:
   all_libraries is not defined" as soon as the page loads
3. With this patch: no console errors
4. Set HoldsSplitQueue to "pickup library", place holds on a title
   for two different pickup libraries, confirm both split tables
   render with working patron-name and pickup-library filtering and
   no console errors
5. Confirm the patron's own Holds tab (moremember.pl/circulation.pl)
   still loads without console errors

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:15 +00:00
Brendan Lawlor 6532c4660a
Bug 42915: (follow-up) Fix holds table display on patron record holds tab
To test:
1. Have a patron with multiple holds
2. Go to the patron record's hold tab
3. Confirm the table loads and no errors are logged to the console

Sponsored-by: CLAMS
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:15 +00:00
Brendan Lawlor e4af8e73f3
Bug 42915: Add patron names to data in column def in holds table
This patch adds the patron firstname and surname to the searchable
data in the patron column definition for the holds table and
removes unnecessary code from the render function.
This enables searching holds by patron names.

To test:
1. Have holds on a bib for different patrons and pickup locations
2. Try filtering by a patron name
3. See 'No matching records found'
4. Apply patch and repeat step 2
5. Filtering by patron name works!
6. Confirm filtering by cardnumber still works too
7. Set system preference HidePatronName to 'Don't show'
8. Confirm searching by patron names doesn't work ;)

Sponsored-by: CLAMS
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:13 +00:00
Brendan Lawlor e4a9f1f531
Bug 42915: Use coded value for library filtering in holds table
This patch adds coded_values for libraries to the holds table
and sets the data type in the pickup library column definition.
This enables searching holds by pickup location.

1. Have holds on a bib for different patrons and pickup locations
2. Try filtering by a pickup library name or branchcode
3. See 'No matching records found'
4. Apply patch and repeat step 2
5. Filtering works!
6. Note: if two libraries share the same beginning text
    both results are returned :)
    For example: 'Fair' returns holds for pickup at Fairview and Fairfield

Sponsored-by: CLAMS
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:12 +00:00
Pedro Amorim e5aa0c0737
Bug 41084: DBRev 26.06.00.004
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:11 +00:00
Martin Renvoize 52358cf3b6
Bug 41084: (QA follow-up) Add new pref to UsageStats
Add the new preference to UsageStats for HEA

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:10 +00:00
Katrin Fischer 667df54aed
Bug 41084: Re-format database update for new way of addind system preferences
... also adds say_success.

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:09 +00:00
Tomás Cohen Arazi 5d8af22026
Bug 41084: Allow utf8 export format when EnableZotero is enabled
When the EnableZotero system preference is enabled, allow the utf8 format
in opac-export.pl regardless of the OpacExportOptions settings.

This enables Zotero integration by ensuring the required UTF-8 encoded
MARC format is always available when the feature is enabled.

To test:
1. Install Zotero (https://www.zotero.org)
2. Install the browser extension (I did in in Chrome, not sure it exists for FF)
3. Perform a search in your koha instance. e.g. http://kohadev.localhost/cgi-bin/koha/opac-search.pl?idx=&q=agrovoc&weight_search=1
4. Open the Zotero extension
=> SUCCESS: It lists the search results for saving them
5. Choose one and save
=> FAIL: It says there was an error saving
6. Apply this patches
7. Run:
   $ ktd --shell
  k$ updatedatabase
8. Make sure EnableZotero is enabled
9. Repeat 3-5
=> SUCCESS: All good this time
10. Sign off :-D

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:08 +00:00
Tomás Cohen Arazi 5b3baf4170
Bug 41084: Add EnableZotero system preference
This system preference will be used to enable opac-export.pl?format=utf8
regardless of the OpacExportOptions values, specifically for Zotero
integration support.

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:07 +00:00
Jonathan Druart 674749c621
Bug 42801: Fix race condition in ERM/UsageReports_spec.ts
cy.get("#report_builder").contains("Submit").click();
cy.intercept("GET", "/api/v1/erm/eUsage/monthly_report/*").as(
    "reportRequest"
);
cy.wait("@reportRequest");

We should intercept before the click.

Test plan:
yarn cypress run --spec t/cypress/integration/ERM/UsageReports_spec.ts
Run it several times and confirm that it always passes

The failure was:
    <testcase name="Custom reports Should show the Display by year filter when filter is required" time="0.000" classname="Should show the Display by year filter when filter is required">
      <failure message="Timed out retrying after 10000ms: `cy.wait()` timed out waiting `10000ms` for the 1st request to the route: `reportRequest`. No request ever occurred.

https://on.cypress.io/wait" type="CypressError"><![CDATA[CypressError: Timed out retrying after 10000ms: `cy.wait()` timed out waiting `10000ms` for the 1st request to the route: `reportRequest`. No request ever occurred.
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:06 +00:00
Martin Renvoize 393c9478a0
Bug 42799: (QA follow-up) Close scroll listener leak in apply_sticky
apply_sticky is invoked from a watch() in Toolbar.vue every time the
toolbar element (re)mounts. The scroll listener it registers was never
removed, so long-lived SPA modules (ERM, ILL, Acquisitions,
Preservation, SIP2) that mount/unmount this Toolbar as the user
navigates between records leaked another window-level scroll listener
on every remount.

An IntersectionObserver + sentinel node was tried instead, since it's
the standard technique and doesn't poll on every scroll frame, but
verified against KTD/Cypress it fails
t/cypress/integration/Toolbar_spec.ts consistently: the observer's
callback fires once on attach and never fires again after
cy.scrollTo("bottom"), even though the sentinel is confirmed (via
instrumentation) to have scrolled fully out of the viewport. The
scroll-listener approach passes the same spec reliably across repeated
runs. So the scroll listener stays; apply_sticky now returns a
disconnect() callback and Toolbar.vue calls it before re-applying and
on unmount, closing the leak without touching detection behaviour.

Test plan:
1. Rebuild assets (yarn build) and confirm:
     yarn cypress run --spec t/cypress/integration/Toolbar_spec.ts
   passes consistently across several runs.
2. Manually confirm sticky toolbars in ERM, Acquisitions (vendors),
   ILL, Preservation and SIP2 still float correctly on scroll, and that
   navigating between several records doesn't accumulate extra "scroll"
   listeners on window (DevTools -> getEventListeners(window)).

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:05 +00:00
Jonathan Druart 35d4eebbb0
Bug 42799: Replace IntersectionObserver with scroll listener for sticky behaviour
Assisted-by: Mistral Vibe <vibe@mistral.ai>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:04 +00:00
Jonathan Druart 74b820394d
Bug 42799: Fix sticky behavior by using a sentinel node
Highlighted by the Cypress upgrade (bug 42772): the sticky element is always
visible and IntersectionObserver rely on the visibility of the element.

Cypress test Toolbar_spec.ts use scrollTo("bottom") to test the
behaviour of the sticky toolbar. We test that the "floating" class is
correctly added to the element. To do that we rely on
IntersectionObserver that will check the visibility of the element. But
the sticky element is always visible.
I actually do not recreate the problem when using the UI but the current
code may be broken in some browsers (or new/future versions).

To fix this problem we insert a new "sentinel" element into the DOM and
rely on its visibility to toggle the "floating" class.

Test plan:
Confirm that the sticky toolbar behaviour is the same after this patch
is applied, ie. that the "floating" class is added when we scroll down.

On top of bug 42772 confirm that the following command passes (run it
several times):
  yarn cypress run --spec t/cypress/integration/Toolbar_spec.ts

QA: Note that the apply_sticky function takes only one node in
parameter, that's why it has been renamed nodes => node

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:03 +00:00
Pedro Amorim 105359b512
Revert "Bug 41717: Bump PDF::Reuse and PDF::Reuse::Barcode versions"
This reverts commit 8b4d69b19d.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-17 13:49:00 +00:00
Jonathan Druart ff8bdc502e
Bug 42979: Prevent failure on My8
In MySQL8 the order on the PK is not preserved. We need to explicitely
pass it.

Test plan:
export DB_IMAGE=mysql:8.0;
then startup ktd
% prove t/db_dependent/api/v1/biblios.t
should pass with this patch
Without this patch it failed with:

13:00:29         #   Failed test 'exact match for JSON Pointer "/4/item_id"'
13:00:29         #   at t/db_dependent/api/v1/biblios.t line 276.
13:00:34         #          got: '3420'
13:00:36         #     expected: '3417'
13:00:37
13:00:38         #   Failed test 'Withdrawn comes after lost'
13:00:57         #   at t/db_dependent/api/v1/biblios.t line 276.
13:00:59         #          got: '3417'
13:01:04         #     expected: '3420'
13:01:05         # Looks like you failed 2 tests of 8.
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:23 +00:00
Nick Clemens c81d27de08
Bug 35948: Add jobs_unfinished_days and jobs_unfinished_type to cleanup_database.pl
This patch adds two new parameters to control the deletion of background jobs that did not complete.
Some libraries may want to keep a failed job longer than a completed one for debugging reasons.

To test:
0 - Apply patch
1 - Make some old failed jobs:
    INSERT INTO background_jobs (status,type,enqueued_on) VALUES
    ('started','update_elastic_index','2020-01-01'),
    ('failed','update_elastic_index','2026-01-01'),
    ('started','other_type','2021-01-01'),
    ('failed','other_type','2026-01-01');
2 - Confirm defaults of 365 and update_elastic_index for new params:
    perl misc/cronjobs/cleanup_database.pl -v --jobs-unfinished-days
3 - Should indicate 1 job older than 365 of type update_elastic_index would have been deleted
4 - Confirm days param works
    perl misc/cronjobs/cleanup_database.pl -v --jobs-unfinished-days 30
5 - Shoudl find 2 elastic jobs
6 - Confirm type param
    perl misc/cronjobs/cleanup_database.pl -v --jobs-unfinished-days --jobs-unfinished-type=other_type
7 - Should find 1
8 - Confirm type and days
    perl misc/cronjobs/cleanup_database.pl -v --jobs-unfinished-days 30 --jobs-unfinished-type=other_type
9 - Should find 2
10 - Confirm repeated type
     perl misc/cronjobs/cleanup_database.pl -v --jobs-unfinished-days=30 --jobs-unfinished-type=other_type --jobs-unfinished-type=update_elastic_index
11 - Finds 4
12 - Confirm deletion
     perl misc/cronjobs/cleanup_database.pl -v --jobs-unfinished-days=30 --jobs-unfinished-type=other_type --jobs-unfinished-type=update_elastic_index --confirm
13 - All your made up jobs are removed

Signed-off-by: Trevor Diamond <trevor.diamond@mainlib.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:22 +00:00
Adolfo Rodríguez f81ec88697
Bug 42999: Fix $valid_items flag contamination in reserve/request.pl
$valid_items was set inside the per-item availability check but used as
the gate of a separate if block, causing it to carry over across loop
iterations. Once any item set it to 1, all subsequent items entered the
if ($valid_items) branch and received $item->{available} = 1 regardless
of their individual availability.

Fix: merge the two consecutive if blocks into one so the pickup-location
logic only executes when the current item individually passes all
availability conditions. $valid_items continues to serve its correct
post-loop role (line 661: none_available_override).

Regression introduced by Bug 38412 (commit 1900effc9b).

Steps to follow:
1. Create a biblio with at least two items: one with a holdable item
   type and one with a not-for-loan item type (itemtypes.notforloan=1).
   The not-for-loan item must have a higher itemnumber than at least
   one holdable item (i.e. it is processed later in the loop).
2. Open reserve/request.pl?biblionumber=X&borrowernumber=Y
3. Observe that the not-for-loan item shows a radio button and a pickup
   location dropdown despite the Information column showing "Not for
   loan (Itemtype not for loan)" — bug confirmed.
4. Apply this patch and restart services. If running under Plack,
   restarting Plack is required for the change to take effect.
5. Reload the place-hold page.
6. Verify the not-for-loan item now shows "X Not for loan" without a
   radio button or pickup location dropdown.
7. Verify the holdable items still show radio buttons and pickup
   location dropdowns correctly.

Signed-off-by: Adolfo Rodríguez <adolfo.rodriguez@xercode.es>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:21 +00:00
Jan Kissig 41cfe9de0c
Bug 42874: Fix missing index param on z3950_auth_search
This patch will enable the forwarding of index param in all z3950_auth_search forms to keep the reference which auth field to fill in addbiblio.

Testplan:
a) edit f.e. 262: /cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=262
b) go to tab 7 and create a new 700 field (icon next to ADDED ENTRY--PERSONAL NAME)
c) click on the icon next to Personal name input field to open Authority search *
d) click create new authority (Adding authority Personal Name) *
e) click Z39.50/SRU search *
f) search an author f.e. Name (any): Dalei
g) on the result list every 'Import' button still has index (via hovering the import button) - but do not click import yet.
h) click on 'Go' to page 1 - see result list again. The 'Import' buttons do not have index param anymore
i) close that search window and go back to page 'Adding authority Personal Name'
j) click Z39.50/SRU search again *
k) search again for an author f.e. Name (any): Dalei
l) click 'Try another search'
m) check that new URL does not have index param anymore
n) search again - the results will not have an index param

Any imported authority will not be set/linked in the biblio that was initially edited.

p) apply patch and redo steps a) - n)
q) Import an author either via a 'Try another search' or via a result on a different page ('Go' button)
r) save authority
s) check that authority is added in your biblio in field 700.

* check that the URL in the popup window has a param like &index=tag_700_subfield_a_13631_9534947903 as this is the reference to the authority you want to add in the biblio.

Signed-off-by: Phil Ringnalda <phil@chetcolibrary.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:20 +00:00
Pedro Amorim b856177781
Bug 43069: Add tests
1) Set system preference 'PatronDuplicateMatchingAddFields' to empty (no fields ticked)
2) Go to <staff_url>/cgi-bin/koha/members/memberentry.pl?op=add and fill in the mandatory fields for a brand new patron (one that definitely doesn't exist yet)
3) Click 'Save'
4) Notice you get the 'Duplicate patron record?' dialog, even though no patron like this exists
5) Apply the patch. Restart plack
6) Repeat steps 2-3 with another new patron
7) Confirm the patron is created fine this time, no false duplicate error
8) Also try setting 'PatronDuplicateMatchingAddFields' to a field (e.g. 'city') and submit a new patron with that field left blank - confirm no false duplicate there either
9) Run:
prove t/db_dependent/Koha/Patrons.t

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:19 +00:00
Pedro Amorim 22221feaa2
Bug 43069: Don't match every patron when no duplicate-match fields apply
check_for_existing_matches() called Koha::Patrons->search(undef) when
PatronDuplicateMatchingAddFields left no usable field to match on,
which matches every patron instead of none.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:18 +00:00
Jonathan Druart e87732a1b6
Bug 42933: Reinstate license.status as a required field
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:17 +00:00
Jonathan Druart 0299c6f3c3
Bug 42933: Add a Cypress test
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:15 +00:00
Nick Clemens 59ac731b1d
Bug 43043: Pass logged in branchcode to OpacMySummaryNote
This patch uses the logged in branchcode to find the correct entries

To test:
1 - Create two new HTML customizations - OpacMySummaryNote - one for all libraries, one for a specific library
2 - Log in to opac as a user from the specific library
3 - Only see the all libaries note
4 - Apply patch
5 - Reload
6 - See both of the notes as expected!
7 - Sign off!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:14 +00:00
Laura_Escamilla 9a875d0202
Bug 42919: Retain suggestion form values when duplicate warning is shown
To test:

1. Make a suggestion that will trigger a duplicate warning, in KTD I like "Clothes, clothes, clothes, music, music, music, boys, boys, boys".
2. Fill out some other details, like notes and public notes.
3. The warning says "Click on "Confirm your suggestion" to ignore this message."
4. Notice none of the details are still there.
5. Apply the patch
6. Repeat steps 1-3. Notice that the details remain now after you get the warning message.
7. Submit the form to ensure all details are saved correctly.
8. Sign off and have a lovely day. :D

Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:13 +00:00
Baptiste Wojtkowski cb534eb7d3
Bug 8127: Fix filters use in 'Most-circulated items report'
Filters are broken for many reasons. Previous patch contains some
(little) tidying.
There are 3 parts in this patch:
1 - The column name is incorrectly named in the dropdown for "branch",
  it is added in this patch
2 - There is a definition for 'Day' and 'Month', but not for 'Week', it
is also added in this patch
3 - Dbh variables are passed in the string definition AND in the call. I
removed them from the string definition since it violates the coding
guidelines.

Test plan:

1. On an fresh ktd db, run the script provided here (https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=8127#c3)  to fill the database with checkins/checkouts:

Visit http://your_koha:8081/cgi-bin/koha/reports/cat_issues_top.pl
2. Run a baseline test with following params:
Checkout date from 01/01/2020
Checkout date to: tomorrow
Library: Any library
Fill nothing else

Notice it is works

3. Test with some limits (Library, Week, and any other), notice you encounter errors on Library and Week, and that any other is empty, without error.

4. Apply patch
5. Rerun 2-3 and notice you now have results. Check with each possible limit, limiting to 5. Test once with 10 as a limit.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:12 +00:00
Baptiste Wojtkowski 0b04d6f76c
Bug 8127: Remove useless commented warns
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:11 +00:00
Lucas Gass c997f83cdf
Bug 43001: Restore non-priority hold text to reserve/request.pl
To test:

1. Place a item level hold, making sure to mark it as "Non priority hold"
2. After placing the hold, on reserve/request.pl look at the "Details" column
3. Notice the hold is missing "Non priority hold" text in the details column
4. APPLY PATCH
5. Look again at reserve/request.pl and now the "Non priority hold" text should be restored to 'Details column'

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:09 +00:00
Chris Nighswonger 8b4d69b19d
Bug 41717: Bump PDF::Reuse and PDF::Reuse::Barcode versions
PDF::Reuse 0.43 fixes a TTFont/docProxy lifecycle bug that caused
prEnd() to crash when TrueType fonts were used.

PDF::Reuse::Barcode 0.09 is the current stable release.

Testing plan:

- Start ktd, note older perl modules versions under About Koha
- Apply patch
- ktd --shell
- sudo cpan -i PDF:Reuse; sudo cpan -i PDF:Reuse:Barcode
- Let cpan auto set itself up (not sure if sudo is needed above)
- `restart_all`
- Revisit About Koha; module versions should be 0.43 & 0.09 respectively

Signed-off-by: Eric Phetteplace <ephetteplace@cca.edu>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:08 +00:00
Martin Renvoize 111637445a
Bug 42868: (follow-up) Add regression test for non-UTC library timezone
The bug this fixed only manifests when the library's configured
timezone (C4::Context->tz) is not UTC: Koha::Object::_recursive_fixup
converts an incoming RFC3339 instant to that timezone before storing
it in the timezone-naive 'bookings.start_date'/'end_date' columns, so
a UTC-anchored boundary shifts by the timezone offset on write.

None of the existing Cypress specs catch this, because they never run
with the browser/test-runner timezone diverging from the configured
library timezone - both always come from the same container clock.

Add a Perl-level API test that mocks C4::Context->tz to a non-UTC zone
(America/New_York) and asserts the boundaries sent by place_booking.js
round-trip to the correct library-local date, pinning this regression
independently of any particular environment's timezone.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:08 +00:00
Martin Renvoize 5a7c328475
Bug 42868: (follow-up) Fix stale comment referencing removed UTC boundary logic
The comment above the day-boundary calculation still described the
previous dayjs.utc() approach, which was replaced by dayjs.tz() in the
parent commit. Update it to describe the current behaviour so it
doesn't mislead future maintainers into re-introducing the timezone
shift bug this patch fixed.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:07 +00:00
Lucas Gass 084660a6df
Bug 42868: Use local timezone instead of UTC when calculating start and end dates
To test:
1 - Edit koha-conf.xml
2 - Set timezone:
    <timezone>America/New_York</timezone>
3 - In Koha, enable bookings syspref
4 - Find a record, click the items tab, make an item bookable
5 - Place new booking, choose a date range
6 - View the bookings tab - note the booking extends into the date before
7- Check the DB, the start_date and end_date are wrong
8. APPLY PATCH, restart_all, clear browsewr cache
9. Place a new booking.
10. MariaDB [koha_kohadev]> select * from bookings WHERE booking_id={new booking}
11. Notice the start time should be {start_date} 00:00:00 and the end time should be {end_date} 23:59:59

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:06 +00:00
Jonathan Druart 50a5dcc4d4
Bug 42349: Adjust tests
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Aleisha Amohia <aleisha@catalyst.net.nz>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:05 +00:00
Jonathan Druart 8f98b493ae
Bug 42349: Fix filtering items by recalls
On the holdings table the filter by status=recalls is not working correctly.

Test plan:
Add a recall on an item of a bibliographic record that has several
items.
Filter the items table by status=recall
=> Without this patch all items are displayed
=> With this patch applied only the item with a recall is displayed

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Aleisha Amohia <aleisha@catalyst.net.nz>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:03 +00:00
Jonathan Druart 9544e68acc
Bug 42066: Add a Cypress test
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:05:02 +00:00
Jonathan Druart 06bac7582f
Bug 42066: Prevent 403 after an expired OPAC session
If a session has expired, the login form will get an empty CSRF token
and the login form will return 403

To recreate:
hit /cgi-bin/koha/sco/sco-main.pl
then hit /cgi-bin/koha/opac-main.pl
login
=> 403
Apply this patch, try again
=> Success

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 17:04:59 +00:00
Baptiste Wojtkowski dcbcea80e4
Bug 41602: (follow-up) Add a validation step for filtering
When there is a filter with the "status" param, we add a validation step
to secure the use of such a filter.

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:43 +00:00
Jonathan Druart 134fe95444
Bug 41602: Allow _status to be in nested structure
This patch allows _status to be in a nested structure.

Cannot be combined in all the possible ways, but at least it should fix
the UI.

Test plan:
Use the filters on the holdings table and confirm that you can combine
the status with other filters.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:42 +00:00
Jonathan Druart 128c24d58c
Bug 41602: Add a test
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:41 +00:00
Martin Renvoize 36bd8e6635
Bug 42669: Use exception type check for NoNodes and don't mask real failures
Two follow-up fixes from QA review of the previous commit:

- Detect Search::Elasticsearch::Error::NoNodes via ref($_) rather than
  regex-matching the stringified exception, matching the existing
  pattern already used in about.pl.
- Only reset jobs to 'new' for retry when NoNodes is the sole problem.
  If a genuine indexing error also occurred in the same batch, record
  the batch as 'failed' instead of silently discarding that error via
  the NoNodes retry path.

Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:40 +00:00
Tomás Cohen Arazi e8482427e9
Bug 42669: Fix es_indexer_daemon.pl silent job failure and NoNodes recovery
When update_index() throws, the daemon now:
- On NoNodes: aborts the batch, recreates the indexer object to reset
  the dead-node connection pool, and resets jobs to 'new' for retry.
- On other errors: marks jobs as 'failed' instead of 'finished'.

This prevents silent data loss where records disappear from search
results because their indexing jobs were incorrectly marked as done.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:39 +00:00
Paul Derscheid 852b0f1dcd
Bug 42225: (QA follow-up) Use configured unitprice field in outer SELECT
The outer SELECT hardcoded unitprice_tax_included while the inner
subquery selects the field returned by FieldsForCalculatingFundValues.
With CalculateFundValuesIncludingTax disabled the derived table only
exposes unitprice_tax_excluded, so spent.pl dies with:
Unknown column 'unitprice_tax_included' in 'field list'

To test:
1. Set CalculateFundValuesIncludingTax to 'Exclude'
2. View spent.pl for a fund with received orders
3. Observe error 500
4. Apply patch, restart_all
5. Reload, page renders correctly
6. Set CalculateFundValuesIncludingTax back to 'Include'
7. Reload, page still renders correctly

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:38 +00:00
Nick Clemens 683142868f
Bug 42225: Improve SQL on spent.pl
To test:
1 - Have some received orders and some invoice adjustments
2 - View spent.pl for a fund
3 - Apply patch, restart_all
4 - Reload
5 - Confirm the display has not changed

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:36 +00:00
Jonathan Druart 98ee165b82
Bug 42886: Add missing rtl
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:35 +00:00
Jonathan Druart f87dfcb26e
Bug 42886: Add bookings.css and edifact_interchange.css
Caught by the xt test

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:34 +00:00
Jonathan Druart ec4b88d6f9
Bug 42886: Add xt test to prevent future omission of css files in Makefile
Note that t/Makefile.t and xt/check_makefile.t were doing the same test.
Merging them.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:33 +00:00
David Cook fccc79dd70
Bug 42850: Add ill-requests-rtl.css to Makefile
Signed-off-by: David Cook <dcook@prosentient.com.au>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:32 +00:00
Andrii Nugged 0c48dd52f0
Bug 42850: Install generated ILL requests CSS
The ILL requests page loads css/ill-requests.css from the staff template. The stylesheet is generated by build-resources.PL and ignored by git, but Makefile.PL does not list it in the generated asset map.

As a result package installs do not copy the generated stylesheet into blib, so Asset.css cannot find it and no link tag is emitted.

Add ill-requests.css to the generated staff CSS file map so it is installed with the other generated staff stylesheets.

Test plan:

1. perl -c Makefile.PL

2. prove -v t/Makefile.t

3. Confirm that /cgi-bin/koha/ill/ill-requests.pl includes a link tag for css/ill-requests.css on a package/source-style install.

Signed-off-by: David Cook <dcook@prosentient.com.au>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:31 +00:00
Pedro Amorim 43b74cba6e
Bug 42617: (41512 follow-up) Remove ownPagination and fix recordsFiltered propagation
ownPagination set recordsFiltered on json.data instead of json, causing
DataTables to display "NaN total entries". Remove the artificial 5000
mechanism, propagate real counts from plugins that provide them, and
fall back to actual result counts for those that don't. Also removes a
dead drawCallback overwritten before it could fire.

Test plan:
1) Install this test version of the unpaywall plugin:
https://github.com/openfifth/koha-plugin-ill-avail-unpaywall/releases/tag/test
2) Enable ILLModule and ILLCheckAvailability
3) Configure the plugin and add any emaill address
4) Create a new ILL request:
<staff_url>/cgi-bin/koha/ill/ill-requests.pl?method=create&backend=Standard
5) Enter '42' for cardnumber, any library, and any DOI e.g. '123'.
6) Verify that pagination works as expected

Before patch, pagination controls dont show and the following label is shown:
No entries to show (filtered from NaN total entries)

Signed-off-by: Edith Speller <Edith.Speller@ukhsa.gov.uk>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:30 +00:00
Samuel Sowanick 4fd2721f9d
Bug 42519: Terminology - Update 'branch' to 'library' in patron categories
The default message in the "Self-renewal failure message" field
(Administration > Patrons and circulation > Patron categories >
 Account expiry and self-renewal) uses incorrect terminology.

Test plan:
1. Go to Administration > Patrons and circulation > Patron categories.
2. Click on a patron category.
3. Scroll down to the "Self-renewal failure message" field (in the
   Account expiry and self-renewal section).
4. Note the text says "...your local branch...".
5. Apply the patch.
6. Reload the page.
7. Note that the text now says "...your library...".

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:29 +00:00
Jonathan Druart 2b872b182a
Bug 42937: Remove unneeded flag in 00-strict.t
$Test::Strict::TEST_STRICT = 0 is only set for all_perl_files_ok
It's no longer used since bug 42126

Test plan:
prove t/db_dependent/00-strict.t
should still return green

Remove a use Modern::Perl for mainpage.pl
=> Should still return green

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:28 +00:00
Martin Renvoize fe8f079121
Bug 41819: (follow-up) Reject 'AC' refunds for anonymous transactions server-side
The previous patch hides the 'Account credit' option in the refund modal
when the accountline has no borrowernumber, but that is a UI convenience
only. A crafted POST (or a stale page) could still submit
refund_type=AC against an anonymous accountline, which would create a
REFUND credit with no patron to attach it to and leave the register
unbalanced because the payout branch is skipped.

Add a server-side guard in pos/register.pl that refuses the operation
and surfaces a new error_refund_anonymous_ac flag, rendered in
pos/register.tt in the same style as the other refund errors.

Test plan:
1. Apply the patch
2. Log in as a user with cash_management/anonymous_refund permission
3. Record an anonymous sale against a register
4. Using the browser devtools (or curl), POST to pos/register.pl with
   op=cud-refund, refund_type=AC and the accountline id of the sale
5. => Confirm the request is rejected and the
   "Account credit refunds are not available for anonymous transactions"
   message is shown
6. => Confirm no REFUND accountline was created and the register
   balance is unchanged
7. Repeat the POST with refund_type=CASH (or any non-AC type) and
   confirm the refund + payout are created normally

Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:27 +00:00
Martin Renvoize b236c67e4a
Bug 41819: POS refunds with 'AC' type incorrectly create PAYOUT accountlines
When processing a refund through the POS register with refund type 'AC'
(Account Credit), the system was incorrectly creating both a REFUND credit
and a PAYOUT debit with payment_type = 'AC'.

When 'AC' is selected, the refund should remain as a credit on the patron's
account without creating a payout. This behavior was already correct in the
member account refund workflow (members/boraccount.pl) but was missing
from the POS refund workflow.

This patch:
1. Adds the same check to pos/register.pl that exists in members/boraccount.pl
   to skip payout creation when refund_type is 'AC', making both workflows
   consistent.

2. Fixes the JavaScript in register.tt to properly hide the 'AC' option for
   anonymous transactions. The original check only tested for empty string,
   but borrowernumber can be undefined/null, which needs to be handled.

Test plan:
Part 1: Test patron transaction refund with Account Credit (AC)
1. Find or create a test patron
2. On the patron's Accounting tab, manually add a charge (e.g., Manual fee)
3. Pay the fee off from the patron's Accounting tab
4. Navigate to Point of Sale > Transaction History
5. Find the payment transaction you just created
6. Click "Issue refund" on the payment line item
7. Verify that "Account credit" appears as an option in the "Transaction type"
   dropdown
8. Select "Account credit" as the transaction type
9. Enter the refund amount and click "Confirm"
10. Navigate back to the patron's Accounting tab
11. Verify that a REFUND credit appears on their account
12. Verify that NO PAYOUT debit was created
13. Verify the credit remains on the patron's account (positive balance)

Part 2: Test patron transaction refund with Cash payout
16. Using the same patron, create another manual fee and payment
17. Navigate to Point of Sale > Transaction History
18. Click "Issue refund" on the new payment
19. Select "Cash" (or another payment type) as the transaction type
20. Enter the refund amount and click "Confirm"
21. Navigate back to the patron's Accounting tab
22. Verify both a REFUND credit and a PAYOUT debit appear
23. Verify the PAYOUT has the correct payment_type (e.g., CASH)

Part 3: Test anonymous transaction (no Account Credit option)
24. Navigate to Point of Sale
25. Add an item to sell and complete the payment
27. Navigate to Point of Sale > Transaction History
28. Find the anonymous payment you just created
29. Click "Issue refund" on the payment line item
30. Verify that "Account credit" does NOT appear in the "Transaction type"
   dropdown (only payment types like Cash, Check, etc. should appear)
31. This confirms anonymous transactions cannot have account credits since
    there's no patron account to credit

Expected results:
- Patron transactions show "Account credit" option in refund dropdown
- Anonymous transactions do NOT show "Account credit" option
- AC refunds create only REFUND credit (no PAYOUT debit)
- Non-AC refunds create both REFUND credit and PAYOUT debit
- Run: prove t/db_dependent/Koha/Cash/Register/Cashup.t

Signed-off-by: Laura Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:26 +00:00
Jason Robb e110ef135c
Bug 43046: Checkbox labels in Currencies and EDI accounts have unnecessary colons
Removes unnecessary colons when adding currencies and edi accounts.

1. Enable EDIFACT syspref
2. Navigate to Admin > Currencies.. > New currency
3. Note unnecessary colons after checkbox labels
4. Navigate to Admin > EDI accounts > New account
5. Note unnecessary colons after checkbox labels
6. Apply patch
7. Repeat steps 2 and 4, note colons have been removed

Signed-off-by: Kris Becker <kbecker@jcls.org>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:25 +00:00
Baptiste Wojtkowski b5fc521017
Bug 39208: Remove xhtml2pdf from printoverdues.sh
Note: this requires a command line access to the test machine

TEST PLAN:
1 - Anywhere on the computer running koha, run following command:
mkdir tmp
echo '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><p>Тетрис — это классика!</p></body></html>' > tmp/tmp.html
echo '* { color: red };' > tmp.css
./misc/cronjobs/printoverdues.sh tmp tmp.css
2 - Open the pdf in tmp/tmp.pdf, notice it is broken
3 - Apply patch
4 - Rerun ./misc/cronjobs/printoverdues.sh tmp tmp.css
5 - Open the pdf in tmp/tmp.pdf, notice it is now displaying properly, notice the css property still applies

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:24 +00:00
Phil Ringnalda b1b12578e0
Bug 42920: Looking up authority records in Advanced editor shouldn't append spaces
The patch for bug 21453 replaced an Advanced editor-specific (in the "[% IF rancor %]" block) "[%- END -%]‡9[% authid | html %]";" which trims whitespace with several lines of  "[% ... %]" which do not trim whitespace. The result is that when you look up an authority from the Advanced editor, you get extra whitespace between the last subfield from the authority record and the $9 field with the authority id.

Test plan:
 1. Without the patch, set the preference EnableAdvancedCatalogingEditor to Enable
 2. Cataloging - Advanced editor
 3. Click in any line of the editor and press Return to get a blank line
 4. Type 655 and press Tab three times
 5. Type Ctrl+Shift+L to open the authority search popup
 6. Click Search (no need for search terms) and click Choose for any record
 7. Note that the editor shows twenty blank spaces between the end of the subfield a
    contents and the ‡9
 8. Apply patch
 9. Repeat steps 3-6, note that now there is no extra space between the end of the
    content and the ‡9

Sponsored-by: Chetco Community Public Library
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:23 +00:00
Martin Renvoize 04381f6aba
Bug 41358: Apply consistent JSON logging to holds (Bug 40866)
Bug 40866 introduced JSON logging for HOLDS CREATE but stored null for
'confirmations' and 'forced' when no overrides were present. This patch
mirrors the earlier change to CIRCULATION ISSUE by defaulting them to
empty arrays, keeping the two log streams structurally identical and
simplifying reporting queries.

Changes:

C4/Reserves.pm:
- Default confirmations/forced to [] in the AddReserve log payload.

t/db_dependent/Reserves.t:
- Update the Bug 40866 subtest to assert the empty-array defaults on
  normal holds, and fix a 'my $log_data' redeclaration.

tools/viewlog.pl:
- Extend the CSV export JSON-decode block to also cover HOLDS logs,
  guarding the item lookup on itemnumber being present (HOLDS payloads
  may lack one) and filling the CSV item columns from the decoded
  payload. The legacy bare-itemnumber fallback is restricted to
  CIRCULATION so a legacy HOLDS hold_id is not mistaken for an
  itemnumber.

koha-tmpl/intranet-tmpl/prog/js/viewlog.js:
- Extend the info column override rendering (renderCircInfo) to HOLDS
  entries so forced/confirmations arrays display translated, exactly as
  they do for CIRCULATION. The log viewer moved to an ajax datatable in
  bug 29016, so this replaces the earlier json_found change to
  viewlog.pl/viewlog.tt.

misc/maintenance/migrate_action_logs_to_json.pl:
- Extended to handle legacy pre-Bug 40866 HOLDS CREATE logs where info
  is a bare hold_id, recovering branchcode / biblionumber / itemnumber
  from old_reserves / reserves.
- Uses the same keyset-pagination pattern as the circulation loop so
  rows are not skipped under --commit.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:22 +00:00
Martin Renvoize 507545734e
Bug 41358: Log forced overrides for offline and ILL checkouts
This patch updates offline circulation and ILL checkouts to properly
record their nature in circulation logs as forced transactions.

Offline circulation (process_koc.pl):
- Offline transactions have already occurred without access to normal
  circulation checks
- Now marks these as forced with 'OFFLINE_CIRCULATION' override code
- This helps identify historical transactions that bypassed checks

ILL checkouts (Koha/ILL/Request.pm):
- ILL checkouts are system-initiated and may bypass certain restrictions
  by design
- Now marks these as forced with 'ILL_CHECKOUT' override code
- This helps track and report on ILL circulation activity separately

Display updates (action-logs.inc):
- Added translation strings for both new override codes:
  - OFFLINE_CIRCULATION: "Offline circulation checkout"
  - ILL_CHECKOUT: "ILL system checkout"
- These will now display properly in Tools > Log viewer

This ensures these special checkout types are properly identified in
reporting and analysis, making it easier to understand the context
of historical transactions.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:21 +00:00
Martin Renvoize c64aea8fcc
Bug 41358: Add migration script for legacy circulation logs
This patch adds an out-of-band migration script to convert legacy
CIRCULATION ISSUE action logs from the old itemnumber-only format
to the new consistent JSON format.

The script:
- Finds all CIRCULATION ISSUE logs where info is just an itemnumber
- Converts them to JSON format with the standard structure
- Attempts to recover issue_id and branchcode from old_issues/issues
- Runs in dry-run mode by default (--commit flag to actually update)
- Processes in batches to handle large databases safely
- Includes verbose mode for detailed output

Usage:
  perl misc/maintenance/migrate_action_logs_to_json.pl --help
  perl misc/maintenance/migrate_action_logs_to_json.pl --verbose
  perl misc/maintenance/migrate_action_logs_to_json.pl --commit

This is designed as an out-of-band maintenance script rather than
an atomic update because:
1. May be expensive on large databases with many log entries
2. Migration is optional - display code handles both formats
3. Can be run during off-peak hours at admin's discretion

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:20 +00:00
Martin Renvoize 8a9fb57a93
Bug 41358: Always store circulation logs in consistent JSON format
Previously, CIRCULATION ISSUE logs would store just the itemnumber when
there were no confirmations or forced overrides, but would store a full
JSON structure when overrides were present. This inconsistency makes
reporting difficult as queries need to handle both formats.

This patch changes AddIssue to always store logs in JSON format,
matching the pattern already used in AddRenewal. The JSON structure
now consistently includes:
- issue: issue_id
- branchcode: branchcode
- itemnumber: itemnumber
- confirmations: array of confirmation codes (empty array if none)
- forced: array of forced override codes (empty array if none)

Benefits:
- Consistent data format simplifies reporting and analysis
- Matches existing RENEWAL logging pattern
- Display code (tools/viewlog.pl) already handles both formats,
  so backward compatibility is maintained

Test plan:
1. prove t/db_dependent/Circulation.t
2. Perform a normal checkout - verify action log contains JSON
3. Perform a checkout with overrides - verify confirmations/forced
   are populated in the JSON
4. Check Tools > Log viewer - both types should display correctly

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:19 +00:00
Nick Clemens 2e4b527782
Bug 42512: Pass already checked/cleaned MARC::Record to Metadata Extractor
To test:
1 - In Koha testing docker record 369 has invalid xml, otherwise corrupt a record like:
    sudo koha-mysql kohadev
    UPDATE biblio_metadata SET metadata = UpdateXML(metadata, '//datafield[@tag="040"]/subfield[@code="c"]',CONCAT('<subfield code',CHAR(27),'="c">OSt</subfield>')) WHERE
 biblionumber=438;
2 - View the record in the staff client - note bad metadata message
    http://localhost:8081/cgi-bin/koha/catalogue/detail.pl?biblionumber=438
3 - Click 'Edit'
4 - Error 500, in logs:
    Invalid data, cannot decode metadata object
5 - Apply patch, restart all
6 - Reload record and click edit
7 - Success! Message about bad metadata, but can edit/save

Signed-off-by: Phil Ringnalda <phil@chetcolibrary.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:18 +00:00
Jan Kissig c0c0b6bbfe
Bug 42986: Fix Uncaught TypeError on paycollect
This patch introduces a check if the variable change is defined, before its property innerHTML is accessed.
When calling "Write off selected" an Uncaught TypeError is thrown on form submission when accessing change.innerHTML as the variable change is undefined.
This regression was introduced by bug 42466 in a new submit handler which did not check if 'change' was defined at all.

Test plan:
1. In the staff interface, go to a patron's account
2. Go to Accounting > Create manual invoice
3. Fill out the manual invoice form with an amount
4. Click Save
5. Go to Make a payment
6. Click "Write off selected" (not write off on a single row)
7. IMPORTANT : Open the browser console
8. Click Confirm
9. apply patch and redo steps 1 - 8.

Signed-off-by: Caroline Cyr La Rose <caroline.cyr-la-rose@inlibro.com>
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:17 +00:00
Owen Leonard abdfe256ec
Bug 42568: Match maxlength attributes to marc_order_accounts column sizes
This patch adds a maxlength attribute to a few form fields in the MARC
order accounts entry form. Text inputs should now allow the user the
enter more characters than the db will accept.

To test, apply the patch and enable the MarcOrderingAutomation system
preference if necessary.

- Go to Administration -> MARC order accounts -> New account.
- Test these inputs to confirm that you can't enter more characters than
  their maxlengths:
  - Description (250)
  - Match field (10)
  - Match value (50)
  - Basket name field (10)

Sponsored-by: Athens County Public Libraries
Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl>
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:16 +00:00
Lucas Gass c5b39eb684
Bug 41604: (QA follow-up) Remove unused JS variable
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:15 +00:00
Lucas Gass d74dea01ac
Bug 41604: (QA follow-up) Add a semicolon
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:14 +00:00
Jonathan Druart 1abfc554f0
Bug 41604: Correctly restore alert when bKohaColumnsUseNames is off
If we define visibility_condition we must have implemented
bKohaColumnsUseNames correctly.

Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:13 +00:00
Jonathan Druart 26d0124c6b
Bug 41604: Hide column if the feature is disabled
Example: supplier_report on the basket page
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:12 +00:00
Jonathan Druart 2a34413601
Bug 41604: Save user decision per column when visibility changes
- Modify column-visibility.dt event handler to store the visibility
  preference of the user
- Improve _dt_force_visibility() to correctly pick the correctly visibility value depending
  on visibility_condition, is_hidden, and the user preference
- Add _dt_get_expected_column_visibility() to guess what's the expected visibility
- Force boolean true/false (vs 1/0) using !!
- Rely on a global variable kohaTableUserChoices to store the user
  choice. It will be part of the state and store in localStorage.

Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:10 +00:00
Jonathan Druart 85ccb3d4d7
Bug 41604: Add one more Cypress test
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:09 +00:00
Jonathan Druart 4dab120d32
Bug 41604: Add Cypress tests
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:08 +00:00
Lucas Gass d20f7bc7a7
Bug 41604: (follow-up) Fix the visibility when visibility_condition is 0 or null, instead of false
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:07 +00:00
Jonathan Druart f1160e7a70
Bug 41604: Fix force display of a column
If a column must be displayed but was hidden before

Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:06 +00:00
Jonathan Druart 6e5ee1fdf7
Bug 41604: Fix typo
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:05 +00:00
Jonathan Druart 060c8918d2
Bug 41604: QA fixes
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:04 +00:00
Jonathan Druart 3efad3f74a
Bug 41604: take visibility_condition when save state is disabled
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:03 +00:00
Jonathan Druart 4d2f963f1c
Bug 41604: Use visibility_condition in other places
There are 3 other places where we force the visibility of columns. They
must als use this new visibility_condition flag, so they are all using
it.

Test plan:
The 3 tables are:
1. basket with the columns edifact, modify, cancel
2. hold history with the column itemtype
3. batch edit patron with the columns checkbox, password expiration,
   protected

Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:02 +00:00
Jonathan Druart 7c688774f1
Bug 41604: Centralize visibility condition in datatables.js
We want to hide column if:
 * the feature is not accessible (pref disabled or insufficient
   permissions)
or
 * the column is hidden in the settings
or
 * the column has been hidden by the user

Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:54:01 +00:00
Hammat Wele 306ebc18ea
Bug 41604: Impossible to hide Checkin column in issues-table in circ/circulation.pl
Bug 41314 caused a regression, making it impossible to hide the Checkin column in the issues table in the patron's circulation page.

To recreate:
1. Hide the checkin column by default in circulation issues-table
   1.1. Go to Administration > Table settings > Circulation tab > Jump to page : circulation > Table id: issues-table
   1.2. Check 'Is hidden by default' for 'checkin'
   1.3. Click 'Save'
2. logout and login
3. Checkout an item to a patron
   3.1. In the search bar, set to Checkout, search for Henry
   3.2. In the checkout field, enter the barcode 39999000010114 and press enter
   --> In the table of issues, Checkin column is visible, it should be hidden by default
4. Apply the patch
5. Repeat step 2, 3
   --> In the table of issues, Checkin column is not visible

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-16 10:53:57 +00:00
Lari Taskula fe312b32d1
Bug 41796: "Forgot your password" link is not visible if OpacResetPassword is enabled but OpacPasswordChange is disabled
Before applying this patch, to reproduce the issue:
1. Set system preference OpacResetPassword to 'not allowed'
2. Set system preference OpacPasswordChange to 'Don't allow'
3. Set preference opacuserlogin to 'Allow'
4. Navigate to OPAC
5. Observe no "Forgot your password?" link on the OPAC main page
6. Set system preference OpacResetPassword to 'allowed'
7. Observe no "Forgot your password?" link on the OPAC main page
8. Set system preference OpacPasswordChange 'Allow'
9. Observe "Forgot your password?" link is present on the OPAC main page.

Instead, it should be already present when OpacResetPassword is allowed.

To test this patch:
1. Apply this patch
2. Set system preference OpacResetPassword to 'not allowed'
3. Set system preference OpacPasswordChange to 'Don't allow'
4. Set preference opacuserlogin to 'Allow'
5. Navigate to OPAC
6. Observe no "Forgot your password?" link on the OPAC main page
7. Set system preference OpacResetPassword to 'allowed'
8. Observe "Forgot your password?" link is present

Signed-off-by: Bo Gustavsson <bosse@gustavsson.one>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:41 +00:00
Laura_Escamilla 787eb80e9d
Bug 42339: Make Record display customization cancel return to record display list
1. Go to the staff interface.
2. Go to Tools > Additional contents.
3. Select the Record display customizations tab/category.
4. Add a new Record display customization, or edit an existing one.
5. Click Cancel.
6. Confirm you are returned to:
   /cgi-bin/koha/tools/additional-contents.pl?category=record_display
7. Confirm you are not returned to the HTML customizations category.
8. Repeat with another Additional content category, such as Pages, and
   confirm its Cancel behavior is unchanged.
9. Sign off and have a great day! :D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:40 +00:00
Jonathan Druart 1ed63c079d
Bug 42735: Fix SQL injection
This change fixes an SQL injection in the circulation stats.

Test plan:
0. Apply the patch
1. sudo koha-plack --restart kohadev
2. Go to /cgi-bin/koha/reports/issues_stats.pl
3. Try out a bunch of options
4. Try to break it
5. Note that you just get output and no errors

Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:38 +00:00
Jonathan Druart 9e39e68a98
Bug 42369: Use C4::Context::csv_delimiter for delimiter validation
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:37 +00:00
David Cook 310201b520
Bug 42369: Fix SQL injection
This change fixes SQL injection in bor issues top

Test plan:
0. Apply patch
1. sudo koha-plack --restart kohadev
2. Go to /cgi-bin/koha/reports/bor_issues_top.pl
3. Try out all the different options
4. Try to break it
5. Note that you get output and no errors

Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:36 +00:00
Jonathan Druart 44c7c10a19
Bug 42368: Use C4::Context::csv_delimiter for delimiter validation
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:35 +00:00
David Cook 0af6fbb7e7
Bug 42368: Fix SQL injection in issue avg stats
This change fixes SQL injection in issues avg stats

Test plan:
0. Apply the patch
1. sudo koha-plack --restart kohadev
2. Go to /cgi-bin/koha/reports/issues_avg_stats.pl
3. Try out all the different options
4. Try to break the report
5. Note that you always get output instead of errors

Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:34 +00:00
Jonathan Druart 70f6545a53
Bug 42363: Use C4::Context::csv_delimiter for delimiter validation
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:33 +00:00
David Cook e7f36c5b87
Bug 42363: Fix SQL injection
This change cixes an SQL injection in cat stats

Test plan:
0. Apply patch
1. sudo koha-plack --restart kohadev
2. Go to /cgi-bin/koha/reports/catalogue_stats.pl
3. Try out different values
4. Try to break the report
5. Note that you just get output and not errors

Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:32 +00:00
Jonathan Druart 5827f2f774
Bug 42360: Move separator validation to C4::Context::csv_delimiter
So we protect other calls

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:31 +00:00
David Cook 1d9c8fd390
Bug 42360: Also validate sep param
Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:30 +00:00
David Cook 033acbe94e
Bug 42360: Fix SQL injection
This change fixes an SQL injection in the acq stats.

Test plan:
0. Apply the patch
1. sudo koha-plack --restart kohadev
2. Go to /cgi-bin/koha/reports/acquisitions_stats.pl
3. Try out a bunch of options
4. Try to break it
5. Note that you just get output and no errors
NOTE: In a fresh KTD you likely don't have any actual statistics

Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:28 +00:00
Martin Renvoize 7a10a59e3d
Bug 43033: Sort unallocated task indices before compacting the retry matrix
The previous two commits on this bug fixed two ordering assumptions in
the RETRY loop of _allocateWithTransportCostMatrix(): resetting $inf at
the start of each attempt, and splicing unallocated task indices out of
@requests in descending order (removing a lower index first would shift
the position of any higher index still waiting to be removed).

This is a third instance of the same underlying problem, a few lines
earlier in the loop that compacts the working cost matrix $m before
that splice runs. @unallocated is built by iterating over agents (item
rows), pushing the column index Munkres assigned to each row that
turned out infeasible - and Munkres' row-to-column assignment is a
permutation, not a monotonic function of the row index, so
@unallocated is no more reliably sorted here than it was for the
splice.

That compaction loop scans matrix columns in ascending order and
advances a single pointer into @unallocated, expecting to meet each
entry in that same ascending order. When @unallocated arrives out of
order, the pointer never catches up: columns that should have been
dropped are left in place, and the columns after them are never
shifted left to fill the gap. From that point on, $m no longer lines
up with @requests, which *is* correctly reduced by the splice - so on
the next retry pass a hold's transport cost gets evaluated using a
different hold's matrix row. Depending on the exact misalignment, this
can silently drop a fillable hold from the queue while a genuinely
available item sits unused, or fill a hold via the wrong-cost item.

It didn't show up in the "many holds" regression test added earlier on
this bug, because that test's transport cost matrix is uniform (every
disabled pair costs the same); this defect only surfaces once the
columns competing for a scarce item have distinct costs.

Sort @unallocated once, ascending, and use that copy for the
compaction pass; reverse it for the splice, which needs descending
order.

Test plan:

1. Run the new t/db_dependent/HoldsQueue/RetryAllocationOrdering.t -
   it calls _allocateWithTransportCostMatrix() directly with a small
   hand-built scarce transport cost matrix and a pinned srand() (the
   specific item-to-branch permutation needed to trigger the bug
   depends on the shuffle() inside the routine). Reverting just this
   patch makes 3 of its 6 subtests fail: the L2 pickup hold is dropped
   even though an available item could have filled it.
2. Run t/db_dependent/HoldsQueue/TransportCostOptimizations.t to
   confirm no regression in the existing retry-loop coverage.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:27 +00:00
Andreas Jonsson f68c481391
Bug 43033: retry allocation with valid items
* The representation of the value "infinity" must be reset to -1 at the
  start of each allocation attempt.
* Unallocated items must be spliced out from the candidate arrays in
  reverse index order.

Test plan:

1. Run the unit tests in
   t/db_dependent/HoldsQueue/TransportCostOptimizations.t

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:26 +00:00
Nick Clemens f94cfcfee4
Bug 43033: Test with a lot of holds
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:24 +00:00
Martin Renvoize 611328b5db
Bug 42430: (QA follow-up) Fix nested plugin package corruption on reload
InstallPlugins forces every discovered plugin class to be deleted and
re-required from disk on every call, so plugin_methods reflects the
.pm file as it currently exists rather than whatever was already
loaded into a long-lived Plack worker. That deletion used
Symbol::delete_package, which clears the target package's whole
symbol table -- including any already-loaded packages nested under it
(e.g. a REST API controller a plugin bundles alongside itself, such as
Koha/Plugin/Foo/Controller.pm). Those nested classes keep their own
%INC entry, so they are never re-required afterwards and are left
permanently blanked out for the rest of the worker's life.

This was caught when running
`prove -r t/db_dependent/Koha/Plugins/ t/db_dependent/Koha/REST/Plugin/`
on top of the original patch. Tests fail several files that have nothing
to do with the bug being fixed (Biblio_and_Items_plugin_hooks.t,
Holds_hooks.t, TemplateIncludePathHook.t, REST/Plugin/PluginRoutes.t),
because Koha::Plugin::Test::Controller (a bundled test-fixture
controller living in t/lib/plugins/Koha/Plugin/Test/Controller.pm)
gets its symbol table wiped as collateral damage every time
Koha::Plugin::Test is reloaded.

_forget_plugin_class() replaces the delete_package call with the same
technique used by the CPAN module Class::Unload (not added as a
dependency here, just the technique): only clear the target class's
own symbols and reset its @ISA, explicitly leaving any nested package
entries in its symbol table alone. Module::Pluggable discovers bundled
nested classes as their own entries in the plugin list regardless, so
they still get reloaded correctly on their own turn.

I also annotate the no strict 'refs' block added by the previous commit
with a '## no critic' justification, as flagged by koha-qa.pl.

Test plan:
1. Confirm the bug exists without this patch:
   a) git checkout <previous commit> (drop this commit, keeping the
      first 3 commits of the bug applied)
   b) Inside KTD:
      prove -r t/db_dependent/Koha/Plugins/ t/db_dependent/Koha/REST/Plugin/
   c) Observe failing files unrelated to this bug's own tests:
      Biblio_and_Items_plugin_hooks.t, Holds_hooks.t,
      TemplateIncludePathHook.t and REST/Plugin/PluginRoutes.t.
2. Confirm the fix:
   a) Re-apply this commit
   b) Re-run the same command from 1(b) -- all tests now pass
3. Confirm the original bug this branch set out to fix is still fixed,
   both via the manual UI steps from the first commit's test plan
   (install Batch Permission Modifier Plugin 1.3.1, upload 1.4.1 over
   it via Tools > Plugins, confirm the renamed method takes effect
   without restarting Plack) and via:
   prove t/db_dependent/Koha/Plugins/Plugins.t
   (see the 'InstallPlugins() reloads stale symbol tables tests'
   subtest)
4. Confirm QA tooling is clean:
   koha-qa.pl -c 4
   => PASS for Koha/Plugins.pm and all touched test files, no critic
      or tidiness failures.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:23 +00:00
Nick Clemens c1c733fc08
Bug 42430: (follow-up) Add missing POD
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:22 +00:00
Kyle M Hall fff6a7574c
Bug 42430: Add unit tests
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:21 +00:00
Kyle M Hall ef26c1acaa
Bug 42430: Fix issue with stale plugin methods after plugin upgrade
When a plugin is upgraded in place (by uploading a new version over
the old one), the list of methods Koha knows about for that plugin
can get out of date. For example, if a plugin used to have a "tool"
method and the new version renames it to "admin", the staff interface
can still show the old "Run tool" link, and clicking it produces an
error saying the plugin has no "tool" method.

This happens because the plugin upload runs inside a long-lived
Plack worker that has already loaded the old version of the plugin
in memory. When Koha scans the plugin to record its methods, Perl
has no reason to re-read the file from disk, so the scan sees the
old method list and stores that in the plugin_methods table. The
plugin code on disk is new, but the record of what methods it has
is stale.

This patch forces Koha to throw away the in-memory copy of each
plugin and reload it from disk before scanning, so the recorded
list of methods matches the plugin that is actually installed.

Test Plan:
1) Install the Batch Permission Modifier Plugin version 1.3.1
(this version exposes a "tool" method)
2) Go to Tools > Plugins and confirm the plugin row shows
"Run tool" in its actions menu
3) Upload Batch Permission Modifier Plugin version 1.4.1 over
the top of 1.3.1 (this version exposes "admin" instead of
"tool")
4) Reload the Plugins page
5) Note the plugin still shows "Run tool" in its actions menu
6) Click "Run tool"
7) Note the error: the plugin has no "tool" method!
8) Apply this patch
9) Restart all the things!
10) Uninstall the plugin
11) Repeat steps 1-4
12) Note that "Run tool" is gone and "Run admin tool" appears
in its place
13) Click "Run admin tool" and confirm it loads without error!

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:20 +00:00
Martin Renvoize 6563255743
Bug 42995: (QA follow-up) Harden nested +count FK-chain resolution
The FK-chain resolution added in the previous patch only handles ONE
level of nesting: it requires $parent_alias to be a direct relationship
of $main_source. This holds for every +count embed currently declared
in api/v1/swagger/ (all exactly one level deep, e.g.
biblio.items+count), but if that assumption is ever violated - a
deeper nested +count, a parent relation with a coderef (custom join)
condition, or no matching FK column name - the previous code silently
fell back to the pre-fix expression ($parent_alias.$fk_self), which can
reference a join alias that isn't available inside DBIC's limiting
subquery, reproducing the original 500 with no diagnostic.

This patch makes _build_count_subquery decline to build a subquery
(return undef) when the correlation can't be safely resolved, instead
of emitting SQL that may be broken. The caller already has an existing,
tested degradation path for "no DBIC relationship info" (prefetch the
association directly; to_api falls back to the Perl-level ->count call;
sorting on it is rejected with a clean 400) - unresolved nested
correlations now go through that same path rather than risking a 500.

Also:
- Extract the repeated "_resultset vs _result" result-source lookup
  into a shared _result_source() helper (was duplicated in
  dbic_merge_prefetch and _build_count_subquery).
- Document the one-level-deep limitation in _build_count_subquery's POD.
- Add a regression test pinning the 2-level-nested-+count case: it must
  fall back to a plain prefetch (no COUNT subquery) rather than error
  or emit unsafe SQL.

Test plan:
1. ktd --shell
2. prove t/Koha/REST/Plugin/Query.t t/db_dependent/Koha/Object.t \
       t/db_dependent/Koha/REST/Plugin/Objects.t \
       t/db_dependent/api/v1/acquisitions_orders.t \
       t/db_dependent/api/v1/acquisitions_vendors.t \
       t/db_dependent/api/v1/ill_requests.t \
       t/db_dependent/api/v1/tickets.t \
       t/db_dependent/api/v1/patrons.t
=> All pass, no regressions.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:18 +00:00
Tomás Cohen Arazi f431fe0da0
Bug 42995: Fix nested +count subqueries with pagination and has_many
When nested +count embeds (e.g. biblio.items+count) are used alongside a
has_many prefetch (e.g. biblio.suggestions or the implicitly added
biblio.biblioitem) and pagination, DBIC's limiting subquery optimization
places +select scalar refs in the inner subquery where the parent join
alias (e.g. 'biblio') is not yet available.

This patch fixes _build_count_subquery to resolve the correlation back to
the main table (me) via the FK chain. Instead of generating:

  (SELECT COUNT(*) FROM items
   WHERE items.biblionumber = biblio.biblionumber)

It now generates:

  (SELECT COUNT(*) FROM items
   WHERE items.biblionumber = me.biblionumber)

This works because me.biblionumber IS the FK to biblio, holding the same
value. The main table columns are always available regardless of DBIC's
query structure.

Changes:
- Add $main_source parameter to _build_count_subquery for FK resolution
- Thread main_source through _parse_prefetch and dbic_merge_prefetch
- When parent_alias != 'me', resolve through parent relationship condition
- Update Query.t assertion to match the new correlation expression

Test plan:
1. Apply the regression tests from the previous commit
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/api/v1/acquisitions_orders.t
=> FAIL: The Bug 42995 subtest fails with 500 errors
3. Apply this patch, repeat from step 2
   k$ prove t/Koha/REST/Plugin/Query.t \
       t/db_dependent/Koha/Object.t \
       t/db_dependent/Koha/REST/Plugin/Objects.t \
       t/db_dependent/api/v1/acquisitions_orders.t \
       t/db_dependent/api/v1/acquisitions_vendors.t \
       t/db_dependent/api/v1/ill_requests.t \
       t/db_dependent/api/v1/tickets.t \
       t/db_dependent/api/v1/patrons.t
=> SUCCESS: Tests pass!
4. Sign off :-D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:17 +00:00
Tomás Cohen Arazi e560b85c6f
Bug 42995: (regression tests) Nested +count embeds with has_many prefetch
This patch adds regression tests for the 500 Internal Server Error
triggered when GET /api/v1/acquisitions/orders is called with nested
+count embeds (e.g. biblio.items+count) combined with a has_many
prefetch (e.g. biblio.suggestions.suggester) and pagination.

The root cause is that DBIC's limiting subquery optimization places
+select scalar refs in the inner subquery where the parent join alias
(biblio) is not yet available, resulting in:
  DBD::mysql: Unknown column 'biblio.biblionumber' in 'WHERE'

The regression was introduced in:
  commit e51334604f
  Bug 41950: Make +count embeds sortable via SQL COUNT subqueries

Test plan:
1. Apply this patch
2. Run:
   $ ktd --shell
   k$ prove t/db_dependent/api/v1/acquisitions_orders.t
=> FAIL: The new subtest fails with 500 errors instead of 200
3. Sign off :-D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-13 15:46:13 +00:00
Owen Leonard b0fa3d7e6a
Bug 42618: Incorrect sidebar menu link to MARC order accounts
The administration sidebar menu has an incorrect link for MARC ordering
accounts. Clicking the link takes you to EDI accounts. This patch fixes
it.

To test, apply the patch and run 'yarn build'

- Go to Administration -> Libraries (for example)
- In the sidebar menu, click the MARC ordering accounts link.
- You should be taken to the MARC ordering accounts page.

Sponsored-by: Athens County Public Libraries
Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-01 09:34:47 +00:00
Caroline Cyr La Rose 2b2b4b74ba
Bug 42885: FineNoRenewals description mentions fines rather than charges
This patch fixes the terminology in the FineNoRenewals system preference
description to use the term 'charges' instead of 'fines'.

To test:

1. Apply patch
2. Go to Administration > System preferences
3. Search for FineNoRenewals
4. Make sure the description is gramatically correct and doesn't have
   contain typos

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-01 09:34:46 +00:00
Chris Mathevet 4003a4aa90
Bug 42579: Added "no-export" class to ratings div
"Title" column of Checkout history export contains too much
informations, making the export file hard to read and understand.

Test plan :
1 - Connect on OPAC
2 - Go to "Your account"
3 - Go to "Checkout history"
4 - Click on "CSV"
5 - Open file
6 - Notice that the "title" column is too long and contains ratings
    informations
7 - Go back on OPAC
8 - Click on "Copy"
9 - Copy to a .txt file
10 - Notice the same problem as the CSV export
11 - Apply patch
12 - Repeat previous instructions
13 - Notice that the "title" column contains only the title

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-01 09:34:45 +00:00
Kyle M Hall 839f3492bc
Bug 42571: Sending EDI order results in variable not available warnings
The warnings look like:

[2026/05/08 15:02:42] [32918] [WARN] Variable "@messages" is not available at /usr/share/koha/intranet/cgi-bin/acqui/basket.pl line 698.
[2026/05/08 15:02:42] [32918] [WARN] Variable "$op" is not available at /usr/share/koha/intranet/cgi-bin/acqui/basket.pl line 704.

Note in the fix, we are switching to the "our" keyword. When we declare
things with "our", and we run this under plack, those variables and
values persist across multiple requests! Due to this behavior we always
need to reinitialize anything that uses "our" unless we specifically
want that behavior ( like caching ).

Test plan:
1) Send an EDI order, note the warnings in the logs
2) Apply this patch
3) Restart all the things!
4) Send another EDI order, note the warnings are gone!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-01 09:34:44 +00:00
Jonathan Druart 2472e423cb
Bug 42551: Fix QA failures
[FAIL] t/db_dependent/Koha/Plugins/C3.t
   FAIL   file_permissions
                File must have the exec flag
   FAIL   test_no_warnings
                'use Test::NoWarnings' must be present in this test file.

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-01 09:34:42 +00:00
Kyle M Hall c79b0b9ef8
Bug 42551: Skip preload when running perl with -c
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-01 09:34:41 +00:00
Kyle M Hall e48c124ac3
Bug 42551: Add unit test
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-01 09:34:40 +00:00
Lisette Scheer 9be8db9c8a
Bug 41889: Update API to allow for null patron_id to be returned when listing checkouts
If a patron is deleted and there's a checkout associated with them, an api call to {url}/api/v1/checkouts?checked_in=true
will result in an error. This patch updates the api to allow for a null value to be returned for patron_id

To test:
Before applying the patch:

1. In Administration->System preference enable the RESTBasicAuth system preference.
2. Checkout an item to a patron (I used item barcode: 39999000000252 and patron barcode 23529000121682)
3. Check the item back in
4. In your brower go to {sandbox/ktdurl}/api/v1/checkouts?checked_in=1
5. Observe there is a patron ID in the JSON response
6. Delete the patron
7. Repeat step 4
8. Observe an error because a "null" value was returned
9. Apply patch
   Sandbox:
       In sandbox management Actions->Apply patches
       Once complete Actions->Build CSS and JS
       Actions->Restart services
   KTD:
       ktd --shell
       git bz apply 41889
       yarn build
       restart_all

10. Repeat step 4
11. Observe the checkout loads, but now has a null patron id.
12. Sign off
On KTD can also run the script: qa
to run tests.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-01 09:34:39 +00:00
Jonathan Druart cb47e6fad7
Bug 42278: Fix JS error
Uncaught SyntaxError: redeclaration of non-configurable global property ClaimReturnedLostValue

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-07-01 09:34:35 +00:00
Jonathan Druart dd918e0de1
Bug 42522: Add link to biblio from eholding show view
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:19 +00:00
Jonathan Druart 4844c93a23
Bug 42290: Tidy all script tags - suggestion (auto tidy)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:17 +00:00
Jonathan Druart e4293f93d2
Bug 42290: Tidy all script tags - suggestion
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the suggestion module.

Trivial changes, the main changes have been done already, we only move
them to a separate script tag.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:16 +00:00
Jan Kissig 36e00f5aff
Bug 42283: (QA follow-up) Change icons
Changes the icon on concerns resolved filter to suit other filters set via AV TICKET_STATUS.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:14 +00:00
Paul Derscheid a4ec38d5fa
Bug 42283: (follow-up) URI-encode z3950 search params in GetZ3950Terms
The original template rendered each Z39.50 search param with the
"| uri" TT filter, so the generated query string was safe for
values containing spaces, ampersands or other reserved characters.

The JS port concatenates the raw values, producing a broken query
string when any search field (title, author, subject, etc.) is not
already URL-safe.

Wrap name and value in encodeURIComponent to restore the original
behaviour.

To test:
- Import or create a biblio whose title or author includes a space
  and an ampersand (e.g. "Q & A: Practical tests").
- From the cataloging search results, trigger the Z39.50 search for
  that record.
- Confirm the request URL includes correctly encoded parameters
  (space as %20, & as %26) and that the Z39.50 results load.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:13 +00:00
Paul Derscheid c7422c5c78
Bug 42283: (follow-up) Fix checkbox selector in concerns status filter
Porting the per-status click handler out of the TT loop dropped the
"hide_" prefix on the checkbox selectors:

- the `is(":checked")` test looked at `#<av>` instead of
  `#hide_<av>`, so the branch that removes the filter never fired
- the toggle line read `!$("#hide${av}")` (no underscore) before
  writing `#hide_<av>`, so the checkbox state was always forced to
  true and further clicks stopped toggling

Restore `#hide_${av}` in both places so the filter toggles match the
original TT-rendered selectors (IDs `hide_<av>` on the hidden
checkboxes).

To test:
- Set up TICKET_STATUS authorised values and enable CatalogConcerns
  and OpacCatalogConcerns.
- Create concerns in multiple statuses, open
  /cgi-bin/koha/cataloguing/concerns.pl.
- Click a "Filter <status>" link: rows with that status disappear.
- Click the same link again: it flips back to "Include <status>"
  and rows reappear. Repeat across several statuses.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:12 +00:00
Jonathan Druart 2cf1f980d0
Bug 42283: Tidy all script tags - cataloguing (auto tidy)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:11 +00:00
Jonathan Druart 0aeb26417e
Bug 42283: Tidy all script tags - cataloguing
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the cataloguing module.

The 3 main changes are in the following files:
* addbooks.tt
* additem.tt
* concerns.tt

There are non-trivial changes in concerns.tt. You need to create several
authorised values for TICKET_STATUS, turn on CatalogConcerns and OpacCatalogConcerns, create several concerns and go to /cgi-bin/koha/cataloguing/concerns.pl
Confirm that the filters are working correctly

Note that cataloguing/addbiblio.tt is on a separate bug (bug 42284)

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:10 +00:00
Jan Kissig 5988883898
Bug 42282: (QA follow-up) Split JS to TT block in results, auto tidy
Splits JS to TT and non TT block. Auto tidy

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:09 +00:00
Jan Kissig c9bfc66026
Bug 42282: (QA follow-up) Split JS to TT block in advsearch, auto tidy
Splits JS to TT and non TT block. Auto tidy

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:08 +00:00
Jan Kissig a0c2e80678
Bug 42282: (QA follow-up) restore searchid in stockrotation
This patch sets the TT param searchid to enable browsing though search results on stockrotation.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:07 +00:00
Jan Kissig 3bf91a73dc
Bug 42282: (QA follow-up): Fix call of HTML5MediaEnabled and HTML5MediaSets.size
This switches the assignment of Koha.prefs.HTML5MediaEnabled from a non existant template param HTML5MediaEnabled to Koha.Preference('HTML5MediaEnabled').
Also a straying template param HTML5MediaSets.size was left in a JS block.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:05 +00:00
Jonathan Druart 8147f968aa
Bug 42282: Avoid JS error
Fix conflict with "Bug 42177: bundlesEnabled fix"
SyntaxError: redeclaration of const bundlesEnabled

Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:04 +00:00
Paul Derscheid 9db99a08e2
Bug 42282: (follow-up) Switch Coce timeout log to console.warn
qa script flags the stray console.log in the Coce wait-for-images
helper under forbidden_patterns. The line is a genuine soft-failure
diagnostic (Coce never resolved after 5s), matching the
console.warn pattern used for AJAX failures elsewhere in the staff
templates.

To test:
- qa script no longer flags forbidden_patterns on detail.tt.
- With IntranetCoce enabled but Coce unreachable, loading a biblio
  logs "Could not retrieve the images" as a warning after ~5s.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:03 +00:00
Paul Derscheid 774c8d1e94
Bug 42282: (follow-up) Fix broken mailto concatenation in Elasticsearch 404 modal
The port folded the admin email into a single string literal
    " <a href='mailto: + Koha.prefs.KohaAdminEmailAddress'>"
so the href became the literal text "mailto: + Koha.prefs..." instead
of mailto:<admin email>.

Close the string and concatenate Koha.prefs.KohaAdminEmailAddress.

To test:
- Set KohaAdminEmailAddress syspref.
- SearchEngine = Elasticsearch, open a biblio detail page.
- Trigger the Elasticsearch preview with a bad id so the 404 branch
  renders, and confirm the "Send email" link points at mailto:<email>.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:02 +00:00
Jonathan Druart f645702cd8
Bug 42282: Tidy all script tags - catalogue (auto tidy)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:01 +00:00
Jonathan Druart ee299e4b86
Bug 42282: Tidy all script tags - catalogue
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the catalogue module.

The 7 main changes are in the following files:
* ISBDdetail.tt
* MARCdetail.tt
* detail.tt
* imageviewer.tt
* labeledMARCdetail.tt
* moredetail.tt
* stockrotation.tt

Several identical changes (logged_in_user_borrowernumber, searchid,
biblio_id, browser, etc.)
detail.tt needs to be tested a bit more thoroughly but the changes are
still trivial.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:20:00 +00:00
Owen Leonard ea62f3b835
Bug 40610: Update OPAC templates to improve ease of translation
This patch updates the markup structure of some OPAC include files in
order to make the translation process easier.

To test, apply the patch and test that the correct strings are
translatable. In this example I'm testing fr-FR:

- Update a translation:

  > gulp po:update --lang fr-FR
  > perl misc/translator/translate update fr-FR

- Open the corresponding .po file for the strings pulled from
  the include file and locate the updated strings.

  - misc/translator/po/fr-FR-messages.po
    - opac-tmpl/bootstrap/en/includes/biblio-title.inc lines 4 & 5
    - opac-tmpl/bootstrap/en/includes/date-format.inc lines 4, 6, 8
    - opac-tmpl/bootstrap/en/includes/holds-table.inc lines 132 & 135
    - opac-tmpl/bootstrap/en/includes/masthead.inc line 498

  - misc/translator/po/fr-FR/fr-FR-opac-bootstrap.po
    - opac-tmpl/bootstrap/en/includes/biblio-title-head.inc
    - opac-tmpl/bootstrap/en/includes/holds-table.inc line 129 & 141

- Edit the "msgstr" string however you want (it's just for testing).
- Remove the "fuzzy, " text if it's there.
- Install the updated translation:

  > perl mis/translator/translate install fr-FR

- Switch to your translated language in the OPAC.
- Confirm that the strings you translated show up correctly on these
  pages:
  - The page title when viewing a bibliographic record detail page for
    a record without a title.
  - On a user summary page, under the hold tab:
    - A hold that is waiting
    - A hold that is in transit
- In Administration -> Identity providers define a new identity provider
  (it doesn't have to be functional).
  - In the OPAC, test that the "Log in with [identity provider]" link is
    correct in each of the three login forms:
    - The modal triggered from the link in the header
    - The form in the right-hand sidebar
    - The form you see if you navigate directly to a user-related page
      e.g. /cgi-bin/koha/opac-user.pl
- The changes to date-format.inc are only visible if JS is turned off
  because Flatpickr hides the format hint when it loads.

Sponsored-by: Athens County Public Libraries
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:59 +00:00
Jan Kissig f7941aee28
Bug 42278: (QA follow-up) Fix HIGHHOLDS to JSON serialisation
Using Koha::Template::Plugin::To to serialize HIGHHOLDS to JSON led to the following error:  Template process failed: undef error - encountered object '2026-05-31T23:59:00', but neither allow_blessed, convert_blessed nor allow_tags settings are enabled (or TO_JSON/FREEZE method missing) at /kohadevbox/koha/Koha/Template/Plugin/To.pm line 32.
This patch fixes the handling of HIGHHOLDS.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:57 +00:00
Jan Kissig e0aee23fc1
Bug 42278: (QA follow-up) Fix Uncaught TypeError in request-article
This fixes an Uncaught TypeError when searching for a patron and the search only returns a single patron.

To test:
 a) go to /cgi-bin/koha/circ/request-article.pl?biblionumber=262
 b) ienable dev tools console
 c) enter 'acev' and click Search
 d) The following error appears: Uncaught TypeError: can't access property "on", patrons_table is undefined
 e) apply patch and redo a) to c). The error is not thrown.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:56 +00:00
Jan Kissig c379c8301d
Bug 42278: (QA follow-up): Split JS to separate TT and tidy
This puts ArticleRequestsSupportedFormats in a separate script tag. Auto tidy

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:55 +00:00
Paul Derscheid 0361fee0e4
Bug 42278: (follow-up) Guard bare-RHS TT interpolations
Several unquoted const assignments rendered to `const X = ;` when
their TT expression was empty (patron.borrowernumber without a
patron, CAN_user_circulate_manage_bookings absent, or the
AlwaysLoadCheckoutsTable syspref unset). The parser aborted the
whole script block, so has_patron, AlwaysLoadCheckoutsTable, etc.
were never defined and later scripts failed with ReferenceError.

Add TT fallbacks so the RHS is always a valid JS literal.

To test:
- /cgi-bin/koha/circ/circulation.pl with no patron → no console errors
- Search a patron, check out an item → still works

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:54 +00:00
Paul Derscheid 72443454b2
Bug 42278: (follow-up) Restore findborrower autofocus when no patron loaded
The JS port inverted the original UNLESS condition, so the patron
search field was focused only after a patron was already selected.
Flip back to !borrowernumber.

To test:
- /cgi-bin/koha/circ/circulation.pl with no patron → cursor in findborrower
- Select patron, reload → no focus jump

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:53 +00:00
Jonathan Druart 7cca8e81a2
Bug 42278: Tidy all script tags - circ (auto tidy)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:52 +00:00
Jonathan Druart 650a8d8a64
Bug 42278: Tidy all script tags - circ
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the circulation module.

The 4 main changes are in the following files:
* circulation.tt
As expected this one is messy to test, we need to look at the diff and
make sure the different features linked with the variables moved at
still working correctly (yes, it's vague!)
* circulation_batch_checkouts.tt
On the batch checkout table the due date hidden input should have the
value of the return date high demand items.
* overdue.tt
On the overdue report view, the "Return claims" column must be hidden if
ClaimReturnedLostValue is not set.
* transfers_to_send.tt
The transfers to send view should show all libraries

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:51 +00:00
Pedro Amorim acfd02af12
Bug 41950: (QA follow-up): Add more tests
The tests were lacking a 'real' scenario proving the
sorting on a _counts works at the DB level

prove t/Koha/REST/Plugin/Query.t

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:50 +00:00
Tomás Cohen Arazi 713c5c3343
Bug 41950: (follow-up) Add DBIC relationship aliases for +count embeds
Adds DBIC relationship aliases so that +count embeds advertised in
the API specs are backed by actual database relationships, making
them sortable via the SQL COUNT subquery infrastructure.

New aliases:
- Borrower: checkouts (issues)
- Aqbookseller: baskets (aqbaskets), invoices (aqinvoices)
- Ticket: updates (ticket_updates)
- ErmEholdingsPackage: resources (erm_eholdings_resources)
- Biblio: holds (reserves)

Koha methods are updated to use the new relationship names directly,
following the one-liner style: Koha::Foo->_new_from_dbic( scalar ... )

Note: checkouts on Borrower and subscriptions on Aqbookseller already
had matching DBIC relationships. comments on Illrequest, requests on
IllBatch, and items on Biblio also already existed.

Test plan:
1. Apply patch
2. Run:
   $ ktd --shell
   k$ prove t/Koha/REST/Plugin/Query.t \
       t/db_dependent/api/v1/patrons.t \
       t/db_dependent/api/v1/acquisitions_vendors.t \
       t/db_dependent/Koha/REST/Plugin/Objects.t \
       t/db_dependent/Koha/Biblio.t
=> SUCCESS: All tests pass
3. Verify sorting works:
   GET /api/v1/patrons
   x-koha-embed: checkouts+count
   _order_by: -me.checkouts_count
=> SUCCESS: Patrons sorted by checkout count
4. Sign off :-D

Assisted-by: Sonnet 4.6 (Anthropic)
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:49 +00:00
Tomás Cohen Arazi e51334604f
Bug 41950: Make +count embeds sortable via SQL COUNT subqueries
Currently, +count embeds (e.g. claims+count) are computed in Perl via
$object->$relation->count during to_api serialization. This means they
cannot be used in _order_by and each count triggers a separate SQL query
per row (N+1).

This patch makes dbic_merge_prefetch detect +count embeds and, when a
matching DBIC relationship exists, inject a correlated COUNT subquery
via +select/+as instead of adding a prefetch.

The order_by entries referencing count aliases are fixed up to strip the
me. prefix, since +as aliases are virtual columns not bound to a table.

In to_api, the is_count handler now checks for the pre-computed column
via get_column before falling back to the Perl-level ->count call. This
preserves backward compatibility for Koha-level methods that don't have
a corresponding DBIC relationship.

Test plan:
1. Apply patches
2. Run:
   $ ktd --shell
   k$ prove t/Koha/REST/Plugin/Query.t \
       t/db_dependent/Koha/Object.t \
       t/db_dependent/Koha/REST/Plugin/Objects.t
   => SUCCESS: Tests pass!
3. Sign off :-D

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:48 +00:00
Tomás Cohen Arazi 9bab898922
Bug 41950: Unit tests for sortable +count embeds
Add tests to verify that +count embeds produce +select/+as attributes
instead of a prefetch, enabling SQL-level sorting by count columns.

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 16:19:45 +00:00
Marcel de Rooy 1c9e5ed219
Bug 42321: Improve handling of empty Z3950/SRU search results
Skips the empty results; only reports the 'real failures'.
Includes adding a grand total of hits to the form.
Some preliminary work to remove the hardcoded 20 results per target
in the code.
This is not yet a complete solution, but an improvement. The code in
C4/Breeding needs a larger overhaul. (And there might be an issue on
the Folio server side at LOC.)

Test plan:
Without this patch:
Use the LOC SRU server (since if frequently generates errcode 61).
Try the same search several times in a row (with Go). You probably will
come across the "LOC SRU record x: 0" messages. (Random behavior.)
Apply this patch.
Repeat the above. Notice that you will sometimes see the error 61 (and the
tip to refresh) but not the code 0 messages.
Run t/db../Breeding.t

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:39:10 +00:00
Olivia Reynolds ea67056197
Bug 38621: Use GetPreparedLetter in EmailReport
As with guided reports through the admin interface, this patch switches
EmailReport to use the same function for processing templates. This
gives patron emails sent via cronjobs access to the same variables as
other report locations.

Test plan:
1. Under Tools > Notices and Slips, create a new notice in the Reports
   module. Choose a code and a name, and under Email assign a subject
   and the following body:

Date: [% today %]

2. Under Reports, create a new report from SQL with a name and the
   following query:

SELECT 1 AS borrowernumber, 'to@example.com' AS email

3. Run the following command from the base of Koha, replacing CODE with
   the notice code created earlier, and ID with the report ID created
   earlier:

misc/cronjobs/patron_emailer.pl -module report -from from@example.com -notice CODE -report ID

4. Verify that the report runs successfully, and the console output
   includes "Date:" on a line with nothing following it.
5. Apply patch.
6. Run the following command from the base of Koha, replacing CODE with
   the notice code created earlier, and ID with the report ID created
   earlier:

misc/cronjobs/patron_emailer.pl -module report -from from@example.com -notice CODE -report ID

7. Verify that the report runs successfully, and the console output
   includes "Date:" on a line with the current time following it.
8. Run `prove t/db_dependent/Reports/Guided.t`, and verify it passes.

Sponsored-by: Open Fifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:39:09 +00:00
Olivia Reynolds cbbfa8c8d2
Bug 38621: Use GetPreparedLetter for report templates
Previously, report templates were rendered using process_tt directly.
This made the available variables inconsistent with other notices and
slips. This patch switches to rendering report templates through the
same function as other notices. To match the arguments to this system,
this patch also makes the report system reference templates by their
code rather than ID.

Test plan:
1.  Under Tools > Notices and Slips, create a new notice in the Reports
    module. Choose a code and a name, and under Email assign a subject
    and the following body:

[% today %]
[% FOREACH d IN data %]
[% d.title %]
[% END %]

2.  Under Reports, create a new report from SQL with a name and the
    following query:

SELECT * FROM biblio LIMIT 1

3.  Create another report from SQL with a different name and the
    following query, specifying a runtime parameter:

SELECT * FROM biblio WHERE 'a'=<<Param>> LIMIT 1

4.  On the first report, select "Run with template" and choose the
    template created earlier.
5.  Verify that the report runs successfully, and the output does not
    include the current date.
6.  On the second report, select "Run with template" and choose the
    template created earlier. Choose "a" as the runtime parameter value
    when asked.
7.  Verify that the report runs successfully, and the output does not
    include the current date.
8.  Apply patch.
9.  On the first report, select "Run with template" and choose the
    template created earlier.
10. Verify that the report runs successfully, and the output now
    includes the current date along with the fetched data.
11. Under the Download dropdown, select "Rendered template (...)".
12. Verify the downloaded file matches the report output.
13. On the second report, select "Run with template" and choose the
    template created earlier. Choose "a" as the runtime parameter value
    when asked.
14. Verify that the report runs successfully, and the output now
    includes the current date along with the fetched data.
15. Under the Download dropdown, select "Rendered template (...)".
16. Verify the downloaded file matches the report output.

Sponsored-by: Open Fifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:39:08 +00:00
Jonathan Druart d77897c203
Bug 42453: Remove warning
[WARN] Use of uninitialized value $index_count in numeric ne (!=) at /kohadevbox/koha/about.pl line 899.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:39:07 +00:00
Jonathan Druart a1f8f35b70
Bug 42453: Prevent 500 on the about page if elastic is not runing
If the elastic countainer is stopped, the about page crashes with a 500
"No nodes are available"

Test plan:
Startup ktd with `--search-engine es8`
Go to the about page
Notice that the "Elasticsearch" line contains the version and "running"
Stop the es container: `docker start koha-es-1`
Refresh the about page
=> Without this patch you get the 500
=> With this patch applied the page is rendered with:
"Version: Unkown" and "Status: not running"

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:39:06 +00:00
Martin Renvoize 88fd58dd6c
Bug 42645: Add tests for FallbackToSMSIfNoEmail with alternate email fields
Verify that the SMS fallback in ModSuggestion() respects EmailFieldPrimary
and EmailFieldPrecedence: a patron with smsalertnumber and an emailpro
address (but no borrowers.email) must receive the notice by email, not SMS.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:39:05 +00:00
Martin Renvoize 57e110ee2c
Bug 42645: Use notice_email_address in ModSuggestion SMS fallback check
The FallbackToSMSIfNoEmail transport selection in ModSuggestion() was
checking $patron->email (the raw borrowers.email column) rather than
$patron->notice_email_address, which respects the EmailFieldPrimary and
EmailFieldPrecedence system preferences. Patrons with only emailpro or
B_email set were incorrectly receiving suggestion status-change notices
via SMS even when a valid email address was available.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:39:04 +00:00
Lucas Gass c0477bc9f1
Bug 42909: Use createdRow to add back suspend class
To test:
0/ APPLY PATCH
1/ View a record with some holds on it
2/ Look at the holds table ( reserve/request.pl?biblionumber=32 )
3/ Suspend at least one hold and notice the 'suspend' class on the <tr>
4/ Unsuspend the hold and notice the class is removed

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:39:03 +00:00
Paul Derscheid fc5ce2dd7e
Bug 41946: Allow superlibrarians to set protected flag on patron creation
Since bug 36085, the protected flag in a patron account was removed from
the patron creation form, regardless of if the user is a superlibrarian.

- Hoist $logged_in_user lookup before the edit_form block
- Move superlibrarian capability checks after the block so they apply
  to all operations including patron creation

To test:
1. Sign in as a superlibrarian
2. Go to Patrons > New patron > choose any category
3. Without this patch: the "Protected" yes/no field is missing
   from the "Library management" section
4. Apply this patch
5. Repeat step 2
   - The "Protected" yes/no field is now visible
6. Create the patron with Protected set to "Yes"
7. Confirm the patron is saved as protected
8. Sign in as a non-superlibrarian with edit_borrowers permission
9. Repeat step 2
   - The "Protected" field should still be hidden
10. Edit an existing patron as superlibrarian
    - The "Protected" field is visible and functional
11. Edit an existing patron as non-superlibrarian
    - The "Protected" field is hidden
12. Sign off

Signed-off-by: Caroline Cyr La Rose <caroline.cyr-la-rose@inlibro.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:39:02 +00:00
Martin Renvoize 38cbe6b195
Bug 41709: Fix EDIfact special character escaping in GIR segments and other fields
Apply encode_text function to all text fields in EDIfact Order messages
to properly escape special characters (?, ', :, +) that could break
message parsing. Previously only bibliographic description fields were
escaped, but GIR segments containing collection codes, branch codes,
item types, call numbers, and other data were vulnerable.

Test plan:
- Verify all existing tests pass
- Added comprehensive tests for special character escaping in GIR segments
- Confirmed that fields containing ?, ', :, + are properly escaped

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:39:00 +00:00
Kyle M Hall b302481a4d
Bug 42862: Use current-relative dates in bookingsModalDatePicker_spec.ts
The booking date picker tests fake "today" with cy.clock and create bookings on
hardcoded June 2026 dates. The modal fetches existing bookings from from
the api and that query uses the real current time, not the fake one in the browser.
Once the real date passes the hardcoded dates, the server stops returning those bookings (
they are now in the past ), so the modal sees no conflicts and assertions such as "the date
should have the leadDisable class" fail.

This patch anchors each test's "today" to the start of next month based on the current
date so the bookings always stay in the future and are returned no
matter when the test runs.

The 'any item' test also sets cy.clock before its second page reload
because the freeze does not survive the reload.

Test Plan:
1) On main run:
   yarn cypress run --spec t/cypress/integration/Circulation/bookingsModalDatePicker_spec.ts
2) Note the test fails
3) Apply this patch
4) Run the command again
5) Note all tests pass!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:38:59 +00:00
Jonathan Druart 761db4772a
Bug 42654: Split on several lines for readability
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:38:58 +00:00
Martin Renvoize ead19931db
Bug 42654: Restore section in OPAC course reserve detail title, breadcrumb and heading
When a course reserve has a section defined, the OPAC course detail page
should display it alongside the course name in the page title, breadcrumb,
and main heading. A previous change inadvertently removed this, leaving only
the course name in all three places.

This patch restores the conditional logic so that:
- If a section is set, the format "Course reserves for 'Course name' - Section"
  is used in the title, breadcrumb and <h1>.
- If no section is set, the existing "Course reserves for 'Course name'"
  format is preserved.

Test plan:
1. Enable the CourseReserves system preference.
2. In the staff interface, go to Course Reserves and create (or edit) a course
   that has a 'Section' value filled in (e.g. "Section A").
3. Add at least one item to that course reserve.
4. In the OPAC, browse to Course Reserves and click through to the detail
   page for that course.
5. Confirm that the browser tab title, the breadcrumb trail, and the page
   heading all show "Course reserves for '<course name>' - <section>".
6. Edit the course and clear the Section field.
7. Revisit the detail page and confirm that all three locations now show
   only "Course reserves for '<course name>'" with no trailing dash or
   section fragment.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Jonathan Druart <jonathan.druart@gmail.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:38:57 +00:00
David Nind eb8b13f429
Bug 42721: Update OPACAuthorIdentifiersAndInformation description and identifier labels
The patch updates the OPACAuthorIdentifiersAndInformation system preference
description so that identifiers are listed in alphabetical order.

It also updates the identifier labels in the system preference
description and what is shown in the OPAC:
- ScopusID --> Scopus ID
- WIKIDATA ID --> Wikidata ID

Test plan:
1. For the OPACAuthorIdentifiersAndInformation system preference, select
   "Identifiers (024$2$a)".
2. Note that in the system preference description the identifiers shown
   in the note are not in alphabetical order.
3. Add identifiers to an authority record in the 024$2 (the code for the
   identifier) and 024$a (the value for the identifier) subfields:
   - Example authority record for authority #483, Wall, Larry:
     . ISNI: isni, 0000000110763894
     . Scopus ID: scopus, 57221714177 (note: not his real Scopus ID)
     . VIAF ID: viaf, 91402770
     . Wikidata ID: wikidata, Q92597
4. View the OPAC authority record details page for an authority record
   with 024$2 and $024a information:
   - Example authority record for authority #483, Wall, Larry
     /cgi-bin/koha/opac-authoritiesdetail.pl?authid=483
5. In another tab view the author information tab for an OPAC
   bibliographic record details page where the author is linked to an
   authority record with 024$2 and $024a information:
   - Example bibliographic record details page for Programming Perl:
     /cgi-bin/koha/opac-detail.pl?biblionumber=262
6. Note for both steps 4 and 5 that:
   - Scopus ID is shown as ScopusID
   - Wikidata ID is shown as WIKIDATA ID
7. Apply the patch.
8. Refresh the OPAC authority record and bibliographic record pages.
9. Note that the identifier labels are now shown as Scopus ID and
   Wikdiata ID.
10. View the OPACAuthorIdentifiersAndInformation system preference
    description and note that the identifiers in the note are now
    listed in alphabetical order.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Olivia Reynolds <olivia.reynolds@openfifth.co.uk>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:38:56 +00:00
David Nind fff08df007
Bug 40603: (follow-up) Update system preference description - add uri
Adds uri (URI) to the note for the OPACAuthorIdentifiersAndInformation
system preference note.

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:38:55 +00:00
Owen Leonard 91f3fde0de
Bug 40603: Add support for "uri" to OPACAuthorIdentifiersAndInformation
This patch adds support for display of the "uri" identifier in the OPAC
when looking at personal name authority records and associated
bibliographic records.

To test you must have:
1. A personal name authority record
2. A bibliographic record which has 100$a linked to that authority
   record.

- Search for OPACAuthorIdentifiersAndInformation and make sure that
  "Identifiers (024$2$a)" is checked.
- Go to Authorities and locate your author's record.
- Add a 024 tag with "uri" as the source in $2, and a URI in $a.
- In the OPAC, locate the bibliographic record you modified.
- On the detail page, check the "Author information" tab
- Go to the authority detail page in OPAC for the author authority
  record you modified. Check the "Author information" section
  in the right-hand column.
  - In both cases you should see the URI you added, labeled with "URI:"

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:38:54 +00:00
Jan Kissig 8ab021665b
Bug 42912: Fix Uncaught TypeError on opac-readingrecord when no Checkout history
This prevents an 'Uncaught TypeError: can't access property "DataTable", table is undefined' when a patron has no items checked out yet and is visiting opac-readingrecord.
The code now checks if table is defined before accessing its properties.

Testplan:
a) log in in opac, open dev console
b) go to /cgi-bin/koha/opac-readingrecord.pl
c) without patch applied the error above occurs
d) apply patch and reload opac-readingrecord.pl, there should not be any error now
e) check out an item in staff to that patron and check that the table on opac-readingrecord.pl is diplayed

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:38:53 +00:00
Kyle M Hall f7197b43f0
Bug 42664: Unit tests
Test Plan:
1) prove t/db_dependent/SIP/SIPServer.t

Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:38:52 +00:00
Kyle M Hall d709c04f57
Bug 42664: Update SIP2 config timestamp after storing the change
Koha::SIP2::Object::store bumps the timestamp before SUPER::store,
opening a race condition where a worker reloading during db update
transaction caches a config that is missing the new row.

Bump the timestamp after the row has been written so it is not advanced
ahead of the data it represents.

Test Plan:
1) prove t/db_dependent/Koha/SIP2/SIP2ModuleMigration.t
2) prove t/db_dependent/api/v1/sip2_accounts.t

Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:38:51 +00:00
Kyle M Hall f899761666
Bug 42664: Reload SIP2 configuration in every worker process
SIP2 accounts/institutions are stored in the database and the SIP server is supposed to reload its configuration when they change.

In practice, after adding or editing a SIP2 account, most SIP worker processes keep serving the old configuration (e.g. new accounts get "SIP access not authorized") until the SIP server is restarted ( I tested multiple times over 30 minutes ).

There seem to be two issues:
* L1 cache flushed too late. process_request() calls _config_up_to_date() before Koha::Caches->flush_L1_caches(). The SIP parent reads sip2_resource_last_modified into its own L1 cache at startup and never flushes it. Every worker inherits that stale L1, so a worker's first connection compares against the startup-era value instead of the current shared value.

* sip2_config_read_timestamp is stored in the shared (memcached) cache, but the configuration it tracks is per-process. When one worker reloads the config from the database, it bumps the shared timestamp, so every other worker then evaluates as "up to date" and never reloads its own stale in-memory config.

Test Plan:
1) Configure the SIP server with several worker processes (e.g. set
   min_servers and max_servers to 5) and confirm an existing SIP2
   account works
2) Add a new SIP2 account in the staff interface
3) Without restarting the SIP server, test the new account with misc/sip_cli_emulator.pl
4) Note the login fails
5) Apply this patch
6) Restart all the things!
7) Add another new SIP2 account
8) Without restarting the SIP server test this new account with sip_cli_emulator.pl
9) Note that the new account works!
10) prove t/db_dependent/SIP/SIPServer.t

Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-29 15:38:47 +00:00
Jonathan Druart fa92634932
Bug 42281: Tidy all script tags - authorities (auto tidy)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:57:04 +00:00
Jonathan Druart f4353bc132
Bug 42281: Tidy all script tags - authorities
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the authority module.

The 3 main changes are in the following files:
* detail.tt
* merge.tt
* searchresultlist-auth.tt
diff is trivial

Note that authorities/authorities.tt is on a separate bug (bug 42284)

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:57:03 +00:00
Jonathan Druart cc73486ab6
Bug 41254: Add '(all)' for top-level entries
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:57:02 +00:00
Jonathan Druart ae0683fd72
Bug 41254: Replace '*' with arrow
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:57:01 +00:00
Jonathan Druart fd935fa7d7
Bug 41254: Allow filtering by all authorised values for multi-valued statuses
Some item statuses can correspond to multiple authorised values: Not
for loan, Withdrawn, Lost and Damaged. Until now, only a single entry
could be selected, and filtering applied to all values at once.

This change introduces finer-grained filtering by exposing each
authorised value individually in the filter.

Test plan:
Assign multiple authorised values to items on a bibliographic record
for the four affected attributes: Not for loan, Withdrawn, Lost and
Damaged. Use the "Status" filter and verify that each value behaves as
expected.

Display note:
The current dropdown presentation is not ideal.
Suggestions for improvement are welcome, although avoiding optgroup
would be preferable to keep the scope small.

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:57:00 +00:00
Jonathan Druart 391ef08e30
Bug 42080: Add Cypress tests
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:56:59 +00:00
David Cook 9a39e4799a
Bug 42080: (follow-up) limit data sent to libmagic to 100 characters
By limiting the data sent to libmagic to 100 characters, we make
the test faster and we avoid situations where a large or malicious
file causes too many resources to be used.

100 characters is more than enough for the file types we're allowing.

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:56:58 +00:00
David Cook 1c6328b82b
Bug 42080: (follow-up) allow some file types, checked by libmagic, to be viewed inline
This patch uses File::LibMagic to determine the real file type using
libmagic and magic numbers of invoice files, and allows a select group
of file types to be viewed inline for user convenience.

Note that a default Content-Security-Policy is still included to offer
an extra layer of protection against XSS.

Test plan:
0a. Apply the patch
0b. koha-plack --restart kohadev
1. Enable the syspref AcqEnableFiles (ie set to Do)
2. Find or create an active vendor
3. Create a basket if one doesn't already exist
4. Receive a shipment for the vendor
5. Create an invoice
6. Click "Finish receiving"
7. Click "Manage invoice files"
8. Upload a range of files including SVG, PDF, PNG, TXT
9. Click on the "Download" button
10. Note that the file downloads for all types
11. Click on the link in the "Name" column
12. Note that only the PDF and PNG display inline in the browser.
The other file types should be downloaded same as if you clicked the
"Download" button.

Signed-off-by: David Cook <dcook@prosentient.com.au>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:56:57 +00:00
Eric Phetteplace 0161a73d17
Bug 42080: Invoice PDFs are forced downloads
Serve invoice files with a CSP header that prevents XSS
instead of with an attachment header that turns them into
downloads.

Testing plan:
- Set AcqEnableFiles preference to Do
- Acquisitions > Vendor > Receive Shipment > Create invoice
- Finish Receiving > Manage invoice files
- Upload an SVG with a <script> tag in it
- Click the SVG filename to open it
- Inspect browser's console: it should say something about a
script being blocked due to the CSP directive

Signed-off-by: David Cook <dcook@prosentient.com.au>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:56:56 +00:00
David Cook a0136e5f2e
Bug 42087: Add dependency to debian/control
This change adds File::LibMagic (via libfile-libmagic-perl)
to debian/control. The debian/control should be automatically
re-generated by debian/control.in during the package process,
but we want to record the change here anyway.

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:56:55 +00:00
David Cook 76ecdf9e6d
Bug 42087: Add dependency to cpanfile
This change adds File::LibMagic to cpanfile

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:56:54 +00:00
Jonathan Druart 711b3fbd83
Bug 42286: Tidy all script tags - ill (auto tidy)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:56:53 +00:00
Jonathan Druart 71cd1efd7d
Bug 42286: Tidy all script tags - ill
In order to tidy all the script tags of the .tt files we isolate the
interpolation of Template::Toolkit variables in a separate script tag.

This patch focuses on the ILL module.

I don't really know how to test this patch...

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-26 11:56:52 +00:00
Pedro Amorim c355bb8574
Bug 42666: (RM follow-up): Tidy datatables.js
prove xt/js_tidy.t

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-24 08:41:35 +00:00
Jonathan Druart ce20c17e4e
Bug 42564: Fix Template/Plugin/Asset.t
lib/fontawesome/css/fontawesome.min.css was used as an example for this
test, let's use datatables.min.css

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-24 08:41:31 +00:00
Jan Kissig 293e00e95c
Bug 42357: Show patron details on holds table
This patch reintroduces patron details in 'Existing holds' table by using $patron_to_html.

test plan:
a) get a nice fresh ktd and navigate to /cgi-bin/koha/reserve/request.pl?biblionumber=262
b) place a hold for a patron f.e. 23529000035676
c) take a look at the Existing holds tables patron column which only shows the patrons card number.
d) apply patch and run 'yarn js:build'
e) refresh /cgi-bin/koha/reserve/request.pl?biblionumber=262
f) the patron column now should show patrons name
g) set system preference  HidePatronName  to 'Don't show'
h) go to /cgi-bin/koha/reserve/request.pl?biblionumber=262
f) the patron column now should only show the patrons card number.

Signed-off-by: Lawrence O'Regan-Lloyd <loreganlloyd@clamsnet.org>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 16:26:38 +00:00
Fridolin Somers 6e51cdae0d
Bug 42640: Script search_for_data_inconsistencies.pl use binmode UTF-8
Like Bug 32012 for runreport.pl the script search_for_data_inconsistencies.pl should use binmode UTF-8 to correctly display datas.

Test plan:
1) Edit an authorised value to add some diacritics in code, in "LOC" for example
2) Edit an item to use this code
3) Delete this authorised value
4) Run misc/maintenance/search_for_data_inconsistencies.pl
=> Check you have a correct message with diacritics

Signed-off-by: Chris Mathevet <chris.mathevet@inLibro.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 16:26:38 +00:00
Adolfo Rodríguez 676e01d346
Bug 42825: ERM Local Title - Start date (started_on) not saved for package resources
The started_on field inside relationshipFields was missing indexRequired: true,
which is required to generate a unique DOM id per row and correctly bind the
flatpickr input to the Vue data model.

The date_to componentProp also needs indexRequired: true so it references the
correct indexed id of the ended_on field.

Additionally, add a null check in calendar.inc before accessing ._flatpickr
on the end date element to prevent a TypeError when the linked element is
not found.

To reproduce:
1. Go to ERM > eHoldings > Local > Titles
2. Create or edit a local title
3. Link it to a package and enter a Start date, End date and Proxy
4. Save
5. Reopen the title and click on the linked package row
=> Start date is empty; End date and Proxy are saved correctly

Apply patch, then:
1. Go to ERM > eHoldings > Local > Titles
2. Create or edit a local title
3. Link it to a package and enter a Start date, End date and Proxy
4. Save
5. Reopen the title and click on the linked package row
=> Start date is now saved and displayed correctly
=> No TypeError in the browser console when selecting a start date

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 16:26:37 +00:00
Janusz Kaczmarek 3bfd382566
Bug 42159: Unit tests
Unit tests

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 16:26:36 +00:00
Janusz Kaczmarek 9071c83d86
Bug 42159: With OPACAuthorIdentifiersAndInformation information lacking from field 110/111
When the OPACAuthorIdentifiersAndInformation system preference is
enabled, selected information about authors is displayed. However, if
the main author is a corporate body or a meeting/conference (MARC 21
fields 110/111), the information is not shown.

Test plan:
==========
1. Enable the display of identifiers in
   OPACAuthorIdentifiersAndInformation.
2. Add a 024 field with a VIAF ID to any corporate authority record,
   e.g.:
   024 7# $a 1234 $2 viaf
3. Use this authority heading as the main entry (110 or 111) in any
   bibliographic record.
4. In the OPAC view of that bibliographic record, verify that the Author
   information tab does not display information from the main entry.
5. Use the same heading as an added entry (710 or 711). In the OPAC
   view, verify that the Author information tab now displays information
   from the added entry, but still not from the main entry.
6. Apply the patch ; restart_all.
7. Verify that OPAC now displays information from the 110/111
   heading fields in the bibliographic record.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 16:26:35 +00:00
Paul Derscheid 78630a1a82
Bug 42231: (QA follow-up) Show item title in note and suspend-hold modals
The static 'hidden' attribute on #addNoteTitle and #suspendHoldTitle was never
removed, so the item title was populated but stayed invisible.

- Toggle 'hidden' off when a modal is populated with a title
- Toggle 'hidden' on when a modal is closed/cleared

Test plan:
- Enable AllowCheckoutNotes
- OPAC > Summary > Checked out: click 'Add note', confirm the item title shows
- OPAC > Summary > Holds: click 'Suspend', confirm the item title shows
- Reopen each modal on a different item, confirm the title updates
- Run an accessibility checker, confirm no empty-heading errors return

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 16:26:34 +00:00
Laura_Escamilla bfa3956cd3
Bug 42231: Fix accessibility issues in OPAC summary table
The OPAC summary page contains several accessibility issues including an unlabeled renew checkbox, unlabeled textarea in the add note modal, and empty headings rendered before JavaScript population.

Test plan:
1. Have a user with checkouts
2. Log into the OPAC and go to "Summary"
3. Notice the renew checkbox does not have a corresponding label
4. Apply the patch
5. Repeat steps 1-2
6. Confirm the renew checkbox now has a corresponding label

Additional accessibility fixes:
7. Open the "Report a problem" modal
8. Confirm the note textarea has a corresponding label
9. Confirm addNoteTitle and suspendHoldTitle are not exposed as empty headings
10. Run an accessibility checker and confirm the related errors are resolved
11. Sign off and have a great day! :D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 16:26:31 +00:00
Eric Phetteplace a7646d86d3
Bug 42116: e.preventDefault is a function, needs parentheses
This change is unnoticeable in the UI, so the patch does not need before/after testing, but assurance that everything works. The affected code is used in 3 places for holds (cancelling a hold from reserve/request.pl, circ/circulation.pl, and members/moremember.pl) and 2 for cataloging (two different functions on cataloguing/additem.pl).

For holds:
1. Create three holds on different bibs for a patron
2. On the same page where you create the last hold (reserve/request.pl), delete it
3. Circulation > Check out (circ/circulation.pl) > Check out a different title to patron
4. Switch to patron's Holds tab, delete a hold
5. Go to Patrons account (members/moremember.pl) > Holds tab, delete a hold
6. All holds should delete successfully

For cataloging:
1. Go to any bib > New > New item
2. Select "Add Multiple Copies of this Item", add a note
3. Confirm multiple items are created with the note
4. Enter a new note, then select "Save as template"
5. Confirm the item template exists & create an item using it

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 11:49:25 +00:00
Jan Kissig 070693abd3
Bug 42582: Fix Uncaught TypeError on 'Place request with partner libraries'
This patch fixes an Uncaught TypeError on Place request with partner libraries (op=generic_confirm) when at least one partner library is available (primary email is set) and system preference ILLCheckAvailability is enabled.
Reason for this is an undefined object services.

Test plan:
a) start a ktd instance
 ktd --proxy --name main up -d
b) enable system preference ILLModule and ILLCheckAvailability
 http://main-intra.localhost/cgi-bin/koha/admin/preferences.pl?tab=interlibrary_loans
 ILLModule: 'enable'
 ILLCheckAvailability: 'check'
c) edit an ILL patron and set an Primary email address:
 http://main-intra.localhost/cgi-bin/koha/members/moremember.pl?borrowernumber=16 (Walker)
d) create an ill request (Interlibrary loans -> (Requests) -> New ILL request) and fill out mandatory data
e) open dev tools (F12 on Firefox) and go to 'Place requests with partners'
f) notice console error: Uncaught TypeError: can't access property "enabled", services[0] is undefined
g) apply patch and reload page 'Place requests with partners' -> no error

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 11:49:24 +00:00
Jan Kissig 35c7cd1ae7
Bug 42141: Fix Save and show/list/edit button on Vue SIP2 config
This patch fixes the behavior of the Save (Save and list, Save and continue editing, Save and show) button which is controlled via useBaseResource.navigationOnFormSave on
Accounts, Institutions and System preference overrides in Administration -> Self-service circulation (SIP2)

To test:
a) go to Administration -> Self-service circulation (SIP2)
b) create one of the given resources (institution, account, system preference override) and fill out mandatory fields
c) click either Save or Save and continue editing or Save and show
d) check that you always end up on the list.
e) apply patch and run 'yarn js:build' , reload your browser
f) repeat b) and c)
g) check that the desired view is now shown
  - show the edited/created resource
  - list all created resources
  - continue editing the resource

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 11:49:23 +00:00
Jonathan Druart 398c329c21
Bug 42611: Add missing raw filter
[FAIL] koha-tmpl/intranet-tmpl/prog/en/modules/members/readingrec.tt
   FAIL   filters
                asset_must_be_raw at line 172 (    [% Asset.js("js/members/readingrec.js") %])

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 11:49:22 +00:00
David Cook 5c06532231
Bug 42611: Fix Javascript error when no Circulation history
This patch checks that "table" is defined before trying to access
the DataTable object for it.

This patch also moves the Javascript from the .tt file into a separate .js
file since it doesn't need any Template::Toolkit itself.

Test plan:
0. Apply the patch (and reset_all)
1. Log into the staff interface
2. Go to http://localhost:8081/cgi-bin/koha/members/readingrec.pl?borrowernumber=51
3. Open the console and see there is no longer a Javascript error
4. Check out an item like 39999000001310 to this user
5. Review the "Circulation history" and note that the DataTable appears
and there are no Javascript errors

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 11:49:21 +00:00
Jonathan Druart c14daf3f3c
Bug 42666: OPAC counterpart
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 11:49:20 +00:00
Jonathan Druart e82a3c7f96
Bug 42666: Add a Cypress test
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 11:49:19 +00:00
Chris Mathevet 487c9701a4
Bug 42666: Parse kohaTable pageLength setting to int
Test plan :
01 - Clear your browser's local storage
02 - Go to Administration > Item types
03- You need to have more than 10 item types, add some if needed
04 - Observe "Showing 1 to 010 of XX" on top of the table
05 - Click on "next" button at the bottom of the table
06 - Nothing happens
07 - Go to Administration > Patron categories
08 - You need to have more than 20 patron categories, add some if needed
09 - Observe "Showing 1 to 020 of XX" on top of the table
10 - Click on "next" button at the bottom of the table
11 - Nothing happens
12 - Apply patch
13 - Clear your browser's local storage
14 - Repeat 2-5 and 7-10, observe that the leading 0 is gone, and that the
"next" button works

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-23 11:49:15 +00:00
Laura_Escamilla 51bdab13ee
Bug 42078: Allow vendor ID searches on serial subscription vendor search page
The serials vendor search currently only searches vendors by name.
This patch allows the search field to also match vendor IDs so
staff can locate vendors using either name or ID.

Test plan:
1. Go to Serials → New subscription.
2. Click to search for a vendor.
3. Search using part of a vendor name and confirm results appear.
4. Search using an exact vendor ID and confirm the correct vendor is returned.
5. Sign off and have a wonderful day! :D

Signed-off-by: Angela Berrett <angela.berrett@familysearch.org>
Signed-off-by: Angela <angela.berrett@familysearch.org>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:13 +00:00
Pedro Amorim c420f23125
Bug 42002: Make 'replace' in screen_msg_regex not required
Default to empty string if null to ensure that leaving it blank produces the required outcome: the replace value is an empty string

Signed-off-by: Marion Durand <marion.durand@biblibre.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:12 +00:00
Baptiste Wojtkowski d294b2ce2a
Bug 39727: Remove DelSuggestionsOlderThan from C4/Suggestions.pm
To test:
Test the cronjob purge_suggestions.pl

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:11 +00:00
Baptiste Wojtkowski b2505f31c3
Bug 39726: Remove op connectDuplicate from acqui/newordersuggestions.pl
ConnectDuplicate does not appear to be used anywhere.
No test plan to provide here.
git grep connectDuplicate should return nothing

Since there is no cud-, it would at least have been broken for a year

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:10 +00:00
Pedro Amorim de837ace63
Bug 41685: (RM follow-up): Remove dead comment code
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:09 +00:00
Paul Derscheid 5cba025ba1
Bug 41685: (QA follow-up) Classify MARC21 $w by position 0
MARC21 4xx/5xx $w is a control subfield with up to four positions;
only position 0 encodes the hierarchical relationship. The previous
regex required the whole subfield to equal g or h, so values such as
"g   " or "gnna" would fall into the middle bucket and lose the
broader/narrower distinction.

This case is hypothetical and likely rare in practice (most catalogs
store a single character in $w), but the correctness improvement still
makes sense and aligns with how BuildSummary reads the same subfield
elsewhere via substr($w, 0, 1).

To test:
- prove -v t/db_dependent/AuthoritiesMarc.t
- Verify BuildSummary/_marc21_sort_hierarchy_alpha subtest passes,
  including the new multi-position $w case.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:08 +00:00
Paul Derscheid 7ce0fef1bd
Bug 41685: (QA follow-up) Add POD for _marc21_sort_hierarchy_alpha
- koha-qa pod_coverage check flagged the new private helper
- sub as uncovered. Document it briefly to satisfy the check.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:07 +00:00
Marcel de Rooy 21f957d868
Bug 41685: (QA follow-up) Prevent uninit warn on missing subfield a
Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:06 +00:00
Marcel de Rooy 8f6c66afbb
Bug 41685: (QA follow-up) Resolve qa warnings
Resolve critic warn:
"return" statement followed by "sort" at line 1238, column 5.  Behavior is undefined if called in scalar context.  (Severity: 5)

Tidied with lastest perltidy version.

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:05 +00:00
Marcel de Rooy aab711b1b3
Bug 41685: Unit tests
Test plan:
Run t/db_dependent/AuthoritiesMarc.t

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:04 +00:00
Marcel de Rooy 27fb770e77
Bug 41685: Sort seefrom/seealso in BuildSummary
Test plan (MARC21):
[1] Create an authority record, say a term. Include two 450s, the first
    one starting with Z and second with A. Include five 550s, where two
    are narrower ($w==h), two broader ($w==g) and one without $w. Make
    sure that the terms will sort differently than the field order (go
    back in alphabet when filling subsequent $a's or so).
[2] Perform an authority search that includes the new authority.
[3] Verify that the terms in the Summary column for this authority
    are sorted first on hierarchy, second alphabetically. The one
    without hierarchy should be in the middle.

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:03 +00:00
Lisette Scheer 7d60cc280a
Bug 42734: (QA follow-up) Remove integer class
The integer class appears to only lmit the with of the text box to 5em, making it difficult to see the full preference.
I removed the class for better readability.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:02 +00:00
Martin Renvoize 914ff32fa2
Bug 42734: Add autoMemberNumValue to patrons sysprefs UI with caution note
This patch adds autoMemberNumValue to the Patrons system preferences
page immediately below autoMemberNum, so superlibrarians can view and
correct a runaway counter without needing vendor support.

The entry carries a caution note explaining:
- the value is managed automatically and should not normally be changed
- Koha generates the next cardnumber from max(counter, live_db_max),
  so lowering the counter only has effect once the patron responsible
  for the inflated number has been deleted or had their cardnumber
  corrected first
- the risk of setting it too low: potential re-issuance of a previously
  used cardnumber, which may grant a new patron access to third-party
  services linked to the original holder

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:01 +00:00
Jonathan Druart dd89d8ab60
Bug 42803: Only perform the lookups when !$filter is true
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:39:00 +00:00
David Cook d2a76b80b0
Bug 42803: Fix case where filter is not supplied
This change fixes the case where "Any library" and "Any item type"
weren't validating.

Test plan:
0) Apply the patch
1) Set strict_sql_modes to 0 in koha-conf.xml and restart plack
2) Go to Reports > Items with no checkouts:
<staff_url>/cgi-bin/koha/reports/catalogue_out.pl
3) Leave Library set to "Any library"
4) Pick a specific item type that you know has items (e.g. "BK")
5) Note that you get results

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 15:38:57 +00:00
Owen Leonard 25496bdc4f
Bug 42128: Remove event attribute from admin-icon-selection.inc
This patch removes the "onmousedown" event attribute from
admin-icon-selection.inc, an include used in itemtypes.tt and
authorised_values.tt. I took this opportunity to create separate JS
files for both of those templates and added the event handler to the JS.

To test, apply the patch and go to Administration.

- Go to Item types and add or edit an item type.
  - In the icon selection section, select one of the built-in icons.
  - Switch to the "Remove image" tab.
  - Click the text input. The corresponding radio button should be
    selected.
- Go to Authorized values and perform the same test.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:22 +00:00
Fridolin Somers b83e2a6fe4
Bug 42846: Koha::Database::DataInconsistency fix typo for biblioitemnumber in MARC
Check of biblioitemnumber in MARC uses $biblionumber instead of $biblioitemnumber

It returns for example :
Biblionumber 542620 has biblioitemnumber '542620' but should be '542620' in 090$a

Test plan (for MARC21 only) :
1) Choose a biblio record id=X
2) Change with SQL :
update biblio_metadata set metadata = UpdateXML( metadata , '//datafield[@tag="999"]/subfield[@code="d"]' , CONCAT('<subfield code="d">1111</subfield>') ) where biblionumber = X
3) Run audit on this record
4) Check message is correct

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:21 +00:00
Pedro Amorim d59c3b1433
Bug 42397: DBRev 26.06.00.003
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:20 +00:00
Pedro Amorim 25bfb77b0b
Bug 42397: Atomicupdate
1) Enable ILLModule and IllLog system preferences.
2) Install a metadata enrichment plugin that supports ILL batches (restart plack if needed), e.g.:
  https://github.com/openfifth/koha-plugin-api-pubmed/releases
3) Visit ILL module:
<staff_url>/cgi-bin/koha/ill/ill-requests.pl
4) Click the 'New ILL requests batch' button
5) Enter any name, cardnumber '42' and any library. Click 'Continue'
6) Add a pubmedid (e.g. '123') and click process identifiers.
7) Click 'Add items to batch' and 'Finish and view batch'.
8) Visit the ILL request we just created:
<staff_url>/cgi-bin/koha/ill/ill-requests.pl?op=illview&illrequest_id=1
9) Confirm it explodes with a nasty 500 error
10) Apply patches, updatedatabase and restart plack.
11) Repeat 8). Confirm it no longer explodes
12) Click' ILL request log'. Confirm it shows the correct log(s) relative to this ILL request
13) Visit the log viewer and query for 'Interlibrary loans':
http://localhost:8081/cgi-bin/koha/tools/viewlog.pl?src=&user=&modules=ILL&actions=&object=&info=&interfaces=&from=&to=&output=screen&basename=Export&report_name=&do_it=1
14) Confirm only ILL requests are shown, do the same for ILL batches:
http://localhost:8081/cgi-bin/koha/tools/viewlog.pl?do_it=1&src=&user=&modules=ILL_BATCHES&actions=&object=&info=&interfaces=&from=&to=&output=screen&basename=Export&report_name=&do_it=1
15) Confirm only ILL batches are shown
16) Run all ILL batch relevant tests:
prove t/db_dependent/Koha/ILL/Batch*

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:19 +00:00
Pedro Amorim 268b5cbd0e
Bug 42397: Add and update ILL batch related tests
Add new tests to assert the usage of the new ILL_BATCHES action_logs modulename.
Update existing tests to utilize real log_something instead of mocking it.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:18 +00:00
Pedro Amorim 0c353be1c1
Bug 42397: Add 'ILL_BATCHES' action_logs modulename
Update ILL/Batch.pm and ILL/Batch/Status.pm to utilize this new modulename instead

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:16 +00:00
Jonathan Druart 10e86285a9
Bug 42535: Remove .news_delete JS from the main page
The .news_delete elements have been removed on
  commit c7b576ad7e
  Bug 34478: Remove 'Delete' link for news on the homepage
- | <a class="news_delete" href="/cgi-bin/koha/tools/additional-contents.pl?op=delete_confirmed&amp;ids=[% koha_new.additional_content_id | html %]&amp;csrf_token=[% csrf_token | uri %]">Delete</a>

We can remove this code

Test plan:
Confirm that there is no "Delete" links to delete news on the main page

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:15 +00:00
Laura_Escamilla f10b232d9e
Bug 42531: Allow repeatable field values to wrap
Test plan
1. Go to Administration → MARC bibliographic frameworks.
2. Edit Books, Booklets, Workbooks (BKS) framework.
3. In tag 508, ensure subfield $a is visible in the editor.
4. Open a bibliographic record (e.g. /cgi-bin/koha/catalogue/detail.pl?biblionumber=90).
5. Edit the record and add a long block of text to 508$a (e.g. extended production credits with many names and separators).
6. Save the record and view it in the staff interface.
    * Before patch: The Production Credits field does not wrap and overflows off the page.
7. Apply the patch and run yarn build.
8. Refresh the record detail page.
    * After patch: The Production Credits field wraps correctly within the page.
9. Confirm that shorter repeatable fields still display inline with separators (|) as expected.
10. Sign off and have a great day!

Signed-off-by: Lawrence O'Regan-Lloyd <loreganlloyd@clamsnet.org>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:14 +00:00
Lucas Gass 82d189a756
Bug 42795: Do not try to update ES mappings if not using ES patch for main
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:13 +00:00
Jonathan Druart 2d72d4e17a
Bug 42550: Remove JS error from report list if none exist
If there are no saved SQL reports, the "rtable" variable is empty and
triggers an error when called later.
Uncaught TypeError: can't access property "DataTable", rtable is
undefined

Test plan:
/cgi-bin/koha/reports/guided_reports.pl?op=list
Without reports
=> Without this patch there is a JS error in the console
=> With the patch the error is gone

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:12 +00:00
Owen Leonard b503cfe22e
Bug 42564: (follow-up) Actually remove calls to old assets
Also incorporate the Font Awesome assets into the installer SCSS.

Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:11 +00:00
Owen Leonard 8ffe7d9edb
Bug 42564: Upgrade Font Awesome in the staff interface to 7.2
We are using Font Awesome 6.3 in the staff interface. The current
version is 7.2. Upgrading will eliminate the repeated warnings in the
browser console about "Glyph bbox was incorrect."

The patch updates the staff interface CSS so that it pulls Font
Awesome CSS from node_modules. Font assets have been updated in
lib/fontawesome.

The many changed .inc and .tt files are a result of a missed aspect of
our upgrade to Bootstrap 5. The "sr-only" class was removed in BS5 but
the Font Awesome CSS included its own "sr-only" class. This made up for
the fact that we missed the required upgrade to BS5's "visually-hidden."
As soon as Font Awesome's "sr-only" was gone all these mistakes were
made visible.

To test, apply the patch and run 'yarn install.'
- Rebuild the staff interface SCSS.
- Clear your browser cache.
- Test many pages in the staff interface to confirm that no icons are
  missing.
- Check changes to the templates to confirm that "sr-only" has been
  replaced with "visually-hidden."
  - Places where a broken "sr-only" class would be obvious: The h1
    heading "Koha home" on the staff interface main page, the <legend>
    tag on the new currency form in Administration -> Currencies and
    exchange rates.

Sponsored-by: Athens County Public Libraries
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:10 +00:00
Owen Leonard 297edd689b
Bug 42641: JavaScript errors on patron attribute types admin page
This patch fixes some JavaScript errors on the patron attributes page.
Vanilla JS doesn't automatically handle null variables like jQuery does.

To test, apply the patch and go to Administration -> Patron attribute
types.

- If necessary, add multiple attribute types.
- The table listing all attributes should be displayed as DataTable and
  there should be no errors in the console.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:09 +00:00
Kyle M Hall 404cb0ee0a
Bug 36988: Update unit tests
These highlight why we may have made this choice. With only 'edit_borrowers' you can make
changes, but you cannot view your changes as you aren't allowed to see other borrowers.

Test now pass, but not sure if this answers the question of why we did it this way, or if there is more
work here. Should you be able to get back the fields that you updated at the least, to confirm your changes? Or
should we send a success with a message that you can't see the data?

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:08 +00:00
Kyle M Hall 3e62b2c44b
Bug 36988: Change permission
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:07 +00:00
David Cook 55d236769d
Bug 42202: Unfilter branches for OPAC search, news, and most popular
This change unfilters the branches for OPAC search, news, and most popular.
This is most noticeable when using the IndependentBranches system preference.
This change aligns the UX for both logged in and not logged in users

__SETUP__
- Do not apply the patch yet
- Log into Koha
- Go to Administration > System preferences
- Search for OpacAddMastheadLibraryPulldown system preference
- Change it to "Add" and save the preference
- Search for IndependentBranches system preference
- Set it to "Yes" and save the preference
- Search for OpacTopissue system preference
- Set it to "Allow" and save the preference
- Search for OpacNewsLibrarySelect system preference
- Set it to "Show" and save the preference
- Go to Patrons
- Create or modify a patron with a username/password but no superlibrarian
permission

__TESTS__
- Go to the OPAC (and logout if you're already logged in)
- Note that the library pull down saying "All libraries" is present
in the masthead
- Log in as your unprivileged user
- Note that the library pull down saying "All libraries" is now hidden
- Go to Advanced search and note there's no "Location and availability" option
- Go to "Most popular" and note there's no "From:" library filter
- Go to the home page and note it does not show a library selector for news

- Apply the patch

- Refresh the page
- Note that the library pull down saying "All libraries" is present
in the masthead
- Note that the "From:" library filter now appears
- Go to Advanced search and note "Location and availability" now appears
- Go to the home page and note it now shows a library selector for news

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-19 12:00:03 +00:00
Olivia Reynolds bb83b600ec
Bug 12757: Set float type on OpenDocument cells
Test plan:
1. Navigate to Reports > Create from SQL.
2. Create a new report with a name and the following SQL query:

SELECT 'test', 1, '2'

3. Run the report.
4. Under the Download dropdown, select "Open Document Spreadsheet".
5. Verify a ".ods" file is downloaded.
6. Open the downloaded spreadsheet.
7. Verify the two number-like columns are handled as such. This depends
   on your choice of spreadsheet software:
   - In Excel, the numbers should not have a leading single quote in the
     edit-bar when selected.
   - In Google Sheets, the numbers should be right-aligned, and
     selecting both of them should display their sum in the bottom
     toolbar.
8. Run `prove t/Koha/Util/OpenDocument.t` and verify all tests were
   successful.

Sponsored-by: Open Fifth
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-18 16:02:11 +00:00
Lucas Gass 4f71f15f0c
Bug 41070: (QA follow-up) Tidy
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-18 16:02:10 +00:00
Baptiste 902ca28a3c
Bug 41070: Fix selector in addorderiso validator
Steps to reproduce, do not apply patch
1 - Go to any record and click save -> marc (Unicode/utf8)
2 - Go to any vendor and add a basket
3 - In your acquisition grid, make sure "barcode" is mandatory
4 - In this basket, create a new order from file, use the generated file and wait for the end of the import. Click on add staged files to basket.
5 - Select the record, make sure no Fund or barcode is filled, neither in first or third tab.
6 - Validate: There will be an error due to the missing barcode. If
there is another error, fill the missing part of the form.
7 - Apply patch, repeat 6, the error will be due to the missing fund
8 - Select a fund in the first panel -> the error will be due to the
missing barcode
9 - Select a fund in the third panel -> the error will be due to the
missing barcode

Note: the point about the barcode are not relevant for the bug, they are
just here to prevent the submission of the form. And avoid repeating
steps 4-5.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-18 16:02:09 +00:00
Martin Renvoize cb993ee5ef
Bug 40225: Send stockrotation report even when all items are stationary
The stockrotation cronjob was only sending email reports when there were
actionable items (items that needed to be moved between branches). This
meant that libraries would not receive any notification when all items
in their rotation were stationary, even when the --send-all flag was
passed to the script.

This patch modifies the report logic to also send reports when there are
only stationary items, ensuring libraries have visibility into the complete
status of their stock rotation items.

Test plan:
1. Set up a stock rotation rota with items that are all stationary
   (items that should stay at their current branch)
2. Run the stockrotation cronjob: perl misc/cronjobs/stockrotation.pl
3. Without this patch: No email report is generated
4. Apply this patch
5. Run the cronjob again
6. Verify that an email report is now generated showing the stationary items
7. Test with mixed scenarios (some actionable, some stationary items)
8. Confirm reports are sent in all cases where items exist in the rotation

Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-18 16:02:08 +00:00
Owen Leonard 369236bb31
Bug 41803: Improvements to consistency in basket groups template
This patch makes some updates to the basket groups template:
- Buttons in a table's "actions" column should have the class "btn
  btn-default btn-xs"
- Adding icons to action buttons
- Group export options (CSV and PDF) under button dropdown
- Use class instead of ID as hook for closing and deleting basket
  groups. This prevents duplicate element IDs on the page.
- When there is no EDIFACT configuration, disable the "Generate EDIFACT
  order" button and put the error message in a tooltip.
- Style warning message as Bootstrap alert

To test, apply the patch and go to Acquisitions and locate a vendor.

- View the vendor details.
- Click "Basket groups" in the sidebar.
- Create multiple basket groups if necessary.
- Test various operations in the list of open groups:
  - Close and export PDF
  - Edit
  - Delete
- Test the actions for a closed basket group:
  - Reopen
  - Export -> CSV and Export -> PDF
- Enable the EDIFACT system preference but do not add any EDI accounts.
  - Under the "Closed" basket group tab the "Generate EDIFACT order"
    buttons should be disabled, and hovering over each one should
    trigger a popup warning that there is no EDIFACT configuration for
    the vendor.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-18 16:02:05 +00:00
Jonathan Druart 38474d7611
Bug 42781: Do not modify window.name
https://developer.mozilla.org/en-US/docs/Web/API/Window/name

Test plan:
0. Do not apply this patch (you can apply the first one)
1. Go to /cgi-bin/koha/members/members-home.pl
2. Open the console
3. type "window.name", it's ""
4. Run a patron search, "d"
5. window.name is now "Duffy, Joann name"
6. Apply this patch, try again
7. window.name is still an empty string

Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:59:36 +00:00
Magnus Enger 98459bab8c
Bug 42781: Declare 'name' before using it in patron-format.js
The code in koha-tmpl/intranet-tmpl/prog/js/patron-format.js uses
a variable called 'name' without declaring it first. The declaration
was removed in bug 32176. No deterioration of functionality has been
discovered in connection with this, but the declaration should be
restored.

To test:
- Apply the patch.
- HidePatronName = Show
- Look at the staff detail view for a record that has at least one
  item on loan.
- Verify the name of the patron is displayed as expected.

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Jan Kissig <bibliothek@th-wildau.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:59:34 +00:00
Jan Kissig 4979f23cb1
Bug 42844: Fix broken subscription search when using additional fields as seach criteria
This fixes an error when searching for a subscription with an additional field when no result is found by changing the call of SearchSubscriptions to expect an array instead of an hash ref.
Serials.pm, when using filter_by_additional_fields may return an empty array, but the calling function only checked for an hash ref which lead to an error when no result was found.

Testplan:
a) create an additional field for subscriptions: More -> Serials -> Manage subscription fields
   f.e. use an AV CCODE as additional field
   make sure "searchable" is checked
   No subscription should use this additional field by now.
b) go to More -> Serials
c1) if bug 40201 is pushed you can search for that additional field on serials-home
   -> An error stack trace will appear.
c2) otherwise perform an empty search on serials home to end on serials-search. In the search form on the left select that additional field as search criteria.
   -> An error stack trace will appear.
d) add a subscription using that additional field
  -> More -> Serials -> New subscription.
  On step two there is a section for Additional fields. Select f.e. CCODE Fiction
e) redo c1 or c2 using that additonal field value as search criteria.
f) Check that the search result shows your subscription.
g) apply patch and redo c1 or c2 for an additional field value that is not used (f.e. Non-fiction)
h) The search should work now, returning 0 results instead of an error
i) check that searching for a used additional field (f.e. Fiction) still works

Signed-off-by: Michaela Sieber <michaela.sieber@kit.edu>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:59:31 +00:00
Pedro Amorim 2bb3865e7e
Bug 28915: DBIC Schema Build
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:17 +00:00
Pedro Amorim 566ea8876b
Bug 28915: DBRev 26.06.00.002
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:16 +00:00
Pedro Amorim f099cb0d62
Bug 28915: (RM follow-up): Remove dead code + typo fix
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:15 +00:00
Lari Taskula e0fb33c3a7
Bug 28915: Adjust input maxlength when subfield is used as charpos
To test MARC modification with the new feature:
1. Add a new record and make sure it has the following fields
1.1 Field 008 character position 35-37 contains "|||"
1.2 Field 041$a contains "eng"
2. Navigate to Cataloging > MARC modification templates > + New template (name "Change 008 lang")
3. Click + New action
4. Add "008" after "field(s)"
5. From dropdown "Delete", select "Update existing or add new"
6. To the input field on the right of "starting from character position", enter value 35
7. To the input field on the right of "with value", enter eng
8. From the empty dropdown below, select "if"
9. To the input field on the right of "field", enter 041, and to the right, enter a
10. From the dropdown on the right, select "matches"
11. Check "RegEx"
12. To the empty input field on the right, enter ^eng$
(the line should now say: [if] field [041] [a] [matches] m/[^eng$]/ RegEx [X])
13. Give any description you want (ex. Change 008 35-37 to eng)
14. Navigate to the record details you added in step 1
15. Click Edit > Modify record using template
16. Click "Show MARC"
17. Double check field 008 character position 35-37 is currently |||
18. Click "Close"
19. From dropdown "Select a template", select your new modification template from step 2
20. Click "Show MARC"
21. Observe field 008 character position 35-37 is now eng
22. Click "Modify selected records"
23. After job is completed, observe field 008 character positions 35-37 is now eng

Also test MARC modification templates for non-control fields in case of regression.

Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:14 +00:00
Lari Taskula 3a672758c8
Bug 28915: Fix field_exists call in C4::MarcModificationTemplates
Control fields (generally, just tags below 010) do not have subfields, use data() at /home/koha/Koha/Koha/SimpleMARC.pm line 460.

To test:
1. prove t/db_dependent/MarcModificationTemplates.t
2. Observe success

Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:13 +00:00
Lari Taskula fed20335a4
Bug 28915: Add test for AddModificationTemplateAction
To test:
1. prove t/db_dependent/MarcModificationTemplates.t

Observe test failing with the following error

t/db_dependent/MarcModificationTemplates.t .. 1/135 Control fields (generally, just tags below 010) do not have subfields, use data() at /home/koha/Koha/Koha/SimpleMARC.pm line 490.

Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:11 +00:00
Lucas Gass bc54c59423
Bug 28915: (follow-up) Update kohatructure.sql
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:10 +00:00
Andrew Fuerste Henry c19f99daa1
Bug 28915: (follow-up) Increase field size and maxlength in template
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:09 +00:00
Andrew Fuerste Henry 6942279435
Bug 28915: (follow-up) atomicupdate to increase from_subfield size
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:08 +00:00
Andrew Fuerste Henry 433fa34c11
Bug 28915: (follow-up) Add POD
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:07 +00:00
Lari Taskula 6db0d42427
Bug 28915: Add character position to MARC modification templates view
To test new form events:
1. Navigate to Cataloging > MARC modification templates > + New template (and provide any name)
2. Click + New action
3. Add "008" after "field(s)" and click tabulator or your mouse anywhere on the screen
4. Observe the second (sub)field input field disappearing
5. From dropdown "Delete", select "Update existing or add new"
6. Observe new text "starting from character position"
7. Select any other action from the dropdown from step 5
8. Observe the text "starting from character position" disappearing (unless action is to update or add new field)
9. Observe subfield input field missing (unless action is to update or add new field)

To test MARC modification with the new feature:
1. Add a new record
2. Modify record's 008 field character position 2-3 to "06" (indicating month June)
3. Navigate to Cataloging > MARC modification templates > + New template (name "Change June to September")
4. Click + New action
5. Add "008" after "field(s)"
6. From dropdown "Delete", select "Update existing or add new"
7. To the input field on the right of "starting from character position", enter value 2
8. To the input field on the right of "with value", enter 09
9. From the empty dropdown below, select "if"
10. To the input field on the right of "field", enter 008
11. From the dropdown on the right, select "matches"
12. Check "RegEx"
13. To the empty input field on the right, enter ^[0-9][0-9]06
14. Give any description you want (ex. Change 008 June to September)
15. Navigate to the record details you added in step 1
16. Click Edit > Modify record using template
17. Click "Show MARC"
18. Double check field 008 character position 2-3 is currently 06
19. Click "Close"
20. From dropdown "Select a template", select your new modification template from step 3
21. Click "Show MARC"
22. Observe field 008 character position 2-3 is now 09
23. Click "Modify selected records"
24. After job is completed, observe field 008 character positions 2-3 are now 09

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:06 +00:00
Lari Taskula 534c54f730
Bug 28915: Add control field support to Koha::SimpleMARC::update_field()
Before this patch, control fields were not modifiable using this subroutine.

Control fields use character positions instead of subfields. When modifying
control fields, this patch makes the subroutine's parameter "subfield" to indicate
a character position. An example

Koha::SimpleMARC::update_field({
  record => $record,
  field  => '008',
  subfield => '2',
  values => [09],
});

changes field 008 starting from character position 2 to contain values "09".
This replaces char pos 2 with "0", and char pos 3 with "9".

To test:
1. prove t/SimpleMARC.t

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Anneli Österman <anneli.osterman@koha-suomi.fi>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:05 +00:00
Saiful Amin 4c255bdde8
Bug 29587: Test mail option in SMTP servers
Edit: Added CSRF protection.

This patch adds a "Send test email" action to the SMTP servers
administration page. It sends a test message to the address in
the KohaAdminEmailAddress system preference and reports success
or failure to the user.

Test plan:
1. Add a valid SMTP server in Administration → SMTP servers.
2. Set KohaAdminEmailAddress to a valid email address.
3. Apply the patch.
4. Click "Send test email" in the Actions column.
5. Confirm that Koha reports "Test email sent successfully".
6. Edit the SMTP server and change the domain to an invalid one.
7. Click "Send test email" again.
8. Confirm that Koha reports failure.
9. Sign off.

Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:04 +00:00
Emmi Takkinen 94a6a93b64
Bug 41829: Set correct id to copied Tag editor button
When one copies MARC field, Tag editor button on fields
containing value builder plugin have same id as original
MARC field. This patch sets correct id to these buttons.

To reproduce:
1. Find MARC framework which has value builder plugin
   attached to one of its fields.
2. Navigate to Cataloging -> New record.
3. Navigate to MARC field with plugin attached to it.
4. Inspect fields Tag editor button with browsers developer
   tools.
   -> Check the id of Tag editor button.
5. Copy the field and check id of copied Tag editor button.
   -> Notice that it has the same id as in original field.
6. Apply this patch.
7. Referesh the page, navigate back to field and copy it.
8. Inspect Tag editors id.
   -> Id should now be same as in other copied fields.

Sponsored-by: Koha-Suomi Oy
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:03 +00:00
Matt Blenkinsop 456fa3809a
Bug 41605: Fix incorrect default key
Test plan:
No behavioural change but it is an inconsistency in the code
1) Look in base-resource.js at the newResource computed method
2) Confirm that the code in the first conditional block is looking for 'hasOwnProperty("defaultValue")'
3) Check the new patch and confirm that the code has been changed from 'value: false' to 'defaultValue: false' to match this

There is no behaviour change as boolean fields default to false within the framework but we should remove this inconsistency

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 15:24:02 +00:00
Nick Clemens 0767f26643
Bug 42245: Prevent preselecting sort values for guarantor search
av-build-dropbox.inc takes a 'default' param

When building the patron we are setting this to the value of sort1, then sort2
This seems to remain cached, and when we build the guarantor search the sort2 value is used as the default
This patch ensures we pass a blank default to the modal

To recreate:
1 - Define 2 values for 'BSort1' in Administration -> Authorised values
2 - Define 2 values for 'BSort2' in Administration -> Authorised values
3 - Find a borrower who is eligible to have a guarantor
4 - Edit patron and assign values to sort 1 and 2 for this borrower and save
5 - Edit patron again and click 'Add guarantor'
6 - Note that sort 2 is populated
7 - Apply patch
8 - Reload borrower
9 - Confirm sort1 and 2 are empty

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 12:54:35 +00:00
Laura_Escamilla b2780e6136
Bug 42233: Add accessible text to empty purchase suggestions table header
The first header cell in the OPAC purchase suggestions table contains
only an icon, leaving the <th> without accessible text. This causes
the column header to be announced unclearly by assistive technology
and can be flagged by accessibility testing as an empty table header.

Add visually hidden text to the header so the column has a meaningful
accessible name while preserving the current visual display.

Test plan:
1. Log in to the OPAC
2. Make a purchase suggestion
3. In the suggestions table, the first <th> contains no text
4. Apply the patch
5. Repeat steps 1-2
6. Inspect the suggestions table header
7. Confirm the first <th> now contains visually hidden text describing
   the checkbox selection column
8. Confirm the info icon still appears and its tooltip still works
9. Run an accessibility checker or inspect the DOM and confirm the
   empty table header issue is resolved

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 12:54:34 +00:00
Laura_Escamilla fee16fa179
Bug 42299: Add accessible label to star rating select in OPAC
To test:
1. Set  OpacStarRatings so it shows on the OPAC detail page
2. Notice star ratings has no associated label element
3. Apply the patch and refresh the OPAC detail page you were viewing.
4. Notice that the star ratings now have an accessible label.
5. Sign off and have a great day. :D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 12:54:33 +00:00
Laura_Escamilla d7f59e7adc
Bug 42232: Add missing legend to action fieldset in OPAC
To test:
1. Log on to the OPAC and go to "Messaging" page.
2. The fieldset that contains the Save/Cancel button is missing a legend.
3. Apply the patch and refresh the "Messaging" page.
4. Notice that the fieldset now contains an accessible legend.
5. Sign off and have a great day!

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 12:54:32 +00:00
Jonathan Druart 6a22a2a0fe
Bug 41992: Do not restore pagination offset
When "Save configuration state on page change" is enabled, the page
number is kept in localStorage and restored.

To prevent that we can set the attribute "start" of the state to 0.

Test plan:
Go on a page with several pages, go to another page than the first one
Go the another view and get back
Without this patch the last visited page was preserved when returning to the previous view
With this patch applied the view now defaults to the first page

Note that this does not affect shareable links.

Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 12:54:31 +00:00
Owen Leonard a7591ea3c6
Bug 42229: (follow-up) Restore card number <label> when necessary
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 12:54:29 +00:00
Owen Leonard e046b14a57
Bug 42229: (follow-up) Markup correction
The "Street type" fields had double "name" attributes.

Signed-off-by: Owen Leonard <oleonard@myacpl.org>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 12:54:28 +00:00
Laura_Escamilla 8073ef26b7
Bug 42229: Avoid using form labels for static values in opac-memberentry.tt
The OPAC personal details page uses <label> elements in some places
where no visible form control is present. In these cases, the content
is display-only and the label is not associated with an interactive
field.

Replace those labels with non-form markup for static values while
preserving existing labels for actual form controls.

Test plan:
1. Log in to the OPAC and go to the "Personal details" page
2. Inspect display-only values such as Username and Expiration date
3. Confirm <label> elements are used for content that is not part of a
   form control
4. Apply the patch
5. Log in to the OPAC and go to the "Personal details" page
6. Confirm display-only values such as Username and Expiration date no
   longer use <label> elements
7. Confirm other static display-only values updated by the patch also no
   longer use <label> elements
8. Confirm actual form controls, such as text inputs and selects, still
   have proper associated labels
9. Run an accessibility checker and confirm the related label misuse
   issues are resolved
10. Sign off and have a great day!

Signed-off-by: Owen Leonard <oleonard@myacpl.org>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 12:54:27 +00:00
Johanna Räisä 86b0fae756
Bug 42395: Fix missing translations on load_patron_holds_table
This patch adds missing translations for the load_patron_holds_table function.

Sponsored-by: Koha-Suomi Oy
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 12:54:26 +00:00
Pedro Amorim 1b93c7f6bd
Bug 42485: (RM follow-up): Tidy
prove xt/perltidy.t

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-15 12:54:23 +00:00
David Cook 4648f79f8d
Bug 42612: Change button colours in manage-marc-import.pl
This patches changes the button colours in manage-marc-import.pl
so that "Import this batch into the catalog" is more prominent,
and "Apply different matching rules" is less prominent.

Test plan:
0. Apply the patch
1. Go to a bib record like
http://localhost:8081/cgi-bin/koha/catalogue/detail.pl?biblionumber=29
2. Click "Save" and choose "MARCXML"
3. Go to http://localhost:8081/cgi-bin/koha/tools/stage-marc-import.pl
4. Click "Upload file"
5. Choose "MARCXML" for the "Format:"
6. Change "Check for embedded item record data" to "No"
7. Click "Stage for import"
8. Wait for the staging to complete. This will be show when the status
bar hits "100% Finished" and a "View batch" link appears.
9. Click "View batch"
10. Note that "Import this batch into the catalog" is a prominent yellow
button with black text
11. Note that "Apply different matching rules" is a less prominent dark grey
button with white text

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-12 16:24:19 +00:00
Artur Mickiewicz 237ecdc4bc
Bug 41987: Remove duplicate searchresult-breeding id
The searchresult-breeding id was present on both the wrapper div and
the "no results" alert element, causing duplicate id usage in the DOM.

This patch ensures the id is only used once on the parent container
and keeps the anchor link functional.

Signed-off-by: Artur Mickiewicz <davno_pora@mail.com>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-12 16:24:18 +00:00
Laura_Escamilla 1683524683
Bug 42234: Add text to checkout history cover image table header
1. Enable at least one cover image service, such as GoogleJackets or
   OPACAmazonCoverImages.
2. Log in to the OPAC as a patron with checkout history.
3. Go to your account > Checkout history.
4. Inspect the checkout history table header.
5. Confirm the cover image column header contains "Cover image".
6. Confirm the table still displays correctly.
7. Confirm sorting/export behavior is unchanged for that column.
8. Sign off and have a wonderful day! :D

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-12 16:24:17 +00:00
Martin Renvoize 081dbc1422
Bug 42485: (QA follow-up) Add explicit JSON::PP import and test coverage
Add explicit `use JSON::PP ()` to Elasticsearch.pm and the test file
to make the dependency on JSON::PP::false visible rather than relying
on YAML::XS loading it as a side effect.

Extend the get_elasticsearch_mappings() test subtest to assert that
date_detection and numeric_detection are disabled at the mapping root.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-12 16:24:16 +00:00
Tomás Cohen Arazi 9684fcd5c3
Bug 42485: Disable date and numeric detection on ES mappings
When ElasticsearchMARCFormat is set to ARRAY, the marc_data_array
field uses dynamic:true mapping. Elasticsearch auto-detects field
types based on the first document indexed. If a MARC subfield value
looks like a date (e.g. 2024-01-15 in 952$e), ES locks that
subfield to type date. Subsequent records with non-date values in
the same subfield fail to index with a document_parsing_exception.

This patch sets date_detection and numeric_detection to false at
the mapping root level, so ES treats all dynamically mapped fields
as text instead of guessing types.

Test plan:
1. $ ktd --search-engine es8 --name test-es --proxy up -d
   $ ktd --name test-es --wait-ready 180
   $ ktd --name test-es --shell
2. k$ koha-mysql kohadev -e "UPDATE systempreferences
     SET value='ARRAY'
     WHERE variable='ElasticsearchMARCFormat';"
   k$ echo 'flush_all' | nc -q1 memcached 11211
   k$ koha-mysql kohadev -e "DELETE FROM biblio_metadata;
     DELETE FROM items; DELETE FROM biblioitems;
     DELETE FROM biblio;"
   k$ rebuild_elasticsearch.pl -d -r --biblios
3. Add a record with date-like 952$e and index it:
   k$ koha-shell kohadev -p -c 'perl -MC4::Biblio
     -MMARC::Record -MMARC::Field -MKoha::Item -e "
     my \$r = MARC::Record->new();
     \$r->append_fields(
       MARC::Field->new(q{245},q{1},q{0},a=>q{Record one}),
       MARC::Field->new(q{942},q{ },q{ },c=>q{BK}));
     my (\$bn) = C4::Biblio::AddBiblio(\$r, q{});
     Koha::Item->new({biblionumber=>\$bn,
       biblioitemnumber=>\$bn, homebranch=>q{CPL},
       holdingbranch=>q{CPL}, itype=>q{BK},
       booksellerid=>q{2024-01-15}})->store;
     print qq{Added biblio \$bn\n};"'
   k$ rebuild_elasticsearch.pl --biblios -bn <biblionumber>
4. Check the mapping for 952$e:
   k$ curl -s es:9200/koha_kohadev_biblios/_mapping/field/\
     marc_data_array.fields.952.subfields.e
=> FAIL: type is "date" (should be "text")
5. Add a record with non-date 952$e and index it:
   k$ koha-shell kohadev -p -c 'perl -MC4::Biblio
     -MMARC::Record -MMARC::Field -MKoha::Item -e "
     my \$r = MARC::Record->new();
     \$r->append_fields(
       MARC::Field->new(q{245},q{1},q{0},a=>q{Record two}),
       MARC::Field->new(q{942},q{ },q{ },c=>q{BK}));
     my (\$bn) = C4::Biblio::AddBiblio(\$r, q{});
     Koha::Item->new({biblionumber=>\$bn,
       biblioitemnumber=>\$bn, homebranch=>q{CPL},
       holdingbranch=>q{CPL}, itype=>q{BK},
       booksellerid=>q{My Vendor Name}})->store;
     print qq{Added biblio \$bn\n};"'
   k$ rebuild_elasticsearch.pl --biblios -bn <biblionumber> -v -v -v
=> FAIL: document_parsing_exception - failed to parse date field
   [My Vendor Name]
6. Apply this patch, repeat from step 2
7. Repeat step 4:
=> SUCCESS: type is "text"
8. Repeat step 5:
=> SUCCESS: No indexing errors
9. Sign off :-D

Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-12 16:24:15 +00:00
Jonathan Druart e5aa1bf86d
Bug 38122: Add tests
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-12 16:24:14 +00:00
Jonathan Druart 9e4f896078
Bug 38122: Add space before the Group/Ungroup links
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-12 16:24:13 +00:00
Jonathan Druart 191b53278e
Bug 38122: Keep filters visibility
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-12 16:24:12 +00:00
Ayoub Glizi-Vicioso 0e0607ce80
Bug 38122: Tidy and Minor cleanup in catalogue_detail.inc (replace Template Toolkit leftovers)
1- perl `misc/devel/tidy.pl` executed.
2- Fixes xt/tt_kohaTable_tidy.t failure triggered by catalogue_detail.inc.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-12 16:24:11 +00:00
Jonathan Druart fe876c795d
Bug 38122: Add the ability to group holdings by status
This is a quite ugly solution, but that may answer end-user needs.

It add the option the group items by status on the bibliographic record
detail page.

However it will have VERY bad impact on performances for record with a
lot of items, as we are going to determine the status of each items of
the record.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-12 16:24:10 +00:00
Owen Leonard 89da78b3b9
Bug 42684: Capitalization: No Barcode
This patch corrects the capitalization of the phrase "No Barcode" and
several other instances of incorrect capitalization.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-12 16:24:07 +00:00
David Cook 18dad35de4
Bug 42361: Fix SQL injection in catalogue_out.pl
This change removes an SQL injection vulnerability from catalogue_out.pl

Test plan:
0. Apply the patch
1. Go to koha-conf.xml and change the following:
<strict_sql_modes>1</strict_sql_modes>
to:
<strict_sql_modes>0</strict_sql_modes>
2. sudo koha-plack --restart kohadev
3. Go to /cgi-bin/koha/reports/catalogue_out.pl
4. Try out the various options and note no errors occur

Signed-off-by: Sanjar Tulkinov Anvar o'g'li <sanjarbiy11@gmail.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-09 14:08:08 +00:00
Jonathan Druart 3c67759548
Bug 42802: Update kohastructure.sql
Test plan:
root@kohadevbox:koha$ perl /kohadevbox/misc4dev/run_tests.pl --run-db-compare-only --compare-with v24.05.00
should pass with this patch

Without this patch you get:
Running [diff /tmp/current_db.sql /tmp/upgraded_db.sql && diff -q /tmp/current_db.sql /tmp/upgraded_db.sql || { echo "ERROR - DB structures are not identical" && exit 1; }]...
7207c7207
< /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-09 14:08:07 +00:00
Pedro Amorim 28c94aeb15
Bug 42783: Fix ManageMarcImport_spec.ts
If import_batch_profiles is slow to respond, it'll trigger select.change() in stage-marc-import.tt and change the format to ISO2709 before the form is submitted, causing the error.

cypress run --spec t/cypress/integration/Tools/ManageMarcImport_spec.ts

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-08 14:16:25 +00:00
Pedro Amorim 8efbd99ef8
Bug 42764: (RM Follow-up): typo fix dbic
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-08 14:16:24 +00:00
Pedro Amorim bad922332b
Bug 42764: (QA Follow-up): Fix DB update mismatch
perl /kohadevbox/misc4dev/run_tests.pl --run-db-compare-only --compare-with v24.05.00

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-08 14:16:16 +00:00
Janusz Kaczmarek 34d708b930
Bug 42555: (Bug 25314 follow-up) Destroyed labels for customized facets
After applying Bug 25314, customized facets generated from
mappings.yaml are displayed with the technical type_id instead of the
proper label.

Test plan:
==========
1. In KTD with Elasticsearch enabled, add the following to
   admin/searchengine/elasticsearch/mappings.yaml

  su-gen:
    facet_order: 33
    label: Genre/Form
    mappings:
    - facet: 1
      marc_field: 655a
      marc_type: marc21
      sort: 1
      suggestible: ''
    opac: 1
    staff_client: 1
    type: ''

   Reindex with: koha-elasticsearch --rebuild -r -d -b kohadev
2. In the OPAC, at the bottom of the facets area, you should see a
   facet labeled su-gen_id instead of Genre/Form.
3. Apply the patch ; restart_all.
4. The facet should now display the correct label: Genre/Form.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:15 +00:00
Lucas Gass c183beefac
Bug 42757: Use default suggestor or fallback to librarian
To test:
1. Find a patron
2. From the patron account click the "Purchase suggestions" button
3. Make a suggestion
4. That suggestion is not associated with the patron from step 1
5. APPLY PATCH and restart_all
6. Repeart steps 1 -3. The purchase suggestion suggesedby should be the patron from step 1
7. Go  Acquisitions -> Suggestions -> Add suggestion
8. Make sure when you do it this way the suggestedby is set to the logged-in-user by default

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:14 +00:00
Lucas Gass 391d9f461c
Bug 42750: Add redirect params back and exit after redirect
To test:
1. Find a patron
2. From the patron account click the "Purchase suggestions" button
3. Make a suggestion
4. Your are redirected to suggestion/suggestion.pl
5. APPLY PATCH, restart_all
6. Try again, this time you should be properly redirected back to the patron account: ( members/purchase-suggestions.pl?borrowernumber=X )

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:13 +00:00
Emily Lamancusa db044d8552
Bug 42701: Fix editable class on additem.pl
When editing the items on a biblio record, you should be able to select
an item to edit by clicking anywhere on the item row. This functionality
was lost because the "editable" class was not getting added correctly to
the table row. This patch fixes that issue.

To test:
1. Find a biblio record that has at least one item
2. Click on Edit > Manage items
3. In the table that lists editable items, click on any cell of any item
   (other than the Actions button)
--> Nothing happens; the buttons to "Edit item" and "Delete item" do not
    appear as expected
4. Apply patch and refresh the page
5. Repeat step 3
--> The buttons to "Edit item" and "Delete item" appear

Signed-off-by: Barbara Johnson <barbara.johnson@bedfordtx.gov>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:12 +00:00
Lucas Gass 86b2eb9a3a
Bug 42740: Fix suggestion status when editing
To test:
1. Make a purchase suggestion.
2. Set the status to something, anything.
3. Edit that suggestion, notice that the "Status:" is set to 'No Status'
4. Now when you save, they actual status is lost.
5. APPLY PATCH, restart_all
6. Try steps 1 - 3. This time at step 3 the suggestion status should be properly set.
7. Save the suggestion, make sure it saves with the correct status.

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:11 +00:00
Baptiste Wojtkowski d1476c8184
Bug 41998: Fix redirect_with_params
Test plan (main only):
1 - Create a suggestion
2 - Delete the suggestion -> error
3 - Apply patch
4 - Repeat 1&2

Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:10 +00:00
Eric Phetteplace 9c994035fe
Bug 42723: Purchase suggestion 500 when EmailPurchaseSuggestions is set to BranchEmailAddress
Testing plan:
- Set EmailPurchaseSuggestions to BranchEmailAddress
- Go to create suggestion on the staff side /suggestion/suggestion.pl?op=add_form
- Create a suggestion with Library: Any
- 500 error: inbound_email_address is not a method of undef
- Apply patch & repeat
- Suggestion created

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:09 +00:00
Julian Maurice d6f1bdbe63
Bug 42741: Fix plural form in lists
Test plan:
1. Go to Lists
2. Create three lists called:
   - Test 0 items
   - Test 1 item
   - Test 2 items
3. Add one item in the "Test 1 item" list and add two items in the "Test
   2 items" list
4. Go to lists and view the list of lists
   --> In the Contents column, "Test 0 items" should be "0 items"
   --> "Test 1 item" should be "1 item"
   --> "Test 2 items" should be "2 items"

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:08 +00:00
Tomás Cohen Arazi 8798f1a7a7
Bug 42739: Add CSRF-TOKEN header to OPAC ratings.js fetch call
The ratings.js file uses raw fetch() to POST to the REST API without
including the CSRF-TOKEN header. This will fail once CSRF enforcement
is enabled on the REST API.

Test plan:
1. Apply patch
2. Enable star ratings in OPAC (StarRatings syspref)
3. Log in to the OPAC
4. Rate a biblio using the star widget
=> SUCCESS: Rating is saved without errors
5. Sign off :-D

Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:07 +00:00
Nick Clemens 0c5a53a8d0
Bug 42659: Use biblio-title.inc for displaying title at checkin
Currently at checking we display the number and parts all grouped,this is different than
other places in Koha, we should use the same include here

To test:
1 - Edit a record and set the title like:
    245 1 4 ‡aThe passion according to G.H. ‡bsubtitle ‡nnumber1 ‡ppart1 ‡nnumber2 ‡ppart2 /‡cClarice Lispector ; translation by Ronald W. Sousa.
2 - Enable syspref 'ShowAllCheckins'
3 - Checkin an item from that record, display is like:
    The passion according to G.H. subtitle number1 number2 part1 part2 /
4 - Apply patch
5 - Check in again
6 - Display is now:
    The passion according to G.H. subtitle number1 part1 number2 part2 /
7 - Sign off!

Signed-off-by: Eric Garcia <cubingguy714@gmail.com>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:06 +00:00
Lucas Gass f2735f73ef
Bug 42710: Set library to logged in library for new suggestions
To test:
1.  Acquisitions -> Suggestions ->Add suggestion
2.  Notice the under Acquisition information the library is set to Any
3.  APPLY PATCH
4.  Try again, library should be set to the logged in library.
5.  Make a suggestion and set it to some branch other then the logged in branch
6   Edit the suggestion from step 5, ensure that the "Library:" is properly set

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:05 +00:00
David Nind af711ae7c0
Bug 42454: Terminology: Use "and" instead of "&" for curbside pickups tab
The "Staged & ready (0)" tab for curbside pickups (Circulation > Holds
and bookings > Curbside pickup) should use "and" instead of "&".

See terminology guidelines for "& (ampersand)" - Only use "&" when part
of a proper noun or a common abbreviation,... Otherwise, use "and":
https://wiki.koha-community.org/wiki/Terminology#characters

Test plan:
1. Enable the CurbsidePickup system preference.
2. Configure curbside pickup for Centerville:
   - Go to Administration > Patrons and circulation > Curbside pickup
   - Set values:
     . Enable: selected
     . Pickup interval: 5
     . Maximum patrons per interval: 1
     . Patron-scheduled pickup: selected
     . Enable for waiting holds only: selected
     . Curbside pickup hours: add from and to hours for some days
     . Click Save configuration
3. Go to Circulation > Holds and bookings > Curbside pickup.
4. Note that there is an '&' in the "Staged & ready (0)" tab title.
5. Apply the patch.
6. Refresh the page.
7. Note that and is spelt in full "Stage and ready (0)".

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:04 +00:00
Owen Leonard c57cb6c6d5
Bug 42498: Fix invalid aria attribute in OPAC search results pagination
This patch corrects the aria-current attribute in OPAC search results
pagination. There should be no aria-current attribute on the list item,
and the aria-current value on the link should be "page."

To test, apply the patch and perform a catalog search in the OPAC which
will return multiple pages of results.

View the page source or use the developer console to inspect the HTML of
the active pagination link. It should have an aria-current label with
the value "page." The containing list item should not have an
aria-current label.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:03 +00:00
Owen Leonard b71b2bbc98
Bug 42691: Typo: automaticaly
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:02 +00:00
Owen Leonard 813c292957
Bug 42692: Typo: regarless
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:01 +00:00
Owen Leonard b021ad4a2e
Bug 42518: Capitalization: Refund Payout Receipt
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:26:00 +00:00
Owen Leonard 94e9d14151
Bug 42707: Malformed Bootstrap dropdown in reports output
This patch updates the SQL report results markup in order to make sure
Bootstrap dropdowns are formatted correctly.

To test, apply the patch and locate or create a report which returns
patron card number, e.g.

SELECT cardnumber, borrowernumber FROM borrowers LIMIT 10

In the results, test the menus triggered when you click a card number or
a borrowernumber. The menu in the cardnumber column should be
formatted like the menu for borrowernumber.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:59 +00:00
David Nind 5036ff8edd
Bug 42695: (follow-up) Format paragraph
Moves the explanation sentence into its own paragraph.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:58 +00:00
Owen Leonard 7dce6c266f
Bug 42695: Minor language and markup corrections to reports error messages
This patch makes a vew minor corrections to guided_reports_start.tt.

To test apply the patch and go to Reports.

- Try to create an SQL report which uses UPDATE, e.g.
  UPDATE items SET barcode = 1 WHERE itemnumber = 1
- Confirm that the text of the error message reads well and is formatted
  well.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:57 +00:00
Jonathan Druart 833937d6d2
Bug 42708: (bug 41566 follow-up) Restore display of itemtype images
> - [% UNLESS noItemTypeImages %]
> + if (prefs.noItemTypeImages){

This is wrong!

Test plan:
Go to /cgi-bin/koha/catalogue/detail.pl?biblionumber=4
Notice that with this patch the "Book" icon is displayed
Turn off noItemTypeImages and reload
=> It's now hidden!

Without this patch the behavior is reversed

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:56 +00:00
Andrii Nugged 2970d72880
Bug 42702: Fix malformed cancel links on parcel.pl
The pending orders table builds the two cancel order links with the
link text concatenated immediately after the invoice_id value. This
leaves the href attribute unclosed and makes the browser absorb the
link text and following escaped HTML into the href.

Add the missing separator after invoice_id for both cancel link builders.

Test plan:
1. Create a closed acquisition basket with a pending order.
2. Open an invoice receive page for the vendor.
3. Inspect the Cancel order link in the pending orders table.
4. Confirm the href ends after invoiceid and the link text is outside the href.

Signed-off-by: Owen Leonard <oleonard@myacpl.org>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:55 +00:00
Jonathan Druart 56075a587c
Bug 42130: ERM - Index biblio when local title is created/updated
The indexation step has been removed by
  commit d2820f57d6
  Bug 34788: (QA follow-up)

Koha/ERM/EHoldings/Title.pm
-            C4::Biblio::ModBiblio( $record, $self->biblio_id, '' );
+            C4::Biblio::ModBiblio( $record, $self->biblio_id, '', { skip_record_index => 1 } );

From the commit message:
2) The background job now uses skip_record_index to avoid queuing indexing jobs for every new biblio and instead queues one job at the end

it's then missing when we add or edit a local title.

Test plan:
1 - Enable ERMModule
2 - Go to ERM
3 - eHoldings->Titles
4 - New title
5 - Populate the form
6 - Check the create bibliographic record box
7 - Save
=> Confirm that the biblio is now indexed
8. Edit the title, modify a value, tick the box, save
=> Confirm that the biblio is now indexed with the new value

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:54 +00:00
Kevin Carnes 2a655af089
Bug 42178: Fix close button for remove from bundle
The close button for remove from bundle has not type, so it submits the form.

This patch adds a button type.

Test plan:
1. Apply bug 42177 or add a local use preference for bundlesEnabled set to 1.
2. Edit a record.
3. Modify the leader so that it has a collection bibliographic level.
4. Ensure that is has at least one item.
5. Click Manage bundle next to the item.
6. Click Remove from bundle.
7. Click the Close button.
8. Observe that the form is submitted.
9. Apply the patch.
10. Repeat steps 5-7.
11. Observe that the form is not submitted.
12. Sign off.

Sponsored-by: Lund University Library
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:53 +00:00
Maryse Simard 0023371a4e
Bug 42193: Fix the "Suspend Hold" modal in the OPAC
In the OPAC, the "Suspend Hold" modal sometimes shows up as (and acts like) a "Resume Hold" modal. The reason for this is a left over "HOLD.suspend" in the template. "HOLD" should not be used in this template and is set as a side effect of the processing of "holds-table.inc".

Since there is a separate form that exist for the "Resume" action and the modal is only needed for suspension, this patch simply removes the inacurate conditional in the modal definition.

To test :
1. Have at least 2 holds.
2. In the holds table in the OPAC, make sure all holds are active and have a "Suspend" link.
3. Click the "Suspend" link on the newest hold (with the highest reserve_id).
    => The modal is titled "Suspend hold" and submitting it correctly suspends the hold.
4. Click the "Suspend" link on any of the other hold.
    => The modal is titled "Resume hold" and submitting it doesn't suspends the hold.
5. Apply patch
6. Repeat step 4.
    => The modal is titled "Suspend hold" and submitting it correctly suspends the hold.

Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:52 +00:00
Julian Maurice 9898bca5d0
Bug 42194: Fix redirect after sharing a report to Mana
Bug 34478 removed a '&' by mistake in the redirect URL

Test plan:
1. Setup a local instance of Mana
   (https://gitlab.com/koha-community/koha-mana) with disabled email
   validation (skip_email_validation in config.yml)
2. Connect Koha to this local instance (mana_config in $KOHA_CONF)
3. From Koha Admin interface, create a Mana security token
4. Share a report to Mana
5. You should be redirected to the list of reports with a message saying
   the report was saved successfully

Signed-off-by: Emmanuel Bétemps <e.betemps@gmail.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:51 +00:00
Martin Renvoize 29b71a0749
Bug 32938: Add ORDRSP message display to basket page
This patch extends the EDIFACT message display functionality on the basket page
to include ORDRSP (Order Response) messages alongside existing QUOTE and ORDERS
messages.

Features added:
- ORDRSP message collection and filtering
- Smart button display logic:
  * Single button for one message type
  * Dropdown for multiple message types (now includes ORDRSP)
- ORDRSP-specific UI elements:
  * Reply icon (fa-reply) indicating response nature
  * "View order response" labeling
  * Integration with existing modal and highlighting system

The ORDRSP messages correctly link to baskets since they are vendor responses
to the original order, following the standard EDI workflow:
ORDERS → ORDRSP → INVOICE

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:50 +00:00
Martin Renvoize 8897fde297
Bug 32938: Fix ORDRSP message linking to point to baskets instead of invoices
ORDRSP (Order Response) messages are responses to ORDERS messages and should
link to the related basket, not to invoices. This follows the standard EDI
workflow:
1. ORDERS message sent to vendor
2. ORDRSP message received from vendor (order response/confirmation)
3. INVOICE message received later when items shipped

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:49 +00:00
Pedro Amorim 370de0a3aa
Bug 42764: DBRev 26.06.00.001
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:48 +00:00
Pedro Amorim 50e1739f87
Bug 42764: dbic
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:47 +00:00
Pedro Amorim 93593ce70c
Bug 42764: (QA follow-up): Update DB schema
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:46 +00:00
Pedro Amorim dcd04a26ca
Bug 42764: (QA follow-up): Atomicupate: Fix typo in SQL table comment
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-05 14:25:43 +00:00
Martin Renvoize 2dc91dcd38
Bug 41705: (follow-up) Fix for new kohaTable requirements
This bug sat in the queue too long and thus there's coding standard
changes to resolve.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
2026-06-04 18:29:08 +01:00
Martin Renvoize 3ed47b65e8
Bug 42764: Use inline codespell:ignore for SAN-Ouest Provence in templates
Rather than excluding the entire about.tt files from codespell scanning,
annotate only the specific lines containing the proper noun "Provence"
(SAN-Ouest Provence, a French library co-operative and Koha sponsor) with
an inline TT comment that codespell recognises as a per-line ignore.

This lets codespell continue to check the rest of each file for genuine
spelling mistakes, while still catching accidental "Provence" for
"Province" in any other template not carrying this annotation.

Replaces the file-level exceptions added to Koha::Devel::Files in
favour of the more surgical inline approach.

Test plan:
1. Apply patch
2. Verify "Provence" on the annotated lines produces no codespell output:
   codespell -d --ignore-words .codespell-ignore \
     koha-tmpl/intranet-tmpl/prog/en/modules/about.tt
   => no output
3. Verify the rest of about.tt is still checked by introducing a
   deliberate typo elsewhere in the file and confirming codespell flags it
4. prove xt/author/codespell.t
=> All tests pass

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
2026-06-04 10:59:21 +01:00
Martin Renvoize cc7c1aa157
Bug 42764: Fix false positives caused by mixed filetypes in CI failure store
The ci-results repo stores all failures from a single test run as one
flat JSON array, regardless of whether they came from the pl, tt, or js
filetype pass. On the next incremental run, get_files_to_test('pl') reads
that entire list -- including any .tt failures -- and calls
remove_exceptions with the 'pl' context. Since .tt files only appear in
the tt exception list, they survive the filter and get codespell'd in the
pl pass where nothing is configured to exclude them.

The fix filters stored failures by filetype before applying exceptions,
so each pass only re-checks failures that belong to its own type.

Test plan:
1. Apply all patches
2. Add a file to the tt codespell exceptions in Koha::Devel::Files that
   contains a word codespell would flag
3. Introduce a deliberate failure in another file so the ci-results repo
   records the excepted .tt file as a previous failure
4. On the next incremental run, confirm the excepted .tt file is not
   re-tested in the pl or js pass
5. prove xt/author/codespell.t
=> Tests pass; no false positives from cross-filetype re-checks

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
2026-06-04 10:59:17 +01:00
Martin Renvoize 84536a08c9
Bug 42764: Include filename in codespell test output
Without a test comment, failures appear as bare 'not ok N' lines with
no indication of which file failed or what codespell found.

Passing the filename as the third argument to is() means:
- Every passing test shows: ok N - path/to/file
- Every failing test shows: not ok N - path/to/file

Test plan:
1. Apply patch
2. Introduce a deliberate spelling error in any .pl file
3. prove xt/author/codespell.t
=> Failing line shows the filename and the codespell finding inline,
   without needing to dig into full console logs

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
2026-06-04 10:59:10 +01:00
Martin Renvoize 5b2293c778
Bug 42764: Fix false positives caused by POD letter-code formatting
Two patterns in the codebase produce spurious codespell hits because
codespell tokenises word fragments out of formatted letter codes:

- B<P>rovider in C4/Auth_with_shibboleth.pm produces 'rovider'
- T(ransit), W(aiting), F(inished), P(rocessing) in C4/Reserves.pm
  produce 'ransit', 'aiting', 'inished', 'rocessing'

For the shibboleth POD line the bold-letter markup cannot be simplified
without losing the formatting intent, so a per-line # codespell:ignore
comment is the right suppression.

For the Reserves.pm status table the fix is made at source: the notation
is changed from X(word) to X (Word) so codespell sees the full English
word and no fragment is produced.

Because the per-line comment on the shibboleth line now suppresses
'rovider' directly, the global .codespell-ignore entry for that fragment
is removed as redundant.

Test plan:
1. Apply patch
2. prove xt/author/codespell.t
=> No new codespell failures

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 16:09:05 +00:00
Martin Renvoize 04809de849
Bug 42764: Fix spelling errors across the codebase
Correct genuine spelling errors identified by the codespell 2.4.x
full scan after the KTD container OS upgrade. Changes are in
comments, POD, strings, and template text across 55 files.

"SAN Ouest Provence" (the French library co-operative and Koha
sponsor) is preserved as a proper noun. Perl copyright lines
receive an inline # codespell:ignore annotation; about.tt files
(contributor attribution pages) are added to the Koha::Devel::Files
codespell exception list so that future uses of "Provence" as a
typo for "Province" are still caught in all other files.

Notable user-visible fixes:
- "submiting" -> "submitting" in authority and bibliographic merge screens
- "deleteable" -> "deletable" in patron exception messages
- "Acquistions" -> "Acquisitions" in reports dictionary

Test plan:
1. Apply patches
2. In the staff interface, go to Cataloguing > Merge records and
   confirm the submit button reads "Submitting" (not "Submiting")
3. In the staff interface, go to Authorities > Merge and confirm
   the same correction
4. Attempt to delete a patron with outstanding issues and confirm
   the error message reads "deletable" (not "deleteable")
5. prove xt/author/codespell.t
=> All tests pass

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 16:09:04 +00:00
Martin Renvoize 7538ee5cf3
Bug 42764: Update .codespell-ignore for valid technical terms
Add words to .codespell-ignore that codespell 2.4.x flags but are
valid technical terms, domain vocabulary, or proper names:

- checkin: core library circulation term (check-in as noun/verb)
- plack: Perl PSGI middleware framework (proper software name)
- requestor: COUNTER 5 standard term used in ERM usage statistics
- laf: EDIFACT 3-letter segment code
- vai/lamba: Unicode script and language names in character set data
- aas: acronym used in Cypress test support files
- carmel: contributor name in about.tt
- prfile: legacy variable abbreviation in C4/Creators/PDF.pm

Test plan:
1. Apply patch
2. prove xt/author/codespell.t
=> Test passes (or only genuine spelling errors remain)

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 16:09:03 +00:00
Owen Leonard 8fc481e57a
Bug 42441: Remove event attributes from authority merge template
This patch moves a couple of "onclick" event attributes from the
authority merge template, moving the event handling to the JS block.

This patch also fixes the behavior that I assume the click handlers are
meant to have, changing the framework selection. A mismatch of IDs in
the code meant that clicking the radio buttons didn't do anything.

To test, apply the patch and go to Authorities.

- Perform a search for authority records which will return multiple
  results, including records with different heading types (e.g. Personal
  Name, Topical Term, etc.)
- In the "Actions" menu for one of the search results, click "Merge"
- Do the same with another result which has a different heading type.
- On the "Merging records" page you should see a frameowrk dropdown.
- When you click the radio buttons next to your two records to merge the
  framework selection should change to match your selection.

Sponsored-by: Athens County Public Libraries
Signed-off-by: Phil Ringnalda <phil@chetcolibrary.org>

Amended-by: Jonathan Druart
Tidy

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 16:09:00 +00:00
Owen Leonard a3569fe0c2
Bug 42154: Bug 38714 hid the "New match check" link in record matching rules
This patch corrects the logic used to determine whether to show the "New
match check" link. I mistakenly changed it to check for the matchpoints
variable instead of the matchchecks variable.

To test, apply the patch and go to Administration -> Record matching
rules.

- Click "New record matching rule" in the toolbar.
- Scroll down to the "Required match checks" section.
- You should see a "New match check" link alongside a "Remove this match
  check" link.
- Go back to the record matching rules main page.
- If necessary, create a rule which doesn't have any match checks.
- In the "Required match checks" section of the edit form you should see
  a "New match check" link, previously hidden by Bug 388714.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:18 +00:00
Marcel de Rooy 6aa37a8430
Bug 42084: (QA follow-up) Simplify
Since this is an intranet svc script (called in intranet js), there
is no need to check the session.

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:17 +00:00
Hammat Wele cb058d68fd
Bug 42084: Incorrect interface shown in log viewer for system preference changes
The log viewer displays the OPAC interface for system preference changes. Since system preferences cannot be modified from the OPAC, the interface displayed in the logs appears to be incorrect.

To reproduce:

1- Disables plack
    1- sudo koha-plack --disable kohadev
    2- sudo koha-plack --stop kohadev
    3- sudo service apache2 restart
2- Change a system preference from the staff interface.
    1- Go to Administration -> System preferences.
    2- Search for a preference such as AcqCreateItem (any system preference will reproduce the issue).
    3- Change the value of the preference.
    4- Click Save all Acquisitions preferences (or save the preference).
3- Go to the log viewer:
    1- Navigate to Tools -> Log viewer.
    2- Fill the form as follows:
        Librarian: leave empty
        Modules: uncheck everything except System preferences
        Actions: keep All actions selected
        Object: leave empty
        Info: leave empty
        Interface: keep All interfaces
        Display from: choose yesterday
        Display to: leave empty
    3- Click Submit.
        ==>The log entry shows the OPAC interface.
4- Apply the patch
5- Repeat step 1,2,3
    ==>The log entry shows the Staff interface.

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:16 +00:00
Jonathan Druart 45f091cbce
Bug 42133: Restore plugins when editing items in batch
It has been broken then fixed then broken again, several times...

This patch fixes 2 problems:
1. The CSRF fix (I think it's coming from there) didn't adjust the 'if show' test and the JS code from plugins was not injected into the template
2. The selector to call the plugin didn't include this view, only the
   add/edit items

Test plan:
Confirm that you can now use cataloging plugins when editing items in
batch

Signed-off-by: Emmi Takkinen <emmi.takkinen@koha-suomi.fi>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:15 +00:00
Pedro Amorim 66c5eb6f29
Bug 34973: Update yarn.lock
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:14 +00:00
Owen Leonard cac6637d83
Bug 34973: (follow-up) Remove old Font Awesome asset references from SCO and SCI
The self-checkout and self-checkin templates no longer need explicit
inclusion of Font Awesome CSS because it's rolled into _common.scss and
thus sco.scss.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:13 +00:00
Owen Leonard bbd56c05d4
Bug 34973: (follow-up) Use lib/fontawesome for font location
It makes more sense to use lib/fontawesome/webfonts as the source
directory for Font Awesome font files rather than bootstrap/css/fonts.

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:12 +00:00
Jonathan Druart e169d53c58
Bug 34973: Adjust opac-curbside-pickups.tt
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:11 +00:00
Owen Leonard 1e8c552fa9
Bug 34973: Upgrade Font Awesome in the OPAC to 7.2
We are using Font Awesome 6.3 in the OPAC. The current version is 7.2.
Upgrading will eliminate the repeated warnings in the browser console
about "Glyph bbox was incorrect."

The patch adds Font Awesome to the list of packages in node_modules and
moves the Font Awesome SCSS into _common.scss so that it can be bundled
along with Bootstrap and other 3rd party assets.

Despite the fact that _common.scss grabs FA SCSS files directly from
node_modules I've added the FA font files to css/fonts. I was unable to
see how to reference the webfonts directory in
node_modules/@fortawesome.

The many changed .inc and .tt files are a result of a missed aspect of
our upgrade to Bootstrap 5. The "sr-only" class was removed in BS5 but
the Font Awesome CSS included its own "sr-only" class. This made up for
the fact that we missed the required upgrade to BS5's "visually-hidden."
As soon as Font Awesome's "sr-only" was gone all these mistakes were
made visible.

To test, apply the patch and run 'yarn install.'
- Rebuild the OPAC SCSS.
- Clear your browser cache.
- Test many pages in the OPAC to confirm that no icons are missing.
- Check changes to the templates to confirm that "sr-only" has been
  replaced with "visually-hidden."
  - Places where a broken "sr-only" class would be obvious: Star ratings
    on the user summary page, <legend>s on the search history page.

Sponsored-by: Athens County Public Libraries
Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:10 +00:00
Martin Renvoize 1d5c4cf19d
Bug 41705: (follow-up) Hide receipt page buttons when printing
The Print receipt and Close buttons added in the previous patch use
class="no-print", but printreceiptinvoice.css had no @media print rule
for that class — so they appeared in the printed output (e.g. when
saving as PDF or auto-printing in browsers that block popups).

Add a @media print rule so the buttons remain visible on screen but
disappear from the printed receipt.

Test plan:
1. Disable browser popups (Chrome: "Don't allow sites to send pop-ups
   or use redirects").
2. Enable FinePaymentAutoPopup.
3. Complete a POS sale.
4. On the receipt page, choose Print to PDF (or trigger auto-print).
5. Verify the printed/PDF output contains only the receipt body — no
   Print receipt or Close buttons.
6. Verify the buttons remain visible on screen.

Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:09 +00:00
Martin Renvoize 23320ff13c
Bug 41705: Use PRG pattern for POS receipt printing to avoid popup blockers
The previous approach triggered popup blockers because the receipt window
was opened via JavaScript after a page reload — browsers treat this as a
non-user-initiated action. This patch reverses the flow: when
FinePaymentAutoPopup is enabled, the payment form submits directly to a
new tab via target="_blank" (a user-initiated action that browsers allow),
the server processes the payment and redirects (303) to the receipt page,
which auto-prints and closes itself, returning focus to the POS tab.

When FinePaymentAutoPopup is disabled, the server redirects back to the
POS page with payment details in the URL (standard PRG), displaying the
existing success banner with print/email receipt buttons.

Changes:
- pos/pay.pl: Implement PRG redirect for both popup and non-popup flows,
  URI-encode redirect parameters, remove session-based state management
- pos/pay.tt: Add target="_blank" when FinePaymentAutoPopup is enabled,
  reset form state after submission via same-scope setTimeout
- pos/printreceipt.pl: Add error handling for missing receipts, pass
  autoprint parameter to template
- pos/printreceipt.tt: Add conditional auto-print, manual print/close
  buttons, error display, and IntranetSlipPrinterJS preference support
- slip-print.inc: Modernize jQuery to vanilla JS, remove obsolete
  Chrome setTimeout hack

Test plan:
1. Enable FinePaymentAutoPopup system preference
2. Go to Point of Sale, add items, and complete a sale
3. Verify receipt opens in a new tab without popup blocker warnings
4. Verify receipt auto-prints and tab closes after printing
5. Verify original POS tab form is reset for next transaction
6. Disable FinePaymentAutoPopup
7. Complete another sale
8. Verify POS page shows success with Print/Email receipt buttons
9. Test receipt printing from other pages (circulation, fees) to
   confirm slip-print.inc change causes no regressions

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:08 +00:00
Owen Leonard 7bde3cbb79
Bug 42475: (follow-up) Fix a couple more instances of 'opac'
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:07 +00:00
Jonathan Druart 93f4a7f65b
Bug 42475: Fix opac vs OPAC
Correct "opac" with "OPAC" in the pref file

Test plan:
git grep opac **/*.pref
and confirm that there is no relevant occurrences in the output (i.e.
no strings displayed on the UI)

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-03 12:08:03 +00:00
Pedro Amorim 8c4344cced
Bug 42705: Fix SCO spec WebBasedSelfCheck setup
cy.login() in beforeEach broke the test in two ways: the intranet staff session cookie bled into the OPAC request (HTTP cookies have no port scope in
browsers), and navigating from intranet→OPAC mid-test hit a cross-origin block in Cypress. The afterEach suffered the same race condition - the syspref POST fired before the login redirect completed.

Replace with cy.request(), which makes HTTP calls at the runner level without touching the browser's current origin. cy.visitOpac() is then the first
browser navigation, so no cross-origin issue arises. A three-step flow satisfies Koha's CSRF middleware:
1) GET the login page to obtain a session and its token
2) POST credentials to authenticate
3) POST to the svc endpoint.

This fixes the #barcode and #patronlogin not found issues

The CSRF token also requires encodeURIComponent() — base64 uses +, which a
URL-encoded form body decodes as a space, silently corrupting the token and causing the login to fail.

This resolves the 403 Forbidden issue

Everything must pass:
$ cypress run --spec t/cypress/integration/OPAC/SCO_spec.ts

as well as npm run cypress open (cypress ui)

ktd selenium:
$ perl /kohadevbox/misc4dev/run_tests.pl --run-only t/cypress/integration/OPAC/SCO_spec.ts

squash

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-02 15:07:22 +00:00
Pedro Amorim d5ae629d8c
Bug 42705: Add HttpClient_spec.ts
This fails without the fix patch in this set:
cypress run --spec t/cypress/integration/HttpClient_spec.ts

It passes with the fix

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-02 15:07:21 +00:00
Pedro Amorim bdd552a05d
Bug 42705: Make http-client.js handle svc endpoint error
The afterEach calls cy.login() then immediately posts to the syspref endpoint. On CI, the machine is slow enough that the POST fires before the login
redirect completes — no valid session yet — so the endpoint returns a C4::Service auth error in this format:

{ "type": "auth", "message": "expired" }

http-client.js only knew how to parse modern REST API errors (json.error or json.errors[]). Neither field exists here, so json.errors.map(...) crashed with a TypeError instead of surfacing the actual error message.

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-02 15:07:19 +00:00
Pedro Amorim dc59aadc4a
Bug 42733: Add tests
Apply tests patch only, run:
yarn cypress run --spec t/cypress/integration/Tools/ManageMarcImport_spec.ts

Confirm it fails with the same error as Jenkins:
Timed out retrying after 10000ms: `cy.wait()` timed out waiting `10000ms` for the 2nd request to the route: `jobPoll`. No request ever occurred.

Apply fix patch. Run again.

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-02 15:07:18 +00:00
Pedro Amorim b6af7a6c3a
Bug 42733: Job progress fix
waitForJobFinished previously had no handling for failed jobs or
non-200 responses — if the job API returned anything unexpected,
the recursive cy.wait() would silently stall waiting for a 2nd
poll that never came, producing a confusing timeout. Adds an
explicit "failed" status check and updates the intercept
to reproduce the exact CI failure (Apache returning 503 during
reset_all).

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-02 15:07:17 +00:00
Martin Renvoize 726b205be2
Bug 42726: Add 26.11 release team
Add the 26.11 release team as voted at the development meeting.

Pedro Amorim as Release Manager, with Tomás Cohen Arazi and Martin
Renvoize as RM Assistants. Lisette Scheer as QA Manager with a
19-person QA team. David Cook as Security Release Manager.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Owen Leonard <oleonard@myacpl.org>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-02 15:07:16 +00:00
Martin Renvoize 3b4c09d753
Bug 42726: Fix 25.11 QA team list to match dashboard
Update the 25.11 QA team to reflect the definitive list from the
dashboard. Remove Aleisha Amohia and Julian Maurice who did not
appear in the final count, and add John Vinke, Matt Blenkinsop,
Alex Carver and Victor Grousset who did. Reorder by descending
QA count.

Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Owen Leonard <oleonard@myacpl.org>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-02 15:07:14 +00:00
Pedro Amorim c98e3a8247
DBRev 26.06.00.000: Start of a new release cycle
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
2026-06-02 15:07:13 +00:00
1170 changed files with 55852 additions and 23185 deletions

View File

@ -1,7 +1,6 @@
ser
thes
nam
rovider
fo
isnt
wither
@ -18,3 +17,12 @@ theses
rouge
connexion
falsy
aas
carmel
checkin
laf
lamba
plack
prfile
requestor
vai

View File

@ -230,6 +230,8 @@ 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>

View File

@ -28,7 +28,6 @@ BEGIN {
}
use C4::Context;
use C4::Stats;
use C4::Members;
use Koha::Account;
use Koha::Account::Lines;

View File

@ -2418,6 +2418,7 @@ 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,
@ -2432,7 +2433,20 @@ sub GetInvoices {
NULL
)
) AS is_linked_to_subscriptions,
SUM(aqorders.quantityreceived) AS receiveditems
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
FROM aqinvoices
LEFT JOIN aqbooksellers ON aqbooksellers.id = aqinvoices.booksellerid
LEFT JOIN aqorders ON aqorders.invoiceid = aqinvoices.invoiceid
@ -2525,12 +2539,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";
" 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";
if ( $args{order_by} ) {
my ( $column, $direction ) = split / /, $args{order_by};
if ( grep { $_ eq $column } @columns ) {
$direction ||= 'ASC';
$direction = ( uc( $direction // '' ) eq 'DESC' ) ? 'DESC' : 'ASC';
$query .= " ORDER BY $column $direction";
}
}

View File

@ -275,15 +275,18 @@ 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 => '',
-value => $session->id,
-HttpOnly => 1,
-secure => ( C4::Context->https_enabled() ? 1 : 0 ),
-sameSite => 'Lax',

View File

@ -312,7 +312,7 @@ Create some metadata for your koha instance (if you're in a single instance setu
=item 2.
Swap metadata with your Identidy Provider (IdP)
Swap metadata with your Identify Provider (IdP)
=item 3.
@ -365,7 +365,7 @@ Note: The minimum you need here is a <matchpoint> block, containing a valid colu
It should be as simple as that; you should now be able to login via shibboleth in the opac.
If you need more help configuring your B<S>ervice B<P>rovider to authenticate against a chosen B<Id>entity B<P>rovider then it might be worth taking a look at the community wiki L<page|https://wiki.koha-community.org/wiki/Shibboleth_Configuration>
If you need more help configuring your B<S>ervice B<P>rovider to authenticate against a chosen B<Id>entity B<P>rovider then it might be worth taking a look at the community wiki L<page|https://wiki.koha-community.org/wiki/Shibboleth_Configuration> # codespell:ignore
=head1 FUNCTIONS

View File

@ -620,16 +620,7 @@ sub AddAuthority {
}
my $date = POSIX::strftime( "%y%m%d", localtime );
if ( !$record->field('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 ) );
$record->insert_fields_ordered( MARC::Field->new( '008', $date . Koha::Authority->default_marc21_008 ) );
}
if ( !$record->field('040') ) {
$record->insert_fields_ordered(
@ -1120,7 +1111,8 @@ sub BuildSummary {
};
}
}
foreach my $field ( $record->field('4..') ) { #See From
my @fields = _marc21_sort_hierarchy_alpha( $record->field('4..') );
foreach my $field (@fields) { #See From
my $type = 'seefrom';
$type = ( $marc21controlrefs{ substr $field->subfield('w'), 0, 1 } || '' ) if ( $field->subfield('w') );
if ( $type eq 'notapplicable' ) {
@ -1143,7 +1135,8 @@ sub BuildSummary {
};
}
}
foreach my $field ( $record->field('5..') ) { #See Also
@fields = _marc21_sort_hierarchy_alpha( $record->field('5..') );
foreach my $field (@fields) { #See Also
my $type = 'seealso';
$type = ( $marc21controlrefs{ substr $field->subfield('w'), 0, 1 } || '' ) if ( $field->subfield('w') );
if ( $type eq 'notapplicable' ) {
@ -1275,6 +1268,32 @@ 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 })

View File

@ -99,6 +99,7 @@ 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;
@ -301,6 +302,7 @@ sub AddBiblio {
{
skip_record_index => 1,
record_source_id => $options->{record_source_id},
( ref $options->{warnings} eq 'ARRAY' ? ( warnings => $options->{warnings} ) : () ),
}
);
@ -330,8 +332,14 @@ sub AddBiblio {
$indexer->index_records( $biblionumber, "specialUpdate", "biblioserver" );
}
} catch {
warn $_;
( $biblionumber, $biblioitemnumber ) = ( undef, undef );
my $error = $_;
if ( ref($error) eq 'Koha::Exceptions::Metadata::Invalid' ) {
( $biblionumber, $biblioitemnumber ) = ( undef, undef );
$error->rethrow;
} else {
warn $error;
( $biblionumber, $biblioitemnumber ) = ( undef, undef );
}
};
return ( $biblionumber, $biblioitemnumber );
}
@ -399,7 +407,12 @@ sub ModBiblio {
( exists $options->{record_source_id} )
? ( record_source_id => $options->{record_source_id} )
: ()
)
),
(
( ref $options->{warnings} eq 'ARRAY' )
? ( warnings => $options->{warnings} )
: ()
),
};
if ( !$record ) {
@ -799,10 +812,47 @@ sub LinkBibHeadingsToAuthorities {
if ( C4::Context->preference('marcflavour') eq 'MARC21' ) {
my $userenv = C4::Context->userenv;
my $library;
if ( $userenv && $userenv->{'branch'} ) {
$library = Koha::Libraries->find( $userenv->{'branch'} );
# 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 ) );
}
$marcrecordauth->insert_fields_ordered(
MARC::Field->new(
'667', '', '',
@ -813,10 +863,8 @@ sub LinkBibHeadingsToAuthorities {
$cite =~ s/^[\s\,]*//;
$cite =~ s/[\s\,]*$//;
$cite =
C4::Context->preference('GenerateAuthorityField670') . ": ("
. (
$library ? $library->get_effective_marcorgcode : C4::Context->preference('MARCOrgCode') )
. ")"
C4::Context->preference('GenerateAuthorityField670') . ": ("
. "$marcorgcode)"
. $bib->subfield( '999', 'c' ) . ": "
. $cite;
$marcrecordauth->insert_fields_ordered( MARC::Field->new( '670', '', '', 'a' => $cite ) );
@ -2352,7 +2400,7 @@ sub TransformHtmlToMarc {
# between 001 and 009 (included)
} elsif ( $fval ne '' ) {
$fval = StripNonXmlChars($fval); #Strip out any non-XML characters like control characters
$fval = StripNonXmlChars($fval);
$newfield = MARC::Field->new( $tag, $fval, );
}
@ -2377,7 +2425,7 @@ sub TransformHtmlToMarc {
if ( $fval ne '' && $newfield ) {
$newfield->add_subfields( $fkey => $fval );
} elsif ( $fval ne '' ) {
$fval = StripNonXmlChars($fval); #Strip out any non-XML characters like control characters
$fval = StripNonXmlChars($fval);
$newfield = MARC::Field->new( $tag, $ind1, $ind2, $fkey => $fval );
}
$j += 2;
@ -3030,6 +3078,12 @@ 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" );

View File

@ -155,7 +155,9 @@ sub Z3950Search {
}
my $xslh = Koha::XSLT::Base->new;
my $nremaining = $s;
my $nremaining = $s;
my $hits_per_page = 20; #TODO make configurable
my $total_hits = 0;
while ( $nremaining-- ) {
my $k;
my $event;
@ -169,18 +171,22 @@ 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}, error => $error } );
push( @errconn, { server => $servers[$k]->{host}, errcode => $error } );
}
} else {
my $numresults = $oResult[$k]->size();
$total_hits += $numresults;
my $i;
my $res;
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 );
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 );
for (
$i = ( $page - 1 ) * 20 ;
$i < ( ( $numresults < ( $page * 20 ) ) ? $numresults : ( $page * 20 ) ) ;
$i = ( $page - 1 ) * $hits_per_page ;
$i < (
( $numresults < ( $page * $hits_per_page ) ) ? $numresults : ( $page * $hits_per_page )
) ;
$i++
)
{
@ -191,14 +197,17 @@ sub Z3950Search {
$xslh
); #ignores error in sequence numbering
push @breeding_loop, $res if $res;
push @errconn, { server => $servers[$k]->{servername}, error => $error, seq => $i + 1 }
push @errconn, { server => $servers[$k]->{servername}, errcode => $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},
error => ( ( $oConnection[$k]->error_x() )[0] ),
error_msg => ( ( $oConnection[$k]->error_x() )[1] ), seq => $i + 1
server => $servers[$k]->{servername},
errcode => $diags[0],
errmsg => $diags[1],
seq => $i + 1,
};
}
}
@ -212,6 +221,7 @@ sub Z3950Search {
total_pages => $total_pages,
show_nextbutton => $show_next ? 1 : 0,
show_prevbutton => $page != 1,
total_hits => $total_hits,
);
} # while nremaining

View File

@ -62,7 +62,6 @@ BEGIN {
GetBudgetChildren
SetOwnerToFundHierarchy
CheckBudgetParent
CheckBudgetParentPerm
HideCols
GetCols
@ -136,30 +135,11 @@ 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
Missing POD for AddBudgetPeriod.
Create a BudgetPeriod period with given parameters
my $rs = AddBudgetPeriod({ budget_period_startdate => $start_date, budget_period_enddate => $budget_period_enddate });
=cut

View File

@ -21,6 +21,7 @@ use vars qw(@EXPORT);
use Carp qw( croak );
use Date::Calc qw( Today );
use Koha::Library::Calendar;
use C4::Context;
use Koha::Caches;
@ -64,30 +65,32 @@ 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 $repeatable = $dbh->prepare(
'SELECT *
FROM repeatable_holidays
WHERE ( branchcode = ? )
AND (ISNULL(weekday) = ?)'
);
$repeatable->execute( $branch, 0 );
my %week_days_holidays;
my $dbh = C4::Context->dbh();
while ( my $row = $repeatable->fetchrow_hashref ) {
my $weekly_sth = $dbh->prepare('SELECT * FROM library_weekly_closures WHERE library_id = ?');
$weekly_sth->execute($branch);
my %week_days_holidays;
while ( my $row = $weekly_sth->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;
$repeatable->execute( $branch, 1 );
my $repeating_sth = $dbh->prepare('SELECT * FROM library_repeating_closures WHERE library_id = ?');
$repeating_sth->execute($branch);
my %day_month_holidays;
while ( my $row = $repeatable->fetchrow_hashref ) {
while ( my $row = $repeating_sth->fetchrow_hashref ) {
my $key = $row->{month} . "/" . $row->{day};
$day_month_holidays{$key}{title} = $row->{title};
$day_month_holidays{$key}{description} = $row->{description};
@ -96,29 +99,26 @@ sub _init {
}
$self->{'day_month_holidays'} = \%day_month_holidays;
my $special = $dbh->prepare(
'SELECT day, month, year, title, description
FROM special_holidays
WHERE ( branchcode = ? )
AND (isexception = ?)'
);
$special->execute( $branch, 1 );
my $exceptions_sth =
$dbh->prepare('SELECT date, title, description FROM library_closure_exceptions WHERE library_id = ?');
$exceptions_sth->execute($branch);
my %exception_holidays;
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 );
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};
}
$self->{'exception_holidays'} = \%exception_holidays;
$special->execute( $branch, 0 );
my $single_sth = $dbh->prepare('SELECT date, title, description FROM library_single_closures WHERE library_id = ?');
$single_sth->execute($branch);
my %single_holidays;
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 );
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};
}
$self->{'single_holidays'} = \%single_holidays;
return $self;
@ -202,17 +202,13 @@ C<$description> Is the description to store for the holiday formed by $year/$mon
sub insert_week_day_holiday {
my $self = shift @_;
my %options = @_;
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,?,? )"
Koha::Library::Calendar->new( branchcode => $self->{branchcode} )->add_weekly_closure(
{
weekday => $options{weekday}, title => $options{title}, 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};
$self->{'week_days_holidays'}->{ $options{weekday} }{title} = $options{title};
$self->{'week_days_holidays'}->{ $options{weekday} }{description} = $options{description};
return $self;
}
@ -238,14 +234,11 @@ C<$description> Is the description to store for the holiday formed by $year/$mon
sub insert_day_month_holiday {
my $self = shift @_;
my %options = @_;
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}
Koha::Library::Calendar->new( branchcode => $self->{branchcode} )->add_repeating_closure(
{
day => $options{day}, month => $options{month}, title => $options{title},
description => $options{description},
}
);
$self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{title} = $options{title};
$self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{description} = $options{description};
@ -277,28 +270,23 @@ C<$description> Is the description to store for the holiday formed by $year/$mon
sub insert_single_holiday {
my $self = shift @_;
my %options = @_;
@options{qw(year month day)} = ( $options{date} =~ m/(\d+)-(\d+)-(\d+)/o )
if $options{date} && !$options{day};
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};
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}
Koha::Library::Calendar->new( branchcode => $self->{branchcode} )->add_single_closure(
{
date => $date, title => $options{title}, description => $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
@ -326,28 +314,23 @@ 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};
@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_exception(
{
date => $date, title => $options{title}, description => $options{description},
}
);
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;
}
@ -368,13 +351,10 @@ C<$description> Is the description to update for the holiday.
sub ModWeekdayholiday {
my $self = shift @_;
my %options = @_;
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};
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;
return $self;
}
@ -400,16 +380,9 @@ C<$description> The description to be update for the holiday.
sub ModDaymonthholiday {
my $self = shift @_;
my %options = @_;
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};
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;
return $self;
}
@ -438,25 +411,12 @@ C<$description> Is the description to update for the holiday formed by $year/$mo
sub ModSingleholiday {
my $self = shift @_;
my %options = @_;
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);
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;
}
return $self;
}
@ -485,25 +445,12 @@ C<$description> Is the description to be modified for the holiday formed by $yea
sub ModExceptionholiday {
my $self = shift @_;
my %options = @_;
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);
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;
}
return $self;
}
@ -529,67 +476,40 @@ 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} );
# 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.
if ( $options{day} && $options{month} && $options{year} ) {
my $date = sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} );
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}"} );
}
# Try single closure first
if ( Koha::Library::Calendar::SingleClosures->search( { library_id => $self->{branchcode}, date => $date } )
->count )
{
$cal->delete_single_closure( { date => $date } );
}
# Also remove any exception for this date
Koha::Library::Calendar::Exceptions->search( { library_id => $self->{branchcode}, date => $date } )->delete;
}
# 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);
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} } );
}
return $self;
}
@ -613,17 +533,8 @@ C<$year> Is year to make the date to delete.
sub delete_holiday_range {
my $self = shift;
my %options = @_;
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);
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 } );
}
=head2 delete_holiday_range_repeatable
@ -642,15 +553,8 @@ C<$month> Is month to make the date to delete.
sub delete_holiday_range_repeatable {
my $self = shift;
my %options = @_;
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);
Koha::Library::Calendar->new( branchcode => $self->{branchcode} )
->delete_repeating_closure( { day => $options{day}, month => $options{month} } );
}
=head2 delete_exception_holiday_range
@ -673,17 +577,8 @@ C<$year> Is year to make the date to delete.
sub delete_exception_holiday_range {
my $self = shift;
my %options = @_;
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);
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 } );
}
=head2 isHoliday
@ -734,47 +629,7 @@ sub isHoliday {
sub copy_to_branch {
my ( $self, $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;
return Koha::Library::Calendar->new( branchcode => $self->{branchcode} )->copy_to($target_branch);
}
1;

View File

@ -49,7 +49,6 @@ BEGIN {
GetUpcomingDueIssues
CheckIfIssuedToPatron
IsItemIssued
GetAgeRestriction
GetTopIssues
AddReturn
@ -73,7 +72,6 @@ BEGIN {
ProcessOfflinePayment
ProcessOfflineIssue
);
push @EXPORT_OK, '_GetCircControlBranch'; # This is wrong!
}
use DateTime;
@ -83,7 +81,7 @@ use Try::Tiny;
use JSON;
use C4::Context;
use C4::Stats qw( UpdateStats );
use Koha::Policy::Circulation;
use C4::Reserves
qw( CheckReserves CanItemBeReserved MoveReserve ModReserve ModReserveMinusPriority IsAvailableForItemLevelRequest );
use C4::Biblio qw( UpdateTotalIssues );
@ -102,12 +100,14 @@ use Koha::AuthorisedValues;
use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
use Koha::Biblioitems;
use Koha::DateUtils qw( dt_from_string );
use Koha::Calendar;
use Koha::Library::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,6 +123,7 @@ 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 );
@ -393,7 +394,7 @@ sub transferbook {
}
# find recall
if ( C4::Context->preference('UseRecalls') ) {
if ( C4::Context->preference('UseRecalls') ne "off" ) {
my $recall = Koha::Recalls->find( { item_id => $itemnumber, status => 'in_transit' } );
if ( defined $recall ) {
@ -435,7 +436,7 @@ sub TooMany {
my $cat_borrower = $patron->categorycode;
# Get which branchcode we need
my $branch = _GetCircControlBranch( $item, $patron );
my $branch = Koha::Policy::Circulation->circ_control_library( $item, $patron );
my $type = $item->effective_itemtype;
my ( $type_object, $parent_type, $parent_maxissueqty_rule );
@ -859,10 +860,12 @@ sub CanBookBeIssued {
my $dbh = C4::Context->dbh;
my $patron_unblessed = $patron->unblessed;
my $circ_library = Koha::Libraries->find( _GetCircControlBranch( $item_object, $patron ) );
my $circ_library =
Koha::Libraries->find( Koha::Policy::Circulation->circ_control_library( $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
@ -885,7 +888,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 .
C4::Stats::UpdateStats(
Koha::Statistic->new(
{
branch => C4::Context->userenv->{'branch'},
type => 'localuse',
@ -897,7 +900,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, }, {} );
@ -973,6 +976,29 @@ 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};
@ -1211,7 +1237,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') and $item_object->can_be_waiting_recall ) {
if ( C4::Context->preference('UseRecalls') ne "off" 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) {
@ -1259,16 +1285,34 @@ sub CanBookBeIssued {
if ($restype) {
my $resbor = $res->{'borrowernumber'};
if ( $resbor ne $patron->borrowernumber ) {
my $patron = Koha::Patrons->find($resbor);
if ( $restype eq "Waiting" ) {
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" ) {
# The item is on reserve and waiting, but has been
# reserved by some other patron.
$needsconfirmation{RESERVE_WAITING} = 1;
$needsconfirmation{'resfirstname'} = $patron->firstname;
$needsconfirmation{'ressurname'} = $patron->surname;
$needsconfirmation{'rescardnumber'} = $patron->cardnumber;
$needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
$needsconfirmation{'resfirstname'} = $hold_patron->firstname;
$needsconfirmation{'ressurname'} = $hold_patron->surname;
$needsconfirmation{'rescardnumber'} = $hold_patron->cardnumber;
$needsconfirmation{'resborrowernumber'} = $hold_patron->borrowernumber;
$needsconfirmation{'resbranchcode'} = $res->{branchcode};
$needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
$needsconfirmation{'reserve_id'} = $res->{reserve_id};
@ -1276,10 +1320,10 @@ sub CanBookBeIssued {
# The item is on reserve for someone else.
$needsconfirmation{RESERVED} = 1;
$needsconfirmation{'resfirstname'} = $patron->firstname;
$needsconfirmation{'ressurname'} = $patron->surname;
$needsconfirmation{'rescardnumber'} = $patron->cardnumber;
$needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
$needsconfirmation{'resfirstname'} = $hold_patron->firstname;
$needsconfirmation{'ressurname'} = $hold_patron->surname;
$needsconfirmation{'rescardnumber'} = $hold_patron->cardnumber;
$needsconfirmation{'resborrowernumber'} = $hold_patron->borrowernumber;
$needsconfirmation{'resbranchcode'} = $res->{branchcode};
$needsconfirmation{'resreservedate'} = $res->{reservedate};
$needsconfirmation{'reserve_id'} = $res->{reserve_id};
@ -1287,10 +1331,10 @@ sub CanBookBeIssued {
# The item is determined hold being transferred for someone else.
$needsconfirmation{TRANSFERRED} = 1;
$needsconfirmation{'resfirstname'} = $patron->firstname;
$needsconfirmation{'ressurname'} = $patron->surname;
$needsconfirmation{'rescardnumber'} = $patron->cardnumber;
$needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
$needsconfirmation{'resfirstname'} = $hold_patron->firstname;
$needsconfirmation{'ressurname'} = $hold_patron->surname;
$needsconfirmation{'rescardnumber'} = $hold_patron->cardnumber;
$needsconfirmation{'resborrowernumber'} = $hold_patron->borrowernumber;
$needsconfirmation{'resbranchcode'} = $res->{branchcode};
$needsconfirmation{'resreservedate'} = $res->{reservedate};
$needsconfirmation{'reserve_id'} = $res->{reserve_id};
@ -1298,10 +1342,10 @@ sub CanBookBeIssued {
# The item is determined hold being processed for someone else.
$needsconfirmation{PROCESSING} = 1;
$needsconfirmation{'resfirstname'} = $patron->firstname;
$needsconfirmation{'ressurname'} = $patron->surname;
$needsconfirmation{'rescardnumber'} = $patron->cardnumber;
$needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
$needsconfirmation{'resfirstname'} = $hold_patron->firstname;
$needsconfirmation{'ressurname'} = $hold_patron->surname;
$needsconfirmation{'rescardnumber'} = $hold_patron->cardnumber;
$needsconfirmation{'resborrowernumber'} = $hold_patron->borrowernumber;
$needsconfirmation{'resbranchcode'} = $res->{branchcode};
$needsconfirmation{'resreservedate'} = $res->{reservedate};
$needsconfirmation{'reserve_id'} = $res->{reserve_id};
@ -1326,6 +1370,21 @@ 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
@ -1343,9 +1402,9 @@ sub CanBookBeIssued {
}
## CHECK AGE RESTRICTION
my $agerestriction = $biblioitem->agerestriction;
my $restriction_age = GetAgeRestriction($agerestriction);
if ( $restriction_age && $patron->dateofbirth && $restriction_age > $patron->get_age() ) {
my $age_check = Koha::Policy::Biblio::AgeRestriction->check( $biblio, $patron );
if ( !$age_check ) {
my $agerestriction = $biblioitem->agerestriction;
if ( C4::Context->preference('AgeRestrictionOverride') ) {
$needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
} else {
@ -1461,37 +1520,7 @@ Returns:
sub CanBookBeReturned {
my ( $item, $returnbranch, $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 );
return $item->can_be_returned_at( { library => $returnbranch, to_library => $transferbranch } );
}
=head2 CheckHighHolds
@ -1510,7 +1539,7 @@ Missing POD for checkHighHolds.
sub checkHighHolds {
my ( $item, $patron ) = @_;
my $branchcode = _GetCircControlBranch( $item, $patron );
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
my $return_data = {
exceeded => 0,
@ -1584,7 +1613,7 @@ sub checkHighHolds {
branchcode => $branchcode,
}
);
my $calendar = Koha::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
my $calendar = Koha::Library::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $patron );
@ -1700,7 +1729,7 @@ sub AddIssue {
or return; # if we don't get an Item, abort.
my $item_unblessed = $item_object->unblessed;
my $branchcode = _GetCircControlBranch( $item_object, $patron );
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item_object, $patron );
# get actual issuing if there is one
my $actualissue = $item_object->checkout;
@ -1774,7 +1803,7 @@ sub AddIssue {
$item_object->discard_changes;
}
if ( C4::Context->preference('UseRecalls') ) {
if ( C4::Context->preference('UseRecalls') ne "off" ) {
Koha::Recalls->move_recall(
{
action => $cancel_recall,
@ -1854,8 +1883,36 @@ sub AddIssue {
{
if ( $booking->patron_id == $patron->borrowernumber ) {
# Patron's own booking - mark as completed and link checkout to booking
$booking->status('completed')->store;
# 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;
}
};
$issue_attributes->{'booking_id'} = $booking->booking_id;
} else {
@ -1959,7 +2016,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 && $charge > 0 ) {
if ( $charge > 0 ) {
AddIssuingCharge( $issue, $charge, 'RENT' );
}
@ -1985,7 +2042,7 @@ sub AddIssue {
}
# Record the fact that this book was issued.
C4::Stats::UpdateStats(
Koha::Statistic->new(
{
branch => C4::Context->userenv->{'branch'},
type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
@ -1998,7 +2055,7 @@ sub AddIssue {
ccode => $item_object->ccode,
categorycode => $patron->categorycode
}
);
)->store();
# Send a checkout slip.
my $circulation_alert = 'C4::ItemCirculationAlertPreference';
@ -2021,19 +2078,16 @@ sub AddIssue {
# Log the checkout
if ( C4::Context->preference('IssueLog') ) {
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 }
);
}
my $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,
@ -2353,51 +2407,13 @@ sub AddReturn {
my $validTransfer = 1;
my $stat_type = 'return';
# get information on item
# Get item
my $item = Koha::Items->find( { barcode => $barcode } );
unless ($item) {
return ( 0, { BadBarcode => $barcode } ); # no barcode means no item or borrower. bail out.
}
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)
# Check availability
my $hbr = Koha::CirculationRules->get_return_branch_policy($item);
# check if returnbranch and homebranch belong to the same float group
@ -2434,6 +2450,66 @@ 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 : {};
@ -2470,22 +2546,22 @@ sub AddReturn {
}
}
# check if the return is allowed at this branch
my ( $returnallowed, $message ) = CanBookBeReturned( $item, $branch, $returnbranch );
# 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;
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;
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;
}
}
# case of a return of document (deal with issues and holdingbranch)
@ -2494,6 +2570,7 @@ 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,
@ -2516,6 +2593,13 @@ 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
@ -2685,7 +2769,7 @@ sub AddReturn {
}
# find recalls...
if ( C4::Context->preference('UseRecalls') ) {
if ( C4::Context->preference('UseRecalls') ne "off" ) {
# check if this item is recallable first, which includes checking if UseRecalls syspref is enabled
my $recall = undef;
@ -2720,7 +2804,7 @@ sub AddReturn {
# Record the fact that this book was returned.
my $categorycode = $patron_unblessed ? $patron_unblessed->{categorycode} : undef;
C4::Stats::UpdateStats(
Koha::Statistic->new(
{
branch => $branch,
type => $stat_type,
@ -2732,7 +2816,8 @@ sub AddReturn {
categorycode => $categorycode,
interface => C4::Context->interface,
}
) unless ( $skip_localuse && !$issue );
)->store()
unless ( $skip_localuse && !$issue );
# Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
if ($patron) {
@ -2775,7 +2860,7 @@ sub AddReturn {
$request->status('RET') if $request;
}
if ( C4::Context->preference('UseRecalls') ) {
if ( C4::Context->preference('UseRecalls') ne "off" ) {
# 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' } );
@ -2918,7 +3003,7 @@ sub MarkIssueReturned {
my $schema = Koha::Database->schema;
# FIXME Improve the return value and handle it from callers
$schema->txn_do(
my $rv = $schema->txn_do(
sub {
my $patron = Koha::Patrons->find($borrowernumber);
@ -2930,7 +3015,9 @@ sub MarkIssueReturned {
$issue->returndate( \'NOW()' )->store->discard_changes; # update and refetch
}
$issue->checkin_library( C4::Context->userenv->{'branch'} );
my $checkin_library = defined C4::Context->userenv
&& exists C4::Context->userenv->{'branch'} ? C4::Context->userenv->{'branch'} : $issue->branchcode;
$issue->checkin_library($checkin_library);
# Create the old_issues entry
my $old_checkout = Koha::Old::Checkout->new( $issue->unblessed )->store;
@ -2963,7 +3050,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( { issue_branchcode => $issue_branchcode } )
? !$patron->has_restricting_overdues()
: !$patron->has_overdues;
if ( $remove_restrictions && $overdue_restrictions->count ) {
DelUniqueDebarment( { borrowernumber => $borrowernumber, type => 'OVERDUES' } );
@ -3032,7 +3119,7 @@ within the grace period).
sub _calculate_new_debar_dt {
my ( $patron, $item, $dt_due, $return_date ) = @_;
my $branchcode = _GetCircControlBranch( $item, $patron );
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
my $circcontrol = C4::Context->preference('CircControl');
my $issuing_rule = Koha::CirculationRules->get_effective_rules(
{
@ -3095,7 +3182,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::Calendar->new(
my $calendar = Koha::Library::Calendar->new(
branchcode => $branchcode,
days_mode => 'Calendar'
);
@ -3219,44 +3306,6 @@ 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 } );
@ -3288,7 +3337,7 @@ sub GetUpcomingDueIssues {
=head2 CanBookBeRenewed
($ok,$error,$info) = &CanBookBeRenewed($patron, $issue, $override_limit, $cron);
($ok,$error,$info) = &CanBookBeRenewed($patron, $issue, $override_limit, $cron, $date_due);
Find out whether a borrowed item may be renewed.
@ -3305,6 +3354,11 @@ 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
@ -3316,7 +3370,7 @@ already renewed the loan.
=cut
sub CanBookBeRenewed {
my ( $patron, $issue, $override_limit, $cron ) = @_;
my ( $patron, $issue, $override_limit, $cron, $date_due ) = @_;
my $auto_renew = "no";
my $soonest;
@ -3332,7 +3386,7 @@ sub CanBookBeRenewed {
my $final_unseen_renewal = 0;
# override_limit will override anything else except on_reserve
my $branchcode = _GetCircControlBranch( $item, $patron );
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
unless ($override_limit) {
( $auto_renew, $soonest ) = _CanBookBeAutoRenewed(
@ -3402,7 +3456,7 @@ sub CanBookBeRenewed {
}
if ( C4::Context->preference('UseRecalls') ) {
if ( C4::Context->preference('UseRecalls') ne "off" ) {
my $recall = undef;
$recall = $item->check_recalls if $item->can_be_waiting_recall;
if ( defined $recall ) {
@ -3481,7 +3535,10 @@ sub CanBookBeRenewed {
( C4::Context->preference('ManualRenewalPeriodBase') eq 'date_due' )
? dt_from_string( $issue->date_due, 'sql' )
: dt_from_string();
my $datedue = CalcDateDue( $startdate, $item->effective_itemtype, $branchcode, $patron, 'is a renewal' );
my $datedue =
$date_due
? dt_from_string($date_due)
: 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 }
@ -3489,6 +3546,19 @@ 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,
}
);
}
}
@ -3497,17 +3567,13 @@ 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;
}
}
$soonest = GetSoonestRenewDate( $patron, $issue );
if ( $soonest > dt_from_string() ) {
return ( 0, "too_soon", { soonest_renew_date => $soonest } ) 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;
}
}
my $auto_renew_code =
@ -3605,7 +3671,8 @@ 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( _GetCircControlBranch( $item_object, $patron ) );
my $circ_library =
Koha::Libraries->find( Koha::Policy::Circulation->circ_control_library( $item_object, $patron ) );
my $schema = Koha::Database->schema;
$schema->txn_do(
@ -3683,6 +3750,36 @@ 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);
@ -3750,7 +3847,7 @@ sub AddRenewal {
)->store();
# Add the renewal to stats
C4::Stats::UpdateStats(
Koha::Statistic->new(
{
branch => $item_object->renewal_branchcode( { branch => $branch } ),
type => 'renew',
@ -3763,7 +3860,7 @@ sub AddRenewal {
categorycode => $patron->categorycode,
interface => C4::Context->interface,
}
);
)->store();
#Update borrowers.lastseen
try {
@ -3850,7 +3947,7 @@ sub GetRenewCount {
$unseencount = $data->{'unseen_renewals'} if $data->{'unseen_renewals'};
# $item and $borrower should be calculated
my $branchcode = _GetCircControlBranch( $item, $patron );
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
my $rules = Koha::CirculationRules->get_effective_rules(
{
@ -3911,7 +4008,7 @@ sub GetSoonestRenewDate {
my $dbh = C4::Context->dbh;
my $branchcode = _GetCircControlBranch( $item, $patron );
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
my $issuing_rule = Koha::CirculationRules->get_effective_rules(
{
categorycode => $patron->categorycode,
@ -3979,7 +4076,7 @@ sub GetLatestAutoRenewDate {
return unless $item;
my $dbh = C4::Context->dbh;
my $branchcode = _GetCircControlBranch( $item, $patron );
my $branchcode = Koha::Policy::Circulation->circ_control_library( $item, $patron );
my $circulation_rules = Koha::CirculationRules->get_effective_rules(
{
@ -4054,7 +4151,7 @@ sub GetIssuingCharges {
my $patron;
if ( my $item_data = $sth->fetchrow_hashref ) {
$item_type = $item_data->{itemtype};
$charge = $item_data->{rentalcharge};
$charge = $item_data->{rentalcharge} || 0;
if ($charge) {
# FIXME This should follow CircControl
@ -4387,7 +4484,7 @@ sub CalcDateDue {
} else { # days
$dur = DateTime::Duration->new( days => $loanlength->{$length_key} );
}
my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
my $calendar = Koha::Library::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);
@ -4425,7 +4522,7 @@ sub CalcDateDue {
}
}
if ( $daysmode ne 'Days' ) {
my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
my $calendar = Koha::Library::Calendar->new( branchcode => $branch, days_mode => $daysmode );
if ( $calendar->is_holiday($datedue) ) {
# Don't return on a closed day
@ -4905,54 +5002,6 @@ 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

View File

@ -19,6 +19,9 @@ 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
@ -41,13 +44,62 @@ sub split_callnumber {
my ( $cn_item, $regexs ) = @_;
for my $regex (@$regexs) {
eval "\$cn_item =~ $regex"; ## no critic (StringyEval)
_apply_substitution( \$cn_item, $regex );
}
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

View File

@ -52,6 +52,10 @@ use Koha::Config::SysPrefs;
use Koha::Config;
use Koha::Database;
# NOTE: The below list should match the corresponding options in the .pref file
# for CSVDelimiter
use constant ALLOWED_CSV_DELIMITERS => ( ';', 'tabulation', ',', '/', '\\', '#' );
=head1 NAME
C4::Context - Maintain and manipulate the context of a Koha script
@ -485,6 +489,13 @@ sub delete_preference {
sub csv_delimiter {
my ( $self, $value ) = @_;
# Validate input against allowed delimiters
if ($value) {
my %allowed = map { $_ => 1 } ALLOWED_CSV_DELIMITERS;
$value = undef unless $allowed{$value};
}
my $delimiter = $value || $self->preference('CSVDelimiter') || ',';
$delimiter = "\t" if $delimiter eq 'tabulation';
return $delimiter;
@ -972,6 +983,26 @@ 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

View File

@ -272,7 +272,7 @@ ADD_LAYOUTS:
This function returns an arrayref whose elements are hashes containing all profiles upon success and 1 upon failure. Errors are logged
to the Apache log. Two parameters are accepted. The first limits the field(s) returned. This parameter should be string of comma separated
fields. ie. "field_1, field_2, ...field_n" The second limits the records returned based on a string containing a valud SQL 'WHERE' filter.
fields. ie. "field_1, field_2, ...field_n" The second limits the records returned based on a string containing a valid SQL 'WHERE' filter.
NOTE: Do not pass in the keyword 'WHERE.'
@ -320,7 +320,7 @@ sub get_all_image_names {
This function returns an arrayref whose elements are hashes containing the batch_ids of current batches along with the item count
for each batch upon success and 1 upon failure. Item counts are stored under the key '_item_count' Errors are logged to the Apache log.
One parameter is accepted which limits the records returned based on a string containing a valud SQL 'WHERE' filter.
One parameter is accepted which limits the records returned based on a string containing a valid SQL 'WHERE' filter.
NOTE: Do not pass in the keyword 'WHERE.'
@ -352,7 +352,7 @@ sub get_batch_summary {
This function returns an arrayref whose elements are hashes containing the label_ids of current labels along with the item count
for each label upon success and 1 upon failure. Item counts are stored under the key '_item_count' Errors are logged to the Apache log.
One parameter is accepted which limits the records returned based on a string containing a valud SQL 'WHERE' filter.
One parameter is accepted which limits the records returned based on a string containing a valid SQL 'WHERE' filter.
NOTE: Do not pass in the keyword 'WHERE.'
@ -405,7 +405,7 @@ sub get_label_summary {
This function returns an arrayref whose elements are hashes containing the label_ids of current cards along with the item count
for each card upon success and 1 upon failure. Item counts are stored under the key '_item_count' Errors are logged to the Apache log.
One parameter is accepted which limits the records returned based on a string containing a valud SQL 'WHERE' filter.
One parameter is accepted which limits the records returned based on a string containing a valid SQL 'WHERE' filter.
NOTE: Do not pass in the keyword 'WHERE.'

80
C4/External/Amazon.pm vendored
View File

@ -1,80 +0,0 @@
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

View File

@ -476,7 +476,6 @@ 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
@ -493,6 +492,7 @@ 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,20 +668,32 @@ 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) && $unallocated[$u] == $j ) {
if ( $u < scalar(@unallocated_sorted) && $unallocated_sorted[$u] == $j ) {
$u++;
} elsif ( $u > 0 ) {
$m[$i][ $j - $u ] = $m[$i][$j];
}
}
}
for ( my $u = 0 ; $u < scalar(@unallocated) ; $u++ ) {
splice @requests, $unallocated[$u], 1;
# 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;
}
$num_tasks = scalar(@requests);
@ -713,7 +725,8 @@ 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 we check all requests to see if local matches can be found
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
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)
@ -748,8 +761,9 @@ sub MapItemsToHoldRequests {
# Look for Local Holds Priority matches first
my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
if ( $LocalHoldsPriority ne 'None' ) {
my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
my $LocalHoldsPriorityScope = C4::Context->preference('LocalHoldsPriorityScope');
if ( $LocalHoldsPriority ne 'None' && $LocalHoldsPriorityScope eq 'capture_and_queue' ) {
my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
foreach my $request (@$hold_requests) {
@ -897,6 +911,7 @@ sub MapItemsToHoldRequests {
$item_map{ $allocation->[0] } = $allocation->[1];
$num_items_remaining--;
}
_flag_local_holdgroup_matches( \%item_map, \%items_by_itemnumber );
return \%item_map;
}
@ -908,7 +923,10 @@ sub MapItemsToHoldRequests {
push @{ $items_by_branch{ $item->{holdingbranch} } }, $item
unless exists $allocated_items{ $item->{itemnumber} };
}
return \%item_map unless keys %items_by_branch;
unless ( keys %items_by_branch ) {
_flag_local_holdgroup_matches( \%item_map, \%items_by_itemnumber );
return \%item_map;
}
# now handle the title-level requests
$num_items_remaining = scalar(@$available_items) - scalar( keys %allocated_items );
@ -1020,9 +1038,63 @@ 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 );
@ -1074,19 +1146,20 @@ 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)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
holdingbranch,pickbranch,notes, hold_group_id, item_level_request, local_holdgroup_match)
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 $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 $item = Koha::Items->find($itemnumber);
my $barcode = $item->barcode;
@ -1111,7 +1184,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
$holdingbranch, $pickbranch, $reservenotes, $hold_group_id, $item_level, $local_holdgroup_match
);
}
}
@ -1127,7 +1200,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) VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO hold_fill_targets (borrowernumber, biblionumber, itemnumber, source_branchcode, item_level_request, reserve_id, local_holdgroup_match) VALUES (?, ?, ?, ?, ?, ?, ?)
);
my $sth_insert = $dbh->prepare($insert_sql);
@ -1138,7 +1211,8 @@ 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->{holdingbranch}, $mapped_item->{item_level}, $mapped_item->{reserve_id},
$mapped_item->{local_holdgroup_match} ? 1 : 0,
);
}
);
@ -1185,7 +1259,8 @@ sub load_branches_to_pull_from {
my $today = dt_from_string();
if ( C4::Context->preference('HoldsQueueSkipClosed') ) {
@branches_to_use = grep { !Koha::Calendar->new( branchcode => $_ )->is_holiday($today) } @branches_to_use;
@branches_to_use =
grep { !Koha::Library::Calendar->new( branchcode => $_ )->is_holiday($today) } @branches_to_use;
}
return \@branches_to_use;

View File

@ -38,6 +38,7 @@ BEGIN {
SearchItems
PrepareItemrecordDisplay
ToggleNewStatus
GetAutomaticItemModificationAgeFields
);
}
@ -58,6 +59,7 @@ 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;
@ -1655,6 +1657,25 @@ 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.
@ -1671,17 +1692,32 @@ 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
my $agefield = $rule->{agefield} ? $rule->{agefield} : 'items.dateaccessioned';
# 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';
}
my $conditions = $rule->{conditions};
my $substitutions = $rule->{substitutions};
foreach (@$substitutions) {
( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
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;
}
}
my @params;
@ -1706,6 +1742,9 @@ 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 ) {
@ -1722,6 +1761,7 @@ 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} );

View File

@ -33,10 +33,7 @@ BEGIN {
getitemtypeimagesrc
getitemtypeimagelocation
GetAuthorisedValues
GetNormalizedUPC
GetNormalizedISBN
GetNormalizedEAN
GetNormalizedOCLCNumber
xml_escape
GetVariationsOfISBN
@ -508,37 +505,51 @@ 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";
my $cache_key = "AuthorisedValues-$category-$opac-$branch_limit-$no_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 $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 $query = qq{
SELECT DISTINCT av.*
$select_clause
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) {
if ( $branch_limit && !$no_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
@ -601,39 +612,6 @@ 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.
@ -674,64 +652,6 @@ 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]*)/;
@ -742,13 +662,28 @@ sub _normalize_match_point {
sub _isbn_cleanup {
my ($isbn) = @_;
return NormalizeISBN(
{
isbn => $isbn,
format => 'ISBN-10',
strip_hyphens => 1,
}
) if $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,
}
);
}
}
=head2 NormalizeISBN

View File

@ -34,7 +34,7 @@ BEGIN {
use Carp qw( carp );
use CGI;
use List::MoreUtils qw( any );
use List::MoreUtils qw( any uniq );
use C4::Context;
use Koha::Caches;
use Koha::Cache::Memory::Lite;
@ -123,6 +123,10 @@ 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;
@ -152,9 +156,7 @@ sub getTranslatedLanguages {
$htdocs = C4::Context->config('opachtdocs');
push @languages, _get_opac_language_dirs($htdocs);
my %seen;
$seen{$_}++ for @languages;
@languages = keys %seen;
@languages = uniq @languages;
}
return _build_languages_arrayref( \@languages, $current_language, \@enabled_languages );
}
@ -369,14 +371,16 @@ 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
@ -443,9 +447,15 @@ sub _build_languages_arrayref {
}
}
}
( exists $idx{$aa} and exists $idx{$bb} and ( $idx{$aa} cmp $idx{$bb} ) )
|| ( exists $idx{$aa} and exists $idx{$bb} )
|| exists $idx{$bb}
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
}
} keys %$language_groups;
for my $key (@ordered_keys) {

View File

@ -1032,6 +1032,7 @@ 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,
@ -1044,6 +1045,10 @@ 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.
@ -1058,6 +1063,18 @@ 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(
{
@ -1084,10 +1101,17 @@ sub SendQueuedMessages {
$params->{message_id} ? ( message_id => $params->{message_id} ) : (),
$params->{borrowernumber} ? ( borrowernumber => $params->{borrowernumber} ) : (),
# 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} ) : (),
# 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} }
)
: (),
ref( $params->{type} )
&& @{ $params->{type} }
? ( message_transport_type => $params->{type} )

View File

@ -38,7 +38,7 @@ use vars qw(@ISA @EXPORT);
BEGIN {
require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(logaction cronlogaction);
@EXPORT = qw(logaction cronlogaction logscriptaction);
}
=head1 NAME
@ -215,17 +215,41 @@ 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";
my $loginfo = ( caller(0) )[1];
# $0 is a special Perl variable containing the program that is currently running
# https://perldoc.perl.org/variables/$0
my $loginfo = $0;
$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__

View File

@ -640,7 +640,7 @@ sub ModifyRecordWithTemplate {
{
record => $record,
field => $from_field,
subfield => $from_subfield,
subfield => $from_field < 10 ? undef : $from_subfield,
}
);
}

View File

@ -94,10 +94,6 @@ 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
@ -108,10 +104,6 @@ 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,
@ -203,14 +195,6 @@ 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 );
}

View File

@ -438,8 +438,7 @@ sub parametrized_url {
=item redirect_if_opac_suppressed
redirect_if_opac_suppressed( $query, $biblio )
if C4::Context->preference('OpacSuppression');
redirect_if_opac_suppressed( $query, $biblio );
For a given I<Koha::Biblio> object, it handles redirection if it is suppressed
from the OPAC.
@ -456,20 +455,9 @@ sub redirect_if_opac_suppressed {
} else {
$redirect_url = "/cgi-bin/koha/errors/404.pl";
}
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();
}
if ( C4::Context->is_opac_suppressed && $biblio->opac_suppressed() ) {
print $query->redirect($redirect_url);
C4::Auth::safe_exit();
}
}

View File

@ -32,14 +32,11 @@ 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 );
@ -351,7 +348,7 @@ sub get_chargeable_units {
if ( $unit eq 'hours' ) {
if ( C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed' ) {
my $calendar = Koha::Calendar->new( branchcode => $branchcode );
my $calendar = Koha::Library::Calendar->new( branchcode => $branchcode );
$charge_duration = $calendar->hours_between( $date_due, $date_returned );
} else {
$charge_duration = $date_returned->delta_ms($date_due);
@ -362,7 +359,7 @@ sub get_chargeable_units {
return $charge_duration->in_units('hours');
} else { # days
if ( C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed' ) {
my $calendar = Koha::Calendar->new( branchcode => $branchcode );
my $calendar = Koha::Library::Calendar->new( branchcode => $branchcode );
$charge_duration = $calendar->days_between( $date_due, $date_returned );
} else {
$charge_duration = $date_returned->delta_days($date_due);
@ -371,131 +368,6 @@ 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);
@ -720,21 +592,19 @@ returns a list of branch codes for branches with overdue rules defined.
=cut
sub GetBranchcodesWithOverdueRules {
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 '' ) {
# If a default rule exists, all branches should be returned
return Koha::Libraries->search( {}, { order_by => 'branchname' } )->get_column('branchcode');
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');
}
return @$branchcodes;
return @branchcodes;
}
=head2 GetOverduesForBranch
@ -801,39 +671,6 @@ 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

View File

@ -919,9 +919,11 @@ sub marc2cites {
$author = $field->subfield("a");
}
if ( $author =~ /([^,]+),?(.*)/ ) {
my $surname = $1;
my $forenames = $2;
my %a;
( $a{'surname'} = $1 ) =~ s/$re_clean//g;
$a{'forenames'} = [ map { my $t = $_; $t =~ s/$re_clean//g; $t } split ' ', $2 ];
( $a{'surname'} = $surname ) =~ s/$re_clean//g;
$a{'forenames'} = [ map { my $t = $_; $t =~ s/$re_clean//g; $t } split ' ', $forenames ];
push( @authors, \%a );
}
}

View File

@ -51,8 +51,8 @@ This will return a list of all the available delimiters.
sub GetDelimiterChoices {
my $default = C4::Context->preference('CSVDelimiter');
# NOTE: The below list should match the corresponding options in the .pref file
my @dels = ( ';', 'tabulation', ',', '/', '\\', '#' );
# Use the centralized list of allowed delimiters from C4::Context
my @dels = C4::Context::ALLOWED_CSV_DELIMITERS;
return {
values => \@dels,

View File

@ -58,6 +58,7 @@ use Koha::Report;
use Koha::Reports;
use Koha::SharedContent;
use Koha::TemplateUtils qw( process_tt );
use C4::Letters qw( GetPreparedLetter );
=head1 NAME
@ -880,7 +881,54 @@ sub get_saved_reports {
my $result = $dbh->selectall_arrayref( $query, { Slice => {} }, @args );
return $result;
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;
}
=head2 get_column_type($column)
@ -1170,8 +1218,7 @@ sub EmailReport {
my ( $sth, $errors ) = execute_query( { sql => $sql, report_id => $report_id } );
return ( undef, [ { FATAL => "REPORT_FAIL" } ] ) if $errors;
my $counter = 1;
my $template = $letter->{content};
my $counter = 1;
while ( my $row = $sth->fetchrow_hashref() ) {
my $email;
@ -1184,13 +1231,16 @@ sub EmailReport {
my $from_address = $from || $row->{from};
my $to_address = $row->{$email_col};
push( @errors, { NOT_PARSE => $counter } ) unless my $content = process_tt( $template, $row );
my $letter = {%$letter}; # GetPreparedLetter mutates, so make a local copy beforehand
push( @errors, { NOT_PARSE => $counter } ) unless my $rendered = GetPreparedLetter(
letter => $letter,
objects => $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} = {%$letter};
$email->{letter} = $rendered;
$email->{from_address} = $from_address;
$email->{to_address} = $to_address;

View File

@ -1,7 +1,7 @@
package C4::Reserves;
# Copyright 2000-2002 Katipo Communications
# 2006 SAN Ouest Provence
# 2006 SAN Ouest Provence # codespell:ignore
# 2007-2010 BibLibre Paul POULAIN
# 2011 Catalyst IT
#
@ -66,7 +66,8 @@ use JSON qw( to_json );
use C4::Accounts;
use C4::Biblio qw( GetMarcFromKohaField );
use C4::Circulation qw( CheckIfIssuedToPatron GetAgeRestriction GetBranchItemRule );
use C4::Circulation qw( CheckIfIssuedToPatron GetBranchItemRule );
use Koha::Policy::Biblio::AgeRestriction;
use C4::Context;
use C4::Items qw( CartToShelf get_hostitemnumbers_of );
use C4::Letters;
@ -76,7 +77,7 @@ use C4::Members;
use Koha::Account::Lines;
use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
use Koha::Biblios;
use Koha::Calendar;
use Koha::Library::Calendar;
use Koha::Cache::Memory::Lite;
use Koha::CirculationRules;
use Koha::Database;
@ -110,10 +111,10 @@ This modules provides some functions to deal with reservations.
- priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
=0 : then the reserve is being dealt
- found : NULL : means the patron requested the 1st available, and we haven't chosen the item
T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
F(inished) : the reserve has been completed, and is done
P(rocessing) : reserved item has been returned using self-check machine and reserve needs to be confirmed
T (Transit) : the reserve is linked to an item but is in transit to the pickup branch
W (Waiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
F (Finished) : the reserve has been completed, and is done
P (Processing) : reserved item has been returned using self-check machine and reserve needs to be confirmed
by librarian before notice is send and status changed to waiting.
Applicable only if HoldsNeedProcessingSIP system preference is set.
- itemnumber : empty : the reserve is still unaffected to an item
@ -271,7 +272,6 @@ sub AddReserve {
# Log the hold creation
if ( C4::Context->preference('HoldsLog') ) {
my $info = to_json(
{
hold => $hold->id,
@ -289,12 +289,11 @@ 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 );
}
@ -539,9 +538,9 @@ sub CanItemBeReserved {
my $biblio = $item->biblio;
# Check for the age restriction
my $ageRestriction = C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction );
my $age_check = Koha::Policy::Biblio::AgeRestriction->check( $biblio, $patron );
return _cache { status => 'ageRestricted' }
if $ageRestriction && $patron->dateofbirth && $ageRestriction > $patron->get_age();
if !$age_check;
}
# Check that the patron doesn't have an item level hold on this item already
@ -897,6 +896,54 @@ 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
@ -1042,7 +1089,7 @@ sub CancelExpiredReserves {
my $cache_key = sprintf "Calendar_CancelExpiredReserves:%s", $hold->branchcode;
my $calendar = $cache->get_from_cache($cache_key);
if ( !$calendar ) {
$calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
$calendar = Koha::Library::Calendar->new( branchcode => $hold->branchcode );
$cache->set_in_cache( $cache_key, $calendar );
}
@ -1136,6 +1183,8 @@ 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
@ -1193,6 +1242,43 @@ 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

View File

@ -106,7 +106,10 @@ sub new {
my $it = $item->effective_itemtype;
$self->{itemtype} = $it;
my $itemtype = Koha::Database->new()->schema()->resultset('Itemtype')->find($it);
$self->{sip_media_type} = $itemtype->sip_media_type() if $itemtype;
if ($itemtype) {
$self->{sip_media_type} = $itemtype->sip_media_type;
$self->{magnetic_media} = $itemtype->sip_magnetic;
}
# check if its on issue and if so get the borrower
my $issue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );

View File

@ -138,6 +138,10 @@ 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 = (
@ -168,7 +172,7 @@ sub new {
screen_msg => 'Greetings from Koha. ' . $kp->{opacnote} . $fines_msg,
print_line => '',
items => [],
hold_items => $flags->{WAITING}->{itemlist},
hold_items => [ map { $_->unblessed } @holds ],
overdue_items => $flags->{ODUES}->{itemlist},
too_many_overdue => $circ_blocked,
fine_items => [],
@ -223,7 +227,7 @@ sub new {
}
# FIXME: populate recall_items
$ilspatron{unavail_holds} = _get_outstanding_holds( $kp->{borrowernumber} );
$ilspatron{unavail_holds} = _get_outstanding_holds( \@holds );
my $pending_checkouts = $patron->pending_checkouts;
my @barcodes;
@ -695,12 +699,12 @@ sub _get_address {
}
sub _get_outstanding_holds {
my $borrowernumber = shift;
my $holds = shift;
my $patron = Koha::Patrons->find($borrowernumber);
my $holds = $patron->holds->search( { -or => [ { found => undef }, { found => { '!=' => 'W' } } ] } );
my @holds;
while ( my $hold = $holds->next ) {
foreach my $hold ( @{$holds} ) {
next if ( $hold->found // '' ) eq 'W';
my $item;
if ( $hold->itemnumber ) {
$item = $hold->item;
@ -921,9 +925,6 @@ __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

View File

@ -124,6 +124,7 @@ 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};

View File

@ -55,6 +55,10 @@ 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;
#
@ -234,13 +238,15 @@ 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_WARNING", "Couldn't find sip2_resource_last_modified, considering configuration not up to date" );
siplog( "LOG_INFO", "Couldn't find sip2_resource_last_modified, considering configuration not up to date" );
return 0;
}
return $sip2_config_read_timestamp >= $sip2_resource_last_modified;
return 0 unless defined $sip2_config_read_timestamp;
return $sip2_config_read_timestamp >= $sip2_resource_last_modified ? 1 : 0;
}
#
@ -260,15 +266,17 @@ 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
@ -468,12 +476,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) );
# Check if user is authorized for SIP access, then authenticate via login_core
if ( exists( $config->{accounts}->{$uid} ) ) {
# 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 ) )
{
$account = $config->{accounts}->{$uid};
if ( C4::SIP::Sip::MsgType::login_core( $self, $uid, $pwd ) ) {
last;
}
last;
}
siplog( "LOG_WARNING", "Invalid login attempt: '%s'", ( $uid || '' ) );
print("Invalid login$CRLF");

View File

@ -97,6 +97,7 @@ 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
@ -140,7 +141,7 @@ sub maybe_add {
}
return ( defined($value) && length($value) )
? add_field( $fid, $value )
? add_field( $fid, $value, $server )
: '';
}

View File

@ -41,17 +41,13 @@ my $parser = XML::Simple->new(
sub get_configuration {
my ( $class, $config_file, $current_config ) = @_;
my $cache = Koha::Caches->get_instance();
my $sip2_resource_last_modified = $cache->get_from_cache("sip2_resource_last_modified");
my $cache = Koha::Caches->get_instance();
if ($sip2_resource_last_modified) {
$cache->set_in_cache( 'sip2_config_read_timestamp', $sip2_resource_last_modified );
} else {
unless ( $cache->get_from_cache("sip2_resource_last_modified") ) {
# 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 );
# 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 );
}
my $cfg;

View File

@ -24,7 +24,8 @@ use Koha::Patrons;
use Koha::Patron::Attributes;
use Koha::Plugins;
use Koha::Items;
use Koha::DateUtils qw( output_pref );
use Koha::DateUtils qw( output_pref );
use Koha::TemplateUtils qw( process_tt );
use UNIVERSAL::can;
@ -625,10 +626,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, $item->title_id, $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, _format_title( { item => $item, server => $server } ), $server );
if ( $item->due_date ) {
my $due_date =
$account->{format_due_date}
@ -663,7 +664,7 @@ sub handle_checkout {
# If the item is valid, provide the title, otherwise
# leave it blank
$resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : '', $server );
$resp .= add_field( FID_TITLE_ID, $item ? _format_title( { item => $item, server => $server } ) : "", $server );
# Due date is required. Since it didn't get checked out,
# it's not due, so leave the date blank
@ -756,14 +757,14 @@ sub handle_checkin {
$resp .= 'U';
}
$resp .= $status->alert ? 'Y' : 'N';
$resp .= ( $status->alert || $item->magnetic_media ) ? '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, $item->title_id, $server );
$resp .= maybe_add( FID_TITLE_ID, _format_title( { item => $item, server => $server } ), $server );
$resp .= $item->build_additional_item_fields_string($server);
} else {
$resp .= add_field( FID_PERM_LOCN, "", $server );
@ -932,19 +933,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;
}
# 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 before storing the account on the session
my $inst = $server->{config}->{accounts}->{$uid}->{institution};
# Authenticate using Koha's internal authentication (checks hashed password in borrowers table)
my $auth_status = api_auth( $uid, $pwd, $inst );
@ -956,6 +957,13 @@ 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 );
#
@ -1336,8 +1344,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, $item->title_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 .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type, $server );
$resp .= maybe_add( FID_PERM_LOCN, $item->permanent_location, $server );
@ -1422,8 +1430,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, $item->title_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 .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
}
@ -1544,10 +1552,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, $status->item->title_id, $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 );
} else {
# Not ok. still need required fields
@ -1611,9 +1619,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, $item->title_id, $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 );
if ( $item->due_date ) {
$resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ), $server );
} else {
@ -1634,10 +1642,13 @@ 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 ? $item->title_id : $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 ? _format_title( { item => $item, server => $server } ) : $title_id,
$server
);
$resp .= add_field( FID_DUE_DATE, '', $server );
}
if ( $status->fee_amount ) {
@ -1893,6 +1904,19 @@ 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__

View File

@ -35,8 +35,8 @@ BEGIN {
use C4::Context;
use C4::Biblio qw( TransformMarcToKoha GetMarcFromKohaField GetFrameworkCode GetAuthorisedValueDesc GetBiblioData );
use C4::Koha
qw( getFacets GetVariationsOfISBN GetNormalizedUPC GetNormalizedEAN GetNormalizedOCLCNumber GetNormalizedISBN getitemtypeimagelocation );
use C4::Koha qw( getFacets GetVariationsOfISBN GetNormalizedISBN getitemtypeimagelocation );
use Koha::Biblio::Metadata::Extractor;
use Koha::DateUtils;
use Koha::Libraries;
use Koha::SearchEngine::QueryBuilder;
@ -1768,9 +1768,10 @@ sub searchResults {
my $oldbiblio = TransformMarcToKoha( { record => $marcrecord, limit_table => 'no_items' } );
$oldbiblio->{result_number} = $i + 1;
$oldbiblio->{normalized_upc} = GetNormalizedUPC( $marcrecord, $marcflavour );
$oldbiblio->{normalized_ean} = GetNormalizedEAN( $marcrecord, $marcflavour );
$oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber( $marcrecord, $marcflavour );
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_isbn} = GetNormalizedISBN( $oldbiblio->{isbn}, $marcrecord, $marcflavour )
; # Use existing ISBN from record if we got one
$oldbiblio->{content_identifier_exists} = 1
@ -1979,7 +1980,7 @@ sub searchResults {
)
: ( '', '', '' );
$reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber} );
if ( C4::Context->preference('UseRecalls') ) {
if ( C4::Context->preference('UseRecalls') ne "off" ) {
if ( Koha::Recalls->search( { item_id => $item->{itemnumber}, status => 'waiting' } )->count ) {
$recallstatus = 'Waiting';
}

View File

@ -28,7 +28,7 @@ BEGIN {
use C4::Biblio qw( GetAuthorisedValueDesc );
use C4::Context;
use C4::Koha qw( GetNormalizedUPC GetNormalizedOCLCNumber GetNormalizedISBN GetNormalizedEAN );
use C4::Koha qw( GetNormalizedISBN );
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'} = GetNormalizedUPC( $this_record, $marcflavour );
$item->{'browser_normalized_oclc'} = GetNormalizedOCLCNumber( $this_record, $marcflavour );
$item->{'browser_normalized_upc'} = $biblio->normalized_upc;
$item->{'browser_normalized_oclc'} = $biblio->normalized_oclc;
$item->{'browser_normalized_isbn'} = GetNormalizedISBN( undef, $this_record, $marcflavour );
$item->{'browser_normalized_ean'} = GetNormalizedEAN( $this_record, $marcflavour );
$item->{'browser_normalized_ean'} = $biblio->normalized_ean;
push @valid_items, $item;
}
return @valid_items;

View File

@ -110,7 +110,7 @@ sub update_data {
$i++;
}
}
say "$i data insered or updated";
say "$i data inserted or updated";
}
=head2 get_report

View File

@ -23,12 +23,10 @@ use base 'Exporter';
BEGIN {
our @EXPORT = qw(
ConnectSuggestionAndBiblio
DelSuggestion
GetSuggestion
ModStatus
ModSuggestion
DelSuggestionsOlderThan
GetUnprocessedSuggestions
MarcRecordFromNewSuggestion
);
@ -108,7 +106,7 @@ sub ModSuggestion {
my $transport =
( C4::Context->preference("FallbackToSMSIfNoEmail") )
&& ( $patron->smsalertnumber )
&& ( !$patron->email ) ? 'sms' : 'email';
&& ( !$patron->notice_email_address ) ? 'sms' : 'email';
if (
my $letter = C4::Letters::GetPreparedLetter(
@ -140,26 +138,6 @@ 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)
@ -199,28 +177,6 @@ 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.

View File

@ -22,6 +22,8 @@ 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
@ -54,7 +56,6 @@ BEGIN {
use Carp qw( carp );
use Module::Load::Conditional qw( check_install );
use Koha::Tags;
use Koha::Tags::Approvals;
use Koha::Tags::Indexes;

View File

@ -241,6 +241,7 @@ sub _shared_preferences {
AmazonCoverImages
OPACAmazonCoverImages
Babeltheque
EnableZotero
GoogleJackets
HTML5MediaEnabled
LibraryThingForLibrariesEnabled
@ -320,6 +321,7 @@ sub _shared_preferences {
autoMemberNum
BorrowerRenewalPeriodBase
EnableBorrowerFiles
LimitReportsByLibrary
EnhancedMessagingPreferences
ExtendedPatronAttributes
intranetreadinghistory
@ -357,6 +359,7 @@ sub _shared_preferences {
ILS-DI
OAI-PMH
version
UseRecalls
/;
return \@preferences;

View File

@ -16,7 +16,7 @@ sub search {
my $start = $params->{start};
my $length = $params->{length};
# If not logged in user, be carreful and set the borrowernumber to 0
# If not logged in user, be careful and set the borrowernumber to 0
# to prevent private lists lack
my $loggedinuser = C4::Context->userenv->{'number'} || 0;

View File

@ -385,7 +385,7 @@ sub buildKohaItemsNamespace {
my $substatus = '';
my $recalls_count;
if ( C4::Context->preference('UseRecalls') ) {
if ( C4::Context->preference('UseRecalls') ne "off" ) {
$recalls_count = Koha::Recalls->search( { item_id => $item->itemnumber, status => 'waiting' } )->count;
}

View File

@ -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.05.00.000";
$VERSION = "26.06.00.030";
sub version {
return $VERSION;

View File

@ -25,7 +25,6 @@ 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;
@ -38,6 +37,7 @@ 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();
C4::Stats::UpdateStats(
Koha::Statistic->new(
{
branch => $library_id,
type => lc($credit_type),
@ -294,7 +294,8 @@ sub add_credit {
borrowernumber => $self->{patron_id},
interface => $interface,
}
) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
)->store()
if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
Koha::Plugins->call(
'after_account_action',

View File

@ -46,8 +46,7 @@ Returns the list of baskets for the vendor
sub baskets {
my ($self) = @_;
my $baskets_rs = $self->_result->aqbaskets;
return Koha::Acquisition::Baskets->_new_from_dbic($baskets_rs);
return Koha::Acquisition::Baskets->_new_from_dbic( scalar $self->_result->baskets );
}
=head3 contacts
@ -112,8 +111,7 @@ Returns the list of subscriptions for the vendor
sub subscriptions {
my ($self) = @_;
my $rs = $self->_result->subscriptions;
return Koha::Subscriptions->_new_from_dbic($rs);
return Koha::Subscriptions->_new_from_dbic( scalar $self->_result->subscriptions );
}
=head3 aliases
@ -198,8 +196,7 @@ Returns the list of invoices for the vendor
sub invoices {
my ($self) = @_;
my $invoices_rs = $self->_result->aqinvoices;
return Koha::Acquisition::Invoices->_new_from_dbic($invoices_rs);
return Koha::Acquisition::Invoices->_new_from_dbic( scalar $self->_result->invoices );
}
=head3 to_api_mapping

View File

@ -654,6 +654,7 @@ 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',

View File

@ -160,6 +160,7 @@ 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' => [
@ -169,7 +170,7 @@ sub filter_by_active {
},
{ 'orderstatus' => [ 'ordered', 'partial' ] }
],
quantityreceived => { '<', \['COALESCE(me.quantity,0)'] },
quantityreceived => { '<', \["COALESCE($source_alias.quantity,0)"] },
},
{ join => 'basket' }
);

View File

@ -19,12 +19,14 @@ 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 at call time,
# by which point it is reliably available.
# fully defined. The accessors below reach for Koha::Patron (and Koha::Items)
# at call time, by which point they are reliably available.
use base qw(Koha::Object);
@ -70,6 +72,42 @@ 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

View File

@ -95,19 +95,31 @@ 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';
$search_params->{-or} = [ { 'lang' => $lang }, '-and' => [ 'lang', 'default', \$subquery ] ]
if !$search_params->{lang};
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
]
]
}
];
}
my $attribs = {
prefetch => 'additional_content',
order_by => { -desc => 'additional_content.published_on' }
order_by => [
\['ISNULL(additional_content.number)'],
{ -asc => 'additional_content.number' },
{ -desc => 'additional_content.published_on' },
],
};
return Koha::AdditionalContentsLocalizations->search( $search_params, $attribs );
}

View File

@ -82,10 +82,7 @@ sub _get_data_and_patron {
my $code = $tx->res->code || 'No response';
return if $code ne '200';
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;
my $claim = $tx->res->json // Mojo::Parameters->new( $tx->res->body )->to_hash;
foreach my $key ( keys %$mapping ) {
my $pkey = $mapping->{$key};

View File

@ -21,6 +21,8 @@ use Modern::Perl;
use Scalar::Util qw( blessed );
use C4::Context;
use base qw(Koha::Object);
use Koha::Authority::ControlledIndicators;
@ -292,6 +294,112 @@ 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

View File

@ -455,6 +455,7 @@ 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',
};
}

View File

@ -70,6 +70,7 @@ 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;
@ -82,8 +83,9 @@ sub process {
{
biblio_id => $biblio_id,
branches_to_use => $branches_to_use,
delete => 1,
transport_cost_matrix => $transport_cost_matrix
delete => $unallocated ? 0 : 1,
transport_cost_matrix => $transport_cost_matrix,
unallocated => $unallocated,
}
);
push @messages,

View File

@ -104,7 +104,7 @@ sub process {
};
try {
my ($results) = Koha::Items->search( { itemnumber => \@record_ids } )->batch_update(
my ($results) = Koha::Items->search( { itemnumber => { -in => \@record_ids } } )->batch_update(
{
regex_mod => $regex_mod,
new_values => $new_values,

View File

@ -0,0 +1,138 @@
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;

View File

@ -18,6 +18,7 @@ package Koha::BackgroundJob::PseudonymizeStatistic;
use Modern::Perl;
use Koha::PseudonymizedTransactions;
use Koha::Statistics;
use base 'Koha::BackgroundJob';

View File

@ -53,7 +53,10 @@ sub process {
my $transport = Koha::File::Transports->find( $args->{transport_id} );
$transport->test_connection;
my $status = { status => 'ok' };
my $status = {
status => 'ok', transport_id => $transport->id, transport_name => $transport->name,
background_job_id => $self->id
};
my $messages = $transport->object_messages;
for my $message (@$messages) {
$status->{status} = 'errors' if $message->{type} eq 'error';

View File

@ -25,7 +25,7 @@ use URI;
use URI::Escape qw( uri_escape_utf8 );
use Try::Tiny;
use C4::Koha qw( GetNormalizedISBN GetNormalizedUPC GetNormalizedOCLCNumber );
use C4::Koha qw( GetNormalizedISBN );
use C4::Biblio qw( DelBiblio );
use C4::Serials qw( CountSubscriptionFromBiblionumber );
use C4::Reserves qw( MergeHolds );
@ -412,6 +412,14 @@ 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;
@ -799,7 +807,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->reserves->search( $params, $attributes );
my $hold_rs = $self->_result->holds->search( $params, $attributes );
return Koha::Holds->_new_from_dbic($hold_rs);
}
@ -1624,7 +1632,9 @@ sub get_marc_authors {
if ( $marcflavour eq "UNIMARC" ) {
$fields_filter = '200';
} else { # marc21/normarc
$fields_filter = '100';
$fields_filter = '1..';
$mintag = '100';
$maxtag = '111';
}
my @first_authors = @{
@ -1701,6 +1711,19 @@ 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
@ -2152,6 +2175,51 @@ 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;
@ -2171,12 +2239,14 @@ 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') );
return 0 if ( C4::Context->preference('UseRecalls') eq "off" );
my $patron = $params->{patron};
@ -2196,7 +2266,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 } ) ) {
if ( $item->can_be_recalled( { patron => $patron, hold_convert => $params->{hold_convert} } ) ) {
push( @itemtypes, $item->effective_itemtype );
push( @itemnumbers, $item->itemnumber );
push( @items, $item );

View File

@ -24,14 +24,19 @@ 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
@ -40,6 +45,188 @@ 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;
@ -255,6 +442,249 @@ 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

View File

@ -0,0 +1,70 @@
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;

View File

@ -0,0 +1,64 @@
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;

View File

@ -104,6 +104,29 @@ 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);

View File

@ -74,26 +74,27 @@ sub get_normalized_upc {
}
}
=head2 get_normalized_oclc
=head2 get_normalized_ean
my $normalized_oclc = $extractor->get_normalized_oclc();
my $normalized_ean = $extractor->get_normalized_ean();
Returns a normalized OCLC number.
Returns a normalized EAN from 024$a with indicator 1 = 3.
=cut
sub get_normalized_oclc {
sub get_normalized_ean {
my ($self) = @_;
my $record = $self->metadata;
my @fields = $record->field('035');
my @fields = $record->field('024');
foreach my $field (@fields) {
my $oclc = $field->subfield('a');
if ( $oclc && $oclc =~ /OCoLC/ ) {
$oclc =~ s/\(OCoLC\)//;
return $oclc;
my $indicator = $field->indicator(1);
my $ean = $self->_normalize_string( $field->subfield('a') );
if ( $ean && $indicator eq '3' ) {
return $ean;
}
}
return;
}
=head2 check_fixed_length

View File

@ -72,6 +72,28 @@ 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>

View File

@ -150,6 +150,13 @@ 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',
@ -173,12 +180,12 @@ sub store {
value => $self->biblio_id,
) unless ( $self->biblio );
# 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 ) {
# 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 ) {
# Throw exception for item level booking clash
Koha::Exceptions::Booking::Clash->throw()
@ -214,7 +221,6 @@ 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;
@ -344,17 +350,22 @@ sub to_api_mapping {
=head2 Internal methods
=head3 _is_final_status_transition
=head3 _skip_clash_detection
my $bool = $self->_is_final_status_transition;
my $bool = $self->_skip_clash_detection;
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.
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.
=cut
sub _is_final_status_transition {
sub _skip_clash_detection {
my ($self) = @_;
return 0 unless $self->in_storage;
@ -364,6 +375,14 @@ sub _is_final_status_transition {
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;
}

View File

@ -36,18 +36,14 @@ Koha::Bookings - Koha Booking object set class
$bookings->filter_by_active;
Will return the bookings that have not ended, were cancelled or are completed.
Returns bookings that are still active, i.e. awaiting collection (C<new>) or
currently on loan (C<issued>). Cancelled and completed bookings are excluded.
=cut
sub filter_by_active {
my ($self) = @_;
return $self->search(
{
end_date => { '>=' => \'NOW()' },
status => { '-not_in' => [ 'cancelled', 'completed' ] }
}
);
return $self->search( { status => { '-in' => [ 'new', 'issued' ] } } );
}
=head2 Internal Methods

View File

@ -45,9 +45,8 @@ use Module::Load::Conditional qw( can_load );
use Sereal::Encoder;
use Sereal::Decoder;
use C4::Context;
use Koha::Cache::Object;
use Koha::Config;
use Koha::Cache::Object;
use base qw(Class::Accessor);
@ -76,8 +75,8 @@ sub new {
# Should we continue to support MEMCACHED ENV vars?
$self->{'namespace'} ||= $ENV{MEMCACHED_NAMESPACE};
my @servers = split /,/, $ENV{MEMCACHED_SERVERS} || '';
$self->{namespace} ||= C4::Context->config('memcached_namespace') || 'koha';
@servers = split /,/, C4::Context->config('memcached_servers') // ''
$self->{namespace} ||= Koha::Config->get_instance->get('memcached_namespace') || 'koha';
@servers = split /,/, Koha::Config->get_instance->get('memcached_servers') // ''
unless @servers;
$self->{namespace} .= ":$subnamespace:";

View File

@ -16,12 +16,19 @@ 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);
@ -112,19 +119,14 @@ Return a set of accountlines linked to this cash register since the last cashup
sub outstanding_accountlines {
my ( $self, $conditions, $attrs ) = @_;
my $since = $self->_result->search_related(
'cash_register_actions',
{ 'code' => 'CASHUP' },
{
order_by => { '-desc' => [ 'timestamp', 'id' ] },
rows => 1
}
);
# Find the start timestamp for the current "open" session
my $start_timestamp = $self->_get_session_start_timestamp;
my $local_conditions =
$since->count
? { 'date' => { '>' => $since->get_column('timestamp')->as_query } }
$start_timestamp
? { 'date' => { '>' => $start_timestamp } }
: {};
my $merged_conditions =
$conditions
? { %{$conditions}, %{$local_conditions} }
@ -138,6 +140,40 @@ 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
@ -204,31 +240,380 @@ 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 => $cash_register->outstanding_accountlines->total
manager_id => $logged_in_user->id,
amount => $amount_removed_from_register,
[ reconciliation_note => $reconciliation_note ]
}
);
Add a new cashup action to the till, returns the added action.
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.
=cut
sub add_cashup {
my ( $self, $params ) = @_;
my $rs = $self->_result->add_to_cash_register_actions(
{
code => 'CASHUP',
manager_id => $params->{manager_id},
amount => $params->{amount}
# 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" );
}
)->discard_changes;
}
my $manager_id = $params->{manager_id};
return Koha::Cash::Register::Cashup->_new_from_dbic($rs);
# 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(
{},
{
order_by => { '-desc' => [ 'timestamp', 'id' ] },
rows => 1
}
)->single;
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;
}
=head3 to_api_mapping

View File

@ -23,7 +23,7 @@ use base qw(Koha::Cash::Register::Action);
=head1 NAME
Koha::Cash::Register::Actions - Koha Cash Register Action Object set class
Koha::Cash::Register::Cashup - Koha Cash Register Cashup Object class
=head1 API
@ -61,29 +61,43 @@ 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
}
);
my $previous = $prior_cashup->single;
# Get the session boundaries for this cashup
my ( $session_start, $session_end ) = $self->_get_session_boundaries;
my $conditions =
$previous
? { 'date' => { '-between' => [ $previous->_result->get_column('timestamp'), $self->timestamp ] } }
: { 'date' => { '<' => $self->timestamp } };
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 $payout_transactions = $self->register->accountlines->search(
{ %{$conditions}, credit_type_code => undef },
{
%{$conditions},
credit_type_code => undef,
debit_type_code => { '!=' => 'CASHUP_DEFICIT' }
},
);
my $income_transactions = $self->register->accountlines->search(
{ %{$conditions}, debit_type_code => undef },
{
%{$conditions},
debit_type_code => undef,
credit_type_code => { '!=' => 'CASHUP_SURPLUS' }
},
);
my $income_summary = Koha::Account::Offsets->search(
@ -170,23 +184,168 @@ 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 );
push @total_grouped, { payment_type => $type->lib, total => $typed_total };
# 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 };
}
# 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 => $previous ? $previous->timestamp : undef,
to_date => $self->timestamp,
from_date => $session_start,
to_date => $session_end,
income_grouped => \@income,
income_total => abs($income_total),
payout_grouped => \@payout,
payout_total => abs($payout_total),
total => $total * -1,
total_grouped => \@total_grouped
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
};
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

View File

@ -24,7 +24,7 @@ use base qw(Koha::Cash::Register::Actions);
=head1 NAME
Koha::Cash::Register::Actions - Koha Cash Register Action Object set class
Koha::Cash::Register::Cashups - Koha Cash Register Cashup Object set class
=head1 API

View File

@ -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::Calendar->new( branchcode => $self->library->id );
my $calendar = Koha::Library::Calendar->new( branchcode => $self->library->id );
if ( $units eq 'hours' ) {
if ( $itemtype->rentalcharge_hourly_calendar ) {

View File

@ -55,7 +55,7 @@ sub calculate_dropbox_date {
branchcode => $branchcode,
}
);
my $calendar = Koha::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
my $calendar = Koha::Library::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

View File

@ -186,6 +186,22 @@ 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' ],
},
@ -386,7 +402,8 @@ sub set_rule {
unless exists $params->{$mandatory_parameter};
}
my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
( my $rule_key = $params->{rule_name} ) =~ s/_\d+_/_X_/;
my $kind_info = $RULE_KINDS->{$rule_key};
Koha::Exceptions::MissingParameter->throw("set_rule given unknown rule '$params->{rule_name}'!")
unless defined $kind_info;
@ -802,7 +819,7 @@ sub get_effective_expire_reserves_charge {
}
=head3 type
=head3 _type
=cut

View File

@ -23,7 +23,7 @@ use base qw(Koha::Object);
=head1 NAME
Koha::ClassSortRule Koha Classfication Sorting Rule Object class
Koha::ClassSortRule Koha Classification Sorting Rule Object class
=head1 API

View File

@ -26,7 +26,7 @@ my $cache = Koha::Caches->get_instance();
=head1 NAME
Koha::ClassSource - Koha Classfication Source Object class
Koha::ClassSource - Koha Classification Source Object class
=head1 API

View File

@ -25,7 +25,7 @@ use base qw(Koha::Object);
=head1 NAME
Koha::ClassSplitRule Koha Classfication Splitting Rule Object class
Koha::ClassSplitRule Koha Classification Splitting Rule Object class
=head1 API

View File

@ -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::Calendar;
use Koha::Library::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::Calendar->new( branchcode => $params->{branchcode} );
my $calendar = Koha::Library::Calendar->new( branchcode => $params->{branchcode} );
Koha::Exceptions::CurbsidePickup::LibraryIsClosed->throw
if $calendar->is_holiday( $params->{scheduled_pickup_datetime} );

View File

@ -68,7 +68,8 @@ sub invalid_item_library {
=head2 ids
Build a hashref of IDs from a Koha::Object Koha::Objects-based object.
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.
=cut
@ -76,8 +77,8 @@ sub ids {
my ( $self, $object ) = @_;
if ( $object->can('_resultset') ) {
# It's a Koha::Objects
return [ $object->get_column('biblionumber') ];
# It's a Koha::Objects, don't materialize the ids, let the database resolve the set
return $object->_resultset->get_column('biblionumber')->as_query;
} else {
# It's a single Koha::Object
@ -98,7 +99,7 @@ sub no_item_type {
if ( C4::Context->preference('item-level_itypes') ) {
my $items_without_itype = Koha::Items->search(
{
biblionumber => $ids,
biblionumber => { -in => $ids },
-or => [ itype => undef, itype => '' ]
}
);
@ -122,7 +123,7 @@ sub no_item_type {
} else {
my $biblioitems_without_itemtype = Koha::Biblioitems->search(
{
biblionumber => $ids,
biblionumber => { -in => $ids },
-or => [ itemtype => undef, itemtype => '' ]
}
);
@ -152,7 +153,7 @@ sub invalid_item_type {
if ( C4::Context->preference('item-level_itypes') ) {
my $items_with_invalid_itype = Koha::Items->search(
{
biblionumber => $ids,
biblionumber => { -in => $ids },
-and => [ itype => { not_in => \@itemtypes }, itype => { '!=' => '' } ]
}
);
@ -169,7 +170,7 @@ sub invalid_item_type {
} else {
my $biblioitems_with_invalid_itemtype = Koha::Biblioitems->search(
{
biblionumber => $ids,
biblionumber => { -in => $ids },
-and => [ itemtype => { not_in => \@itemtypes }, itemtype => { '!=' => '' } ]
}
);
@ -200,7 +201,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 => $ids } )->search( \$search_string );
Koha::Biblio::Metadatas->search( { biblionumber => { -in => $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,
@ -212,7 +213,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 => $ids } );
$biblios = Koha::Biblios->search( { biblionumber => { -in => $ids } } );
while ( my $biblio = $biblios->next ) {
my $record = eval { $biblio->metadata->record; };
if ($@) {
@ -248,7 +249,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 => $biblionumber,
biblioitemnumber_in_marc => $biblioitemnumber,
biblioitemnumber => $biblio->biblioitem->biblioitemnumber,
biblioitem_tag => $biblioitem_tag,
biblioitem_subfield => $biblioitem_subfield,
@ -316,7 +317,7 @@ sub nonexistent_AV {
# We are only checking biblios with items
my $items = Koha::Items->search(
{
'me.biblionumber' => $ids,
'me.biblionumber' => { -in => $ids },
$tmp_kohafield => {
-not_in => [ $avs->get_column('authorised_value'), '' ],
'!=' => undef,
@ -371,7 +372,7 @@ sub empty_title {
my @errors;
$biblios = Koha::Biblios->search(
{
biblionumber => $ids,
biblionumber => { -in => $ids },
-or => [
title => '',
title => undef,

View File

@ -140,6 +140,7 @@ sub get_files_to_test {
@koha_commit_history;
if ($last_build_commit) {
@files = @{ from_json( read_file("$self->{git_repo_dir}/$self->{test_dir}/$last_build_commit") ) };
@files = grep { $dev_files->get_filetype($_) eq $filetype } @files;
@files = $dev_files->remove_exceptions( \@files, $filetype );
push @files, $dev_files->ls_files( $filetype, "$last_build_commit HEAD" );
} else {

View File

@ -34,6 +34,7 @@ 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 );
@ -54,6 +55,7 @@ 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 );
@ -216,14 +218,26 @@ sub process_ordrsp {
my $action = $line->action_notification();
if ( $action eq 'cancelled' ) {
my $reason = $line->coded_orderline_text();
ModOrder(
{
ordernumber => $ordernumber,
cancellationreason => $reason,
orderstatus => 'cancelled',
datecancellationprinted => dt_from_string()->ymd(),
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{}
)
);
}
);
} 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();
@ -319,8 +333,9 @@ sub process_invoice {
if ( !$vendor_acct ) {
$invoice_message->add_to_edifact_errors(
{
section => "NAD+SU+" . $msg->supplier_ean,
details => "Skipped invoice $invoicenumber with unmatched vendor san: $vendor_ean"
invoicenumber => $invoicenumber,
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 "
@ -349,8 +364,9 @@ sub process_invoice {
if ( !$ordernumber ) {
$invoice_message->add_to_edifact_errors(
{
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
details => "Skipped invoice line " . $line->line_item_number . ", missing ordernumber"
invoicenumber => $invoicenumber,
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");
@ -362,8 +378,9 @@ sub process_invoice {
if ( !$order ) {
$invoice_message->add_to_edifact_errors(
{
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
details => "Skipped invoice line "
invoicenumber => $invoicenumber,
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
details => "Skipped invoice line "
. $line->line_item_number
. ", cannot find order with ordernumber "
. $ordernumber
@ -377,8 +394,9 @@ sub process_invoice {
if ( !$bib ) {
$invoice_message->add_to_edifact_errors(
{
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
details => "Skipped invoice line "
invoicenumber => $invoicenumber,
section => join( "\n", map { $_->as_string } @{ $line->{segs} } ),
details => "Skipped invoice line "
. $line->line_item_number
. ", cannot find biblio for ordernumber "
. $ordernumber
@ -393,6 +411,24 @@ 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(
{
@ -447,7 +483,8 @@ sub process_invoice {
transfer_items( $schema, $line, $order, $received_order, $quantity );
receipt_items(
$schema, $line,
$received_order->ordernumber, $quantity, $invoice_message
$received_order->ordernumber, $quantity, $invoice_message,
$invoicenumber
);
} else { # simple receipt all copies on order
$order->quantityreceived($quantity);
@ -461,7 +498,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 );
receipt_items( $schema, $line, $ordernumber, $quantity, $invoice_message, $invoicenumber );
}
}
}
@ -498,7 +535,7 @@ sub _get_invoiced_price {
}
sub receipt_items {
my ( $schema, $inv_line, $ordernumber, $quantity, $invoice_message ) = @_;
my ( $schema, $inv_line, $ordernumber, $quantity, $invoice_message, $invoicenumber ) = @_;
my $logger = Koha::Logger->get( { interface => 'edi' } );
# itemnumber is not a foreign key ??? makes this a bit cumbersome
@ -514,12 +551,13 @@ sub receipt_items {
my $i = $ilink->itemnumber;
$invoice_message->add_to_edifact_errors(
{
section => join( "\n", map { $_->as_string } @{ $inv_line->{segs} } ),
details => "Skipped invoice line "
invoicenumber => $invoicenumber,
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");
@ -562,10 +600,11 @@ sub receipt_items {
if ( $rs->count > 0 ) {
$invoice_message->add_to_edifact_errors(
{
section => join( "\n", map { $_->as_string } @{ $inv_line->{segs} } ),
details => "Failed to assign barcode "
invoicenumber => $invoicenumber,
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"
}
@ -592,16 +631,20 @@ sub receipt_items {
$item->update;
} else {
my $available_branches = join( ', ', sort keys %branch_map ) || 'none';
$invoice_message->add_to_edifact_errors(
{
section => join( "\n", map { $_->as_string } @{ $inv_line->{segs} } ),
details => "No matching item found for invoice line "
invoicenumber => $invoicenumber,
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");
$logger->warn("Unmatched item at branch:$branch for order:$ordernumber (available: $available_branches)");
}
++$gir_occurrence;
}
@ -916,17 +959,55 @@ sub quote_item {
$order_hash->{line_item_id} = $item->item_number_id;
}
if ( $item->girfield('servicing_instruction') ) {
my $occ = 0;
my $txt = q{};
my $si;
while ( $si = $item->girfield( 'servicing_instruction', $occ ) ) {
if ($occ) {
$txt .= q{: };
# 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");
}
$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;
@ -1056,6 +1137,15 @@ 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");
@ -1603,7 +1693,7 @@ Koha::EDI
Returns 1 on success undef on error
Most usual cause of error is a line with no or incorrect budget codes
which woild cause order creation to abort
which would cause order creation to abort
If other correct lines exist these are processed and the erroneous line os logged
=head2 title_level_class
@ -1669,6 +1759,20 @@ 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>

View File

@ -67,8 +67,7 @@ Returns the resources from this package
sub resources {
my ($self) = @_;
my $rs = $self->_result->erm_eholdings_resources;
return Koha::ERM::EHoldings::Resources->_new_from_dbic($rs);
return Koha::ERM::EHoldings::Resources->_new_from_dbic( scalar $self->_result->resources );
}
=head3 vendor

View File

@ -414,7 +414,7 @@ Edifact - Edifact message handler
=head2 service_string_advice
Examines the Service String Advice returns 1 if the default separartors are in use
Examines the Service String Advice returns 1 if the default separators are in use
undef otherwise
=head2 segmentize

View File

@ -26,7 +26,9 @@ use DateTime;
use Readonly qw( Readonly );
use Koha::Database;
use Koha::DateUtils qw( dt_from_string );
use C4::Budgets qw( GetBudget );
use Koha::Logger;
use C4::Budgets qw( GetBudget );
use JSON qw( decode_json );
use Koha::Acquisition::Orders;
@ -149,7 +151,7 @@ sub interchange_header {
$hdr .= $self->interchange_control_reference();
$hdr .= $separator;
# Recipents reference password not usually used in edifact
# Recipients reference password not usually used in edifact
$hdr .= q{+ORDERS}; # application reference
#Edifact does not usually include the following
@ -432,7 +434,10 @@ sub order_line {
}
}
my $budget = GetBudget( $orderline->budget_id );
my $ol_fields = { budget_code => $budget->{budget_code}, };
my $ol_fields = {
budget_code => $budget->{budget_code},
servicing_instruction => $orderline->servicing_instruction,
};
my $item_fields = [];
for my $item (@items) {
@ -468,7 +473,7 @@ sub order_line {
# FTX free text for current orderline
# Pass vendor note in FTX free text segment
if ( $orderline->order_vendornote ) {
my $vendornote = $orderline->order_vendornote;
my $vendornote = encode_text( $orderline->order_vendornote );
chomp $vendornote;
my $ftx = 'FTX+LIN+++';
$ftx .= $vendornote;
@ -492,7 +497,7 @@ sub order_line {
# RFF : suppliers unique quotation reference number
if ( $orderline->suppliers_reference_number ) {
$rff = join q{}, 'RFF+', $orderline->suppliers_reference_qualifier,
':', $orderline->suppliers_reference_number, $seg_terminator;
':', encode_text( $orderline->suppliers_reference_number ), $seg_terminator;
$self->add_seg($rff);
}
@ -579,7 +584,7 @@ sub gir_segments {
my $orderfields = $params->{ol_fields};
my @onorderitems = @{ $params->{items} };
my $budget_code = $orderfields->{budget_code};
my $budget_code = encode_text( $orderfields->{budget_code} );
my @segments;
my $sequence_no = 1;
my $lsq_field = C4::Context->preference('EdifactLSQ');
@ -593,36 +598,61 @@ sub gir_segments {
}
if ( $item->{branchcode} ) {
push @gir_elements,
{ identity_number => 'LLO', data => $item->{branchcode} };
{ identity_number => 'LLO', data => encode_text( $item->{branchcode} ) };
}
if ( $item->{itype} ) {
push @gir_elements,
{ identity_number => 'LST', data => $item->{itype} };
{ identity_number => 'LST', data => encode_text( $item->{itype} ) };
}
# Handle LSQ mapping
if ( $lsq_field && $item->{$lsq_field} ) {
push @gir_elements,
{ identity_number => 'LSQ', data => $item->{$lsq_field} };
{ identity_number => 'LSQ', data => encode_text( $item->{$lsq_field} ) };
}
# Handle LSL mapping
if ( $lsl_field && $item->{$lsl_field} ) {
push @gir_elements,
{ identity_number => 'LSL', data => $item->{$lsl_field} };
{ identity_number => 'LSL', data => encode_text( $item->{$lsl_field} ) };
}
if ( $item->{itemcallnumber} ) {
push @gir_elements,
{ identity_number => 'LSM', data => $item->{itemcallnumber} };
{ identity_number => 'LSM', data => encode_text( $item->{itemcallnumber} ) };
}
# itemcallnumber -> shelfmark
# 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
if ( $orderfields->{servicing_instruction} ) {
push @gir_elements,
{
identity_number => 'LVT',
data => $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} )
};
}
}
}
}
}
}
my $e_cnt = 0; # count number of elements so we dont exceed 5 per segment
my $copy_no = sprintf 'GIR+%03d', $sequence_no;
@ -630,7 +660,8 @@ sub gir_segments {
foreach my $e (@gir_elements) {
if ( $e_cnt == 5 ) {
push @segments, $seg;
$seg = $copy_no;
$seg = $copy_no;
$e_cnt = 0;
}
$seg .=
add_gir_identity_number( $e->{identity_number}, $e->{data} );
@ -666,7 +697,7 @@ sub lin_segment {
my ( $line_number, $item_number_id ) = @_;
if ($item_number_id) {
$item_number_id = "++${item_number_id}:EN";
$item_number_id = "++" . encode_text($item_number_id) . ":EN";
} else {
$item_number_id = q||;
}
@ -691,7 +722,7 @@ sub additional_product_id {
}
# function id set to 5 states this is the main product id
return "PIA+5+$product_id:$product_code$seg_terminator";
return "PIA+5+" . encode_text($product_id) . ":$product_code$seg_terminator";
}
sub message_date_segment {

View File

@ -28,6 +28,7 @@ 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 ) = @_;
@ -42,8 +43,9 @@ 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;
@ -63,7 +65,7 @@ sub download_messages {
$self->{message_type} = $message_type;
unless ( $self->{file_transport} ) {
carp "No file transport configured for EDI account " . $self->{account}->id;
$self->{logger}->error( "No file transport configured for EDI account " . $self->{account}->id );
return;
}
@ -73,21 +75,21 @@ sub download_messages {
# Connect to the transport
unless ( $self->{file_transport}->connect() ) {
carp "Failed to connect to file transport: " . $self->{file_transport}->id;
$self->{logger}->error( "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) ) {
carp "Failed to change to download directory: $download_dir";
$self->{logger}->error("Failed to change to download directory: $download_dir");
return;
}
# Get file list
my $file_list = $self->{file_transport}->list_files();
unless ($file_list) {
carp "Failed to get file list from transport";
$self->{logger}->error("Failed to get file list from transport");
return;
}
@ -109,7 +111,7 @@ sub download_messages {
# Mark file as processed using the transport's rename functionality
$self->{file_transport}->rename_file( $filename, $processed_name );
} else {
carp "Failed to download file: $filename";
$self->{logger}->error("Failed to download file: $filename");
}
}
}
@ -131,20 +133,20 @@ sub upload_messages {
}
unless ( $self->{file_transport} ) {
carp "No file transport configured for EDI account " . $self->{account}->id;
$self->{logger}->error( "No file transport configured for EDI account " . $self->{account}->id );
return;
}
# Connect to the transport
unless ( $self->{file_transport}->connect() ) {
carp "Failed to connect to file transport: " . $self->{file_transport}->id;
$self->{logger}->error( "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) ) {
carp "Failed to change to upload directory: $upload_dir";
$self->{logger}->error("Failed to change to upload directory: $upload_dir");
return;
}
@ -165,13 +167,13 @@ sub upload_messages {
$m->status('sent');
$m->update;
} else {
carp "Failed to upload file: " . $m->filename;
$self->{logger}->error( "Failed to upload file: " . $m->filename );
}
# Clean up temp file
unlink $temp_file;
} else {
carp "Could not create temporary file for upload: " . $m->filename;
$self->{logger}->error( "Could not create temporary file for upload: " . $m->filename );
}
}
}

View File

@ -40,7 +40,8 @@ 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 // ''
);
}
}

View File

@ -69,6 +69,15 @@ 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 {

View File

@ -21,7 +21,7 @@ use Exception::Class (
},
'Koha::Exceptions::Patron::FailedDeleteAnonymousPatron' => {
isa => 'Koha::Exceptions::Patron',
description => "Deleting patron failed, AnonymousPatron is not deleteable"
description => "Deleting patron failed, AnonymousPatron is not deletable"
},
'Koha::Exceptions::Patron::FailedDeleteProtectedPatron' => {
isa => 'Koha::Exceptions::Patron',

View File

@ -0,0 +1,56 @@
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;

View File

@ -2,8 +2,13 @@ package Koha::Exceptions::Report;
use Modern::Perl;
use Koha::Exception;
use Exception::Class (
'Koha::Exceptions::Report' => { description => "Something went wrong!" },
'Koha::Exceptions::Report' => {
isa => 'Koha::Exception',
description => 'A report operation failed',
},
'Koha::Exceptions::Report::DuplicateRunning' => {
isa => 'Koha::Exceptions::Report',
description => "The configured per-user limit on simultaneous runs of this report has been reached",
@ -14,6 +19,46 @@ 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;

View File

@ -28,6 +28,7 @@ use List::MoreUtils qw( any );
use Koha::Database;
use Koha::Exceptions::Object;
use Koha::Encryption;
use Koha::Logger;
use base qw(Koha::Object);
@ -244,6 +245,103 @@ 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);

View File

@ -17,6 +17,7 @@ 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);
@ -122,6 +123,26 @@ 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);
@ -136,15 +157,15 @@ sub _change_directory {
my ( $self, $remote_directory ) = @_;
my $operation = "change_directory";
$self->{connection}->cwd($remote_directory) or return $self->_abort_operation($operation);
$self->{connection}->cwd($remote_directory) or return $self->_abort_operation( $operation, $remote_directory );
$self->add_message(
{
message => $operation,
type => 'success',
payload => {
directory => $remote_directory,
pwd => $self->{connection}->pwd
path => $remote_directory,
pwd => $self->_current_directory
}
}
);
@ -156,7 +177,22 @@ 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.
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.
=cut
@ -164,26 +200,63 @@ 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} ) {
# 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 );
# 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");
# Skip directories (start with 'd')
next if $perms =~ /^d/;
if ( $mlsd && @{$mlsd} ) {
foreach my $line ( @{$mlsd} ) {
# 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 => $perms
perms => undef,
mtime => $self->{connection}->mdtm($name),
type => defined $size ? 'file' : 'directory',
};
}
}
@ -194,7 +267,7 @@ sub _list_files {
type => 'success',
payload => {
count => scalar @file_list,
pwd => $self->{connection}->pwd
pwd => $self->_current_directory
}
}
);
@ -202,6 +275,60 @@ 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.
@ -255,20 +382,35 @@ Internal method to check if transport is currently connected.
sub _is_connected {
my ($self) = @_;
return $self->{connection} && $self->{connection}->pwd();
return $self->{connection} && $self->_current_directory();
}
sub _abort_operation {
my ( $self, $operation ) = @_;
=head3 _abort_operation
$self->add_message(
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 ) = @_;
$self->_record_error(
$operation,
{
message => $operation || 'operation',
type => 'error',
payload => {
detail => $self->{connection} ? $self->{connection}->status : '',
error => $self->{connection} ? $self->{connection}->message : $@
}
detail => $self->{connection} ? $self->{connection}->status : '',
error => $self->{connection} ? $self->{connection}->message : $@,
path => $path,
}
);

View File

@ -19,6 +19,7 @@ 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);
@ -43,62 +44,46 @@ sub _connect {
# Check download directory if configured
if ( my $download_dir = $self->download_directory ) {
unless ( -d $download_dir ) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => "Download directory does not exist: $download_dir",
path => $download_dir
}
error => "Download directory does not exist: $download_dir",
path => $download_dir
}
);
return;
}
unless ( -r $download_dir ) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => "Download directory is not readable: $download_dir",
path => $download_dir
}
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 ) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => "Upload directory does not exist: $upload_dir",
path => $upload_dir
}
error => "Upload directory does not exist: $upload_dir",
path => $upload_dir
}
);
return;
}
unless ( -w $upload_dir ) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => "Upload directory is not writable: $upload_dir",
path => $upload_dir
}
error => "Upload directory is not writable: $upload_dir",
path => $upload_dir
}
);
return;
}
}
@ -117,6 +102,28 @@ 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.
@ -129,21 +136,24 @@ sub _upload_file {
my ( $self, $local_file, $remote_file ) = @_;
my $operation = "upload";
my $upload_dir = $self->{current_directory} || $self->upload_directory || '.';
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 $destination = File::Spec->catfile( $upload_dir, $remote_file );
unless ( copy( $local_file, $destination ) ) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => $!,
path => $destination
}
error => $!,
path => $destination
}
);
return;
}
$self->add_message(
@ -169,35 +179,34 @@ sub _download_file {
my ( $self, $remote_file, $local_file ) = @_;
my $operation = 'download';
my $download_dir = $self->{current_directory} || $self->download_directory || '.';
my $source = File::Spec->catfile( $download_dir, $remote_file );
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 );
unless ( -f $source ) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => "File not found: $source",
path => $source
}
error => "File not found: $source",
path => $source
}
);
return;
}
unless ( copy( $source, $local_file ) ) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => $!,
path => $source
}
error => $!,
path => $source
}
);
return;
}
$self->add_message(
@ -227,17 +236,13 @@ sub _change_directory {
# For local file transport, we just track the current directory
if ( $remote_directory && !-d $remote_directory ) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => "Directory not found: $remote_directory",
path => $remote_directory
}
error => "Directory not found: $remote_directory",
path => $remote_directory
}
);
return;
}
$self->{current_directory} = $remote_directory;
@ -253,11 +258,27 @@ 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.
Each hashref contains: filename, longname, size, perms, mtime, type.
=cut
@ -265,49 +286,53 @@ sub _list_files {
my ($self) = @_;
my $operation = "list";
my $directory = $self->{current_directory} || $self->download_directory || '.';
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()' }
);
}
unless ( -d $directory ) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => "Directory not found: $directory",
path => $directory
}
error => "Directory not found: $directory",
path => $directory
}
);
return;
}
my $dir_handle = IO::Dir->new($directory);
unless ($dir_handle) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => "Cannot open directory: $!",
path => $directory
}
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 SFTP format
my @stat = stat($full_path);
# 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)
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') );
@ -316,7 +341,8 @@ sub _list_files {
longname => sprintf( "%s %8d %s %s", $perms, $size, scalar( localtime($mtime) ), $file ),
size => $size,
perms => $perms,
mtime => $mtime
mtime => $mtime,
type => $type,
};
}
$dir_handle->close;
@ -347,36 +373,35 @@ sub _rename_file {
my ( $self, $old_name, $new_name ) = @_;
my $operation = "rename";
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 );
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 );
unless ( -f $old_path ) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => "File not found: $old_path",
path => $old_path
}
error => "File not found: $old_path",
path => $old_path
}
);
return;
}
unless ( move( $old_path, $new_path ) ) {
$self->add_message(
return $self->_abort_operation(
$operation,
{
message => $operation,
type => 'error',
payload => {
error => $!,
path => "$old_path -> $new_path"
}
error => $!,
path => "$old_path -> $new_path"
}
);
return;
}
$self->add_message(
@ -390,6 +415,29 @@ 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.

View File

@ -23,7 +23,7 @@ use File::Spec;
use IO::File;
use Net::SFTP::Foreign;
use Try::Tiny;
use JSON qw( decode_json encode_json );
use Fcntl qw( S_ISDIR S_ISREG );
use base qw(Koha::File::Transport);
@ -137,6 +137,24 @@ 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);
@ -172,7 +190,16 @@ 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, a (attributes).
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.
=cut
@ -182,6 +209,22 @@ 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,
@ -194,7 +237,7 @@ sub _list_files {
}
);
return $file_list;
return \@files;
}
=head3 _rename_file
@ -338,6 +381,10 @@ 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 {
@ -353,24 +400,12 @@ sub _abort_operation {
error_raw => $stderr
};
$self->add_message(
{
message => $operation,
type => 'error',
payload => $payload
}
);
$self->_record_error( $operation, $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;
}

View File

@ -56,7 +56,7 @@ Koha::FrameworkPlugin - Facilitate use of plugins in MARC/items editor
This object support two code styles:
- In the new style, the plugin returns a hashref with a builder and a
launcher key pointing to two anynomous subroutines.
launcher key pointing to two anonymous subroutines.
- In the old style, the builder is subroutine plugin_javascript and the
launcher is subroutine plugin. For each plugin the routines are
redefined.

View File

@ -34,7 +34,7 @@ use Koha::Biblios;
use Koha::Hold::CancellationRequests;
use Koha::Items;
use Koha::Libraries;
use Koha::Calendar;
use Koha::Library::Calendar;
use Koha::Plugins;
use Koha::ILL::ISO18626::Request;
@ -72,7 +72,7 @@ sub age {
my $age;
if ($use_calendar) {
my $calendar = Koha::Calendar->new( branchcode => $self->branchcode );
my $calendar = Koha::Library::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,14 +214,32 @@ 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' };
}
@ -410,7 +428,7 @@ sub set_waiting {
branchcode => $self->branchcode,
}
);
my $calendar = Koha::Calendar->new( branchcode => $self->branchcode, days_mode => $daysmode );
my $calendar = Koha::Library::Calendar->new( branchcode => $self->branchcode, days_mode => $daysmode );
$new_expiration_date = $calendar->days_forward( dt_from_string( $self->waitingdate ), $max_pickup_delay );
}
@ -1697,6 +1715,30 @@ 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

View File

@ -161,11 +161,18 @@ 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 ] },
itemnumber => { -not_in => [ @branchtransfers, @waiting_holds, @pending_recalls ] },
onloan => undef,
notforloan => 0,
}

View File

@ -87,8 +87,7 @@ Return the I<Koha::ILL::Requests> for this batch
sub requests {
my ($self) = @_;
my $requests = $self->_result->requests;
return Koha::ILL::Requests->_new_from_dbic($requests);
return Koha::ILL::Requests->_new_from_dbic( scalar $self->_result->requests );
}
=head3 create_and_log
@ -108,7 +107,7 @@ sub create_and_log {
$logger->log_something(
{
modulename => 'ILL',
modulename => 'ILL_BATCHES',
actionname => 'batch_create',
objectnumber => $self->id,
infos => to_json( {} )
@ -144,7 +143,7 @@ sub update_and_log {
$logger->log_something(
{
modulename => 'ILL',
modulename => 'ILL_BATCHES',
actionname => 'batch_update',
objectnumber => $self->id,
infos => to_json(
@ -172,7 +171,7 @@ sub delete_and_log {
$logger->log_something(
{
modulename => 'ILL',
modulename => 'ILL_BATCHES',
actionname => 'batch_delete',
objectnumber => $self->id,
infos => to_json( {} )

Some files were not shown because too many files have changed in this diff Show More