Main Koha release repository https://koha-community.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

570 lines
15 KiB

  1. package Koha::REST::V1::Holds;
  2. # This file is part of Koha.
  3. #
  4. # Koha is free software; you can redistribute it and/or modify it
  5. # under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # Koha is distributed in the hope that it will be useful, but
  10. # WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with Koha; if not, see <http://www.gnu.org/licenses>.
  16. use Modern::Perl;
  17. use Mojo::Base 'Mojolicious::Controller';
  18. use Mojo::JSON qw(decode_json);
  19. use C4::Biblio;
  20. use C4::Reserves;
  21. use Koha::Items;
  22. use Koha::Patrons;
  23. use Koha::Holds;
  24. use Koha::DateUtils;
  25. use List::MoreUtils qw(any);
  26. use Try::Tiny;
  27. =head1 API
  28. =head2 Methods
  29. =head3 list
  30. Method that handles listing Koha::Hold objects
  31. =cut
  32. sub list {
  33. my $c = shift->openapi->valid_input or return;
  34. return try {
  35. my $holds_set = Koha::Holds->new;
  36. my $holds = $c->objects->search( $holds_set );
  37. return $c->render( status => 200, openapi => $holds );
  38. }
  39. catch {
  40. $c->unhandled_exception($_);
  41. };
  42. }
  43. =head3 add
  44. Method that handles adding a new Koha::Hold object
  45. =cut
  46. sub add {
  47. my $c = shift->openapi->valid_input or return;
  48. return try {
  49. my $body = $c->validation->param('body');
  50. my $biblio;
  51. my $item;
  52. my $biblio_id = $body->{biblio_id};
  53. my $pickup_library_id = $body->{pickup_library_id};
  54. my $item_id = $body->{item_id};
  55. my $patron_id = $body->{patron_id};
  56. my $item_type = $body->{item_type};
  57. my $expiration_date = $body->{expiration_date};
  58. my $notes = $body->{notes};
  59. my $hold_date = $body->{hold_date};
  60. my $non_priority = $body->{non_priority};
  61. my $overrides = $c->stash('koha.overrides');
  62. my $can_override = $overrides->{any} && C4::Context->preference('AllowHoldPolicyOverride');
  63. if(!C4::Context->preference( 'AllowHoldDateInFuture' ) && $hold_date) {
  64. return $c->render(
  65. status => 400,
  66. openapi => { error => "Hold date in future not allowed" }
  67. );
  68. }
  69. if ( $item_id and $biblio_id ) {
  70. # check they are consistent
  71. unless ( Koha::Items->search( { itemnumber => $item_id, biblionumber => $biblio_id } )
  72. ->count > 0 )
  73. {
  74. return $c->render(
  75. status => 400,
  76. openapi => { error => "Item $item_id doesn't belong to biblio $biblio_id" }
  77. );
  78. }
  79. else {
  80. $biblio = Koha::Biblios->find($biblio_id);
  81. }
  82. }
  83. elsif ($item_id) {
  84. $item = Koha::Items->find($item_id);
  85. unless ($item) {
  86. return $c->render(
  87. status => 404,
  88. openapi => { error => "item_id not found." }
  89. );
  90. }
  91. else {
  92. $biblio = $item->biblio;
  93. }
  94. }
  95. elsif ($biblio_id) {
  96. $biblio = Koha::Biblios->find($biblio_id);
  97. }
  98. else {
  99. return $c->render(
  100. status => 400,
  101. openapi => { error => "At least one of biblio_id, item_id should be given" }
  102. );
  103. }
  104. unless ($biblio) {
  105. return $c->render(
  106. status => 400,
  107. openapi => "Biblio not found."
  108. );
  109. }
  110. my $patron = Koha::Patrons->find( $patron_id );
  111. unless ($patron) {
  112. return $c->render(
  113. status => 400,
  114. openapi => { error => 'patron_id not found' }
  115. );
  116. }
  117. # Validate pickup location
  118. my $valid_pickup_location;
  119. if ($item) { # item-level hold
  120. $valid_pickup_location =
  121. any { $_->branchcode eq $pickup_library_id }
  122. $item->pickup_locations(
  123. { patron => $patron } );
  124. }
  125. else {
  126. $valid_pickup_location =
  127. any { $_->branchcode eq $pickup_library_id }
  128. $biblio->pickup_locations(
  129. { patron => $patron } );
  130. }
  131. return $c->render(
  132. status => 400,
  133. openapi => {
  134. error => 'The supplied pickup location is not valid'
  135. }
  136. ) unless $valid_pickup_location || $can_override;
  137. my $can_place_hold
  138. = $item_id
  139. ? C4::Reserves::CanItemBeReserved( $patron_id, $item_id )
  140. : C4::Reserves::CanBookBeReserved( $patron_id, $biblio_id );
  141. if ( $patron->holds->count + 1 > C4::Context->preference('maxreserves') ) {
  142. $can_place_hold->{status} = 'tooManyReserves';
  143. }
  144. unless ( $can_override || $can_place_hold->{status} eq 'OK' ) {
  145. return $c->render(
  146. status => 403,
  147. openapi =>
  148. { error => "Hold cannot be placed. Reason: " . $can_place_hold->{status} }
  149. );
  150. }
  151. my $priority = C4::Reserves::CalculatePriority($biblio_id);
  152. # AddReserve expects date to be in syspref format
  153. if ($expiration_date) {
  154. $expiration_date = output_pref( dt_from_string( $expiration_date, 'rfc3339' ) );
  155. }
  156. my $hold_id = C4::Reserves::AddReserve(
  157. {
  158. branchcode => $pickup_library_id,
  159. borrowernumber => $patron_id,
  160. biblionumber => $biblio_id,
  161. priority => $priority,
  162. reservation_date => $hold_date,
  163. expiration_date => $expiration_date,
  164. notes => $notes,
  165. title => $biblio->title,
  166. itemnumber => $item_id,
  167. found => undef, # TODO: Why not?
  168. itemtype => $item_type,
  169. non_priority => $non_priority,
  170. }
  171. );
  172. unless ($hold_id) {
  173. return $c->render(
  174. status => 500,
  175. openapi => 'Error placing the hold. See Koha logs for details.'
  176. );
  177. }
  178. my $hold = Koha::Holds->find($hold_id);
  179. return $c->render(
  180. status => 201,
  181. openapi => $hold->to_api
  182. );
  183. }
  184. catch {
  185. if ( blessed $_ and $_->isa('Koha::Exceptions') ) {
  186. if ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) {
  187. my $broken_fk = $_->broken_fk;
  188. if ( grep { $_ eq $broken_fk } keys %{Koha::Holds->new->to_api_mapping} ) {
  189. $c->render(
  190. status => 404,
  191. openapi => Koha::Holds->new->to_api_mapping->{$broken_fk} . ' not found.'
  192. );
  193. }
  194. }
  195. }
  196. $c->unhandled_exception($_);
  197. };
  198. }
  199. =head3 edit
  200. Method that handles modifying a Koha::Hold object
  201. =cut
  202. sub edit {
  203. my $c = shift->openapi->valid_input or return;
  204. return try {
  205. my $hold_id = $c->validation->param('hold_id');
  206. my $hold = Koha::Holds->find( $hold_id );
  207. unless ($hold) {
  208. return $c->render(
  209. status => 404,
  210. openapi => { error => "Hold not found" }
  211. );
  212. }
  213. my $overrides = $c->stash('koha.overrides');
  214. my $can_override = $overrides->{any} && C4::Context->preference('AllowHoldPolicyOverride');
  215. my $body = $c->validation->output->{body};
  216. my $pickup_library_id = $body->{pickup_library_id};
  217. if (
  218. defined $pickup_library_id
  219. && ( !$hold->is_pickup_location_valid({ library_id => $pickup_library_id }) && !$can_override )
  220. )
  221. {
  222. return $c->render(
  223. status => 400,
  224. openapi => {
  225. error => 'The supplied pickup location is not valid'
  226. }
  227. );
  228. }
  229. $pickup_library_id //= $hold->branchcode;
  230. my $priority = $body->{priority} // $hold->priority;
  231. # suspended_until can also be set to undef
  232. my $suspended_until = exists $body->{suspended_until} ? $body->{suspended_until} : $hold->suspend_until;
  233. my $params = {
  234. reserve_id => $hold_id,
  235. branchcode => $pickup_library_id,
  236. rank => $priority,
  237. suspend_until => $suspended_until ? output_pref(dt_from_string($suspended_until, 'rfc3339')) : '',
  238. itemnumber => $hold->itemnumber
  239. };
  240. C4::Reserves::ModReserve($params);
  241. $hold->discard_changes; # refresh
  242. return $c->render(
  243. status => 200,
  244. openapi => $hold->to_api
  245. );
  246. }
  247. catch {
  248. $c->unhandled_exception($_);
  249. };
  250. }
  251. =head3 delete
  252. Method that handles deleting a Koha::Hold object
  253. =cut
  254. sub delete {
  255. my $c = shift->openapi->valid_input or return;
  256. my $hold_id = $c->validation->param('hold_id');
  257. my $hold = Koha::Holds->find($hold_id);
  258. unless ($hold) {
  259. return $c->render( status => 404, openapi => { error => "Hold not found." } );
  260. }
  261. return try {
  262. $hold->cancel;
  263. return $c->render(
  264. status => 204,
  265. openapi => q{}
  266. );
  267. }
  268. catch {
  269. $c->unhandled_exception($_);
  270. };
  271. }
  272. =head3 suspend
  273. Method that handles suspending a hold
  274. =cut
  275. sub suspend {
  276. my $c = shift->openapi->valid_input or return;
  277. my $hold_id = $c->validation->param('hold_id');
  278. my $hold = Koha::Holds->find($hold_id);
  279. my $body = $c->req->json;
  280. my $end_date = ($body) ? $body->{end_date} : undef;
  281. unless ($hold) {
  282. return $c->render( status => 404, openapi => { error => 'Hold not found.' } );
  283. }
  284. return try {
  285. my $date = ($end_date) ? dt_from_string( $end_date, 'rfc3339' ) : undef;
  286. $hold->suspend_hold($date);
  287. $hold->discard_changes;
  288. $c->res->headers->location( $c->req->url->to_string );
  289. my $suspend_end_date;
  290. if ($hold->suspend_until) {
  291. $suspend_end_date = output_pref({
  292. dt => dt_from_string( $hold->suspend_until ),
  293. dateformat => 'rfc3339',
  294. dateonly => 1
  295. }
  296. );
  297. }
  298. return $c->render(
  299. status => 201,
  300. openapi => {
  301. end_date => $suspend_end_date
  302. }
  303. );
  304. }
  305. catch {
  306. if ( blessed $_ and $_->isa('Koha::Exceptions::Hold::CannotSuspendFound') ) {
  307. return $c->render( status => 400, openapi => { error => "$_" } );
  308. }
  309. $c->unhandled_exception($_);
  310. };
  311. }
  312. =head3 resume
  313. Method that handles resuming a hold
  314. =cut
  315. sub resume {
  316. my $c = shift->openapi->valid_input or return;
  317. my $hold_id = $c->validation->param('hold_id');
  318. my $hold = Koha::Holds->find($hold_id);
  319. my $body = $c->req->json;
  320. unless ($hold) {
  321. return $c->render( status => 404, openapi => { error => 'Hold not found.' } );
  322. }
  323. return try {
  324. $hold->resume;
  325. return $c->render( status => 204, openapi => {} );
  326. }
  327. catch {
  328. $c->unhandled_exception($_);
  329. };
  330. }
  331. =head3 update_priority
  332. Method that handles modifying a Koha::Hold object
  333. =cut
  334. sub update_priority {
  335. my $c = shift->openapi->valid_input or return;
  336. my $hold_id = $c->validation->param('hold_id');
  337. my $hold = Koha::Holds->find($hold_id);
  338. unless ($hold) {
  339. return $c->render(
  340. status => 404,
  341. openapi => { error => "Hold not found" }
  342. );
  343. }
  344. return try {
  345. my $priority = $c->req->json;
  346. C4::Reserves::_FixPriority(
  347. {
  348. reserve_id => $hold_id,
  349. rank => $priority
  350. }
  351. );
  352. return $c->render( status => 200, openapi => $priority );
  353. }
  354. catch {
  355. $c->unhandled_exception($_);
  356. };
  357. }
  358. =head3 pickup_locations
  359. Method that returns the possible pickup_locations for a given hold
  360. used for building the dropdown selector
  361. =cut
  362. sub pickup_locations {
  363. my $c = shift->openapi->valid_input or return;
  364. my $hold_id = $c->validation->param('hold_id');
  365. my $hold = Koha::Holds->find( $hold_id, { prefetch => [ 'patron' ] } );
  366. unless ($hold) {
  367. return $c->render(
  368. status => 404,
  369. openapi => { error => "Hold not found" }
  370. );
  371. }
  372. return try {
  373. my $ps_set;
  374. if ( $hold->itemnumber ) {
  375. $ps_set = $hold->item->pickup_locations( { patron => $hold->patron } );
  376. }
  377. else {
  378. $ps_set = $hold->biblio->pickup_locations( { patron => $hold->patron } );
  379. }
  380. my $pickup_locations = $c->objects->search( $ps_set );
  381. my @response = ();
  382. if ( C4::Context->preference('AllowHoldPolicyOverride') ) {
  383. my $libraries_rs = Koha::Libraries->search( { pickup_location => 1 } );
  384. my $libraries = $c->objects->search($libraries_rs);
  385. @response = map {
  386. my $library = $_;
  387. $library->{needs_override} = (
  388. any { $_->{library_id} eq $library->{library_id} }
  389. @{$pickup_locations}
  390. )
  391. ? Mojo::JSON->false
  392. : Mojo::JSON->true;
  393. $library;
  394. } @{$libraries};
  395. return $c->render(
  396. status => 200,
  397. openapi => \@response
  398. );
  399. }
  400. @response = map { $_->{needs_override} = Mojo::JSON->false; $_; } @{$pickup_locations};
  401. return $c->render(
  402. status => 200,
  403. openapi => \@response
  404. );
  405. }
  406. catch {
  407. $c->unhandled_exception($_);
  408. };
  409. }
  410. =head3 update_pickup_location
  411. Method that handles modifying the pickup location of a Koha::Hold object
  412. =cut
  413. sub update_pickup_location {
  414. my $c = shift->openapi->valid_input or return;
  415. my $hold_id = $c->validation->param('hold_id');
  416. my $body = $c->validation->param('body');
  417. my $pickup_library_id = $body->{pickup_library_id};
  418. my $hold = Koha::Holds->find($hold_id);
  419. unless ($hold) {
  420. return $c->render(
  421. status => 404,
  422. openapi => { error => "Hold not found" }
  423. );
  424. }
  425. return try {
  426. my $overrides = $c->stash('koha.overrides');
  427. my $can_override = $overrides->{any} && C4::Context->preference('AllowHoldPolicyOverride');
  428. $hold->set_pickup_location(
  429. {
  430. library_id => $pickup_library_id,
  431. override => $can_override
  432. }
  433. );
  434. return $c->render(
  435. status => 200,
  436. openapi => {
  437. pickup_library_id => $pickup_library_id
  438. }
  439. );
  440. }
  441. catch {
  442. if ( blessed $_ and $_->isa('Koha::Exceptions::Hold::InvalidPickupLocation') ) {
  443. return $c->render(
  444. status => 400,
  445. openapi => {
  446. error => "$_"
  447. }
  448. );
  449. }
  450. $c->unhandled_exception($_);
  451. };
  452. }
  453. 1;