View | Details | Raw Unified | Return to bug 41358
Collapse All | Expand All

(-)a/C4/Reserves.pm (-14 / +11 lines)
Lines 265-284 sub AddReserve { Link Here
265
265
266
    # Log the hold creation
266
    # Log the hold creation
267
    if ( C4::Context->preference('HoldsLog') ) {
267
    if ( C4::Context->preference('HoldsLog') ) {
268
        my $info = $hold->id;
268
        my $info = to_json(
269
        if ( defined($confirmations) || defined($forced) ) {
269
            {
270
            $info = to_json(
270
                hold          => $hold->id,
271
                {
271
                branchcode    => $hold->branchcode,
272
                    hold          => $hold->id,
272
                biblionumber  => $hold->biblionumber,
273
                    branchcode    => $hold->branchcode,
273
                itemnumber    => $hold->itemnumber,
274
                    biblionumber  => $hold->biblionumber,
274
                confirmations => $confirmations || [],
275
                    itemnumber    => $hold->itemnumber,
275
                forced        => $forced        || []
276
                    confirmations => $confirmations,
276
            },
277
                    forced        => $forced
277
            { pretty => 1, canonical => 1 }
278
                },
278
        );
279
                { pretty => 1, canonical => 1 }
280
            );
281
        }
282
        logaction( 'HOLDS', 'CREATE', $hold->id, $info );
279
        logaction( 'HOLDS', 'CREATE', $hold->id, $info );
283
    }
280
    }
284
281
(-)a/misc/maintenance/migrate_circulation_logs_to_json.pl (-20 / +179 lines)
Lines 27-40 use C4::Context; Link Here
27
use Koha::ActionLogs;
27
use Koha::ActionLogs;
28
use Koha::Checkouts;
28
use Koha::Checkouts;
29
use Koha::Old::Checkouts;
29
use Koha::Old::Checkouts;
30
use Koha::Holds;
31
use Koha::Old::Holds;
30
32
31
=head1 NAME
33
=head1 NAME
32
34
33
migrate_circulation_logs_to_json.pl - Migrate old circulation log entries to JSON format
35
migrate_action_logs_to_json.pl - Migrate old circulation and holds log entries to JSON format
34
36
35
=head1 SYNOPSIS
37
=head1 SYNOPSIS
36
38
37
migrate_circulation_logs_to_json.pl [ -c | --commit ] [ -v | --verbose ] [ --help ]
39
migrate_action_logs_to_json.pl [ -c | --commit ] [ -v | --verbose ] [ --help ]
38
40
39
 Options:
41
 Options:
40
   --help or -h       Brief usage message
42
   --help or -h       Brief usage message
Lines 44-57 migrate_circulation_logs_to_json.pl [ -c | --commit ] [ -v | --verbose ] [ --hel Link Here
44
46
45
=head1 DESCRIPTION
47
=head1 DESCRIPTION
46
48
47
This script migrates old CIRCULATION ISSUE action log entries from the legacy
49
This script migrates old action log entries from the legacy ID-only format to
48
itemnumber-only format to the new consistent JSON format. This is necessary
50
the new consistent JSON format. This is necessary after the fix for Bug 41358
49
after the fix for Bug 41358 which ensures all circulation logs are stored in
51
which ensures all circulation and holds logs are stored in JSON format for
50
JSON format for consistent reporting.
52
consistent reporting.
51
53
52
The script will:
54
The script will:
53
1. Find all CIRCULATION ISSUE log entries where info is just an itemnumber (not JSON)
55
1. Find all CIRCULATION ISSUE log entries where info is just an itemnumber (not JSON)
54
2. Convert them to JSON format with the structure:
56
2. Find all HOLDS CREATE log entries where info is just a hold_id (not JSON)
57
3. Convert them to JSON format with the appropriate structure:
58
59
   For CIRCULATION ISSUE:
55
   {
60
   {
56
     "issue": <issue_id if available>,
61
     "issue": <issue_id if available>,
57
     "branchcode": <branchcode if available>,
62
     "branchcode": <branchcode if available>,
Lines 60-65 The script will: Link Here
60
     "forced": []
65
     "forced": []
61
   }
66
   }
62
67
68
   For HOLDS CREATE:
69
   {
70
     "hold": <hold_id>,
71
     "branchcode": <branchcode if available>,
72
     "biblionumber": <biblionumber if available>,
73
     "itemnumber": <itemnumber if available>,
74
     "confirmations": [],
75
     "forced": []
76
   }
77
63
By default, the script runs in dry-run mode. Use --commit to actually update the database.
78
By default, the script runs in dry-run mode. Use --commit to actually update the database.
64
79
65
=head1 WARNING
80
=head1 WARNING
Lines 87-100 if ($help) { Link Here
87
102
88
my $dbh = C4::Context->dbh;
103
my $dbh = C4::Context->dbh;
89
104
90
print "Starting migration of CIRCULATION ISSUE logs to JSON format...\n";
105
print "Starting migration of action logs to JSON format...\n";
91
print $commit
106
print $commit
92
    ? "COMMIT MODE - Changes will be saved\n"
107
    ? "COMMIT MODE - Changes will be saved\n"
93
    : "DRY RUN MODE - No changes will be saved (use --commit to save)\n";
108
    : "DRY RUN MODE - No changes will be saved (use --commit to save)\n";
94
print "\n";
109
print "\n";
95
110
96
# First, count how many entries need conversion
111
# Count CIRCULATION ISSUE entries
97
my $count_sql = q(
112
my $circ_count_sql = q(
98
    SELECT COUNT(*)
113
    SELECT COUNT(*)
99
    FROM action_logs
114
    FROM action_logs
100
    WHERE module = 'CIRCULATION'
115
    WHERE module = 'CIRCULATION'
Lines 104-112 my $count_sql = q( Link Here
104
      AND info REGEXP '^[0-9]+$'
119
      AND info REGEXP '^[0-9]+$'
105
);
120
);
106
121
107
my ($total_count) = $dbh->selectrow_array($count_sql);
122
my ($circ_count) = $dbh->selectrow_array($circ_count_sql);
123
124
# Count HOLDS CREATE entries
125
my $holds_count_sql = q(
126
    SELECT COUNT(*)
127
    FROM action_logs
128
    WHERE module = 'HOLDS'
129
      AND action = 'CREATE'
130
      AND info IS NOT NULL
131
      AND info NOT LIKE '{%'
132
      AND info REGEXP '^[0-9]+$'
133
);
134
135
my ($holds_count) = $dbh->selectrow_array($holds_count_sql);
108
136
109
print "Found $total_count log entries to migrate\n";
137
my $total_count = $circ_count + $holds_count;
138
139
print "Found $circ_count CIRCULATION ISSUE log entries to migrate\n";
140
print "Found $holds_count HOLDS CREATE log entries to migrate\n";
141
print "Total: $total_count entries\n";
110
print "\n";
142
print "\n";
111
143
112
if ( $total_count == 0 ) {
144
if ( $total_count == 0 ) {
Lines 114-126 if ( $total_count == 0 ) { Link Here
114
    exit 0;
146
    exit 0;
115
}
147
}
116
148
117
# Process in batches to avoid memory issues
149
# Process CIRCULATION ISSUE logs
118
my $offset    = 0;
150
print "=== Processing CIRCULATION ISSUE logs ===\n\n" if $circ_count > 0;
151
119
my $converted = 0;
152
my $converted = 0;
120
my $skipped   = 0;
121
my $errors    = 0;
153
my $errors    = 0;
122
154
123
while ( $offset < $total_count ) {
155
my $offset = 0;
156
while ( $offset < $circ_count ) {
124
    my $select_sql = q(
157
    my $select_sql = q(
125
        SELECT action_id, info, object, timestamp
158
        SELECT action_id, info, object, timestamp
126
        FROM action_logs
159
        FROM action_logs
Lines 229-245 while ( $offset < $total_count ) { Link Here
229
    $offset += $batch_size;
262
    $offset += $batch_size;
230
263
231
    unless ($verbose) {
264
    unless ($verbose) {
232
        print "Processed $offset / $total_count records...\r";
265
        print "Processed $offset / $circ_count records...\r";
233
    }
266
    }
234
}
267
}
235
268
236
print "\n";
269
print "\n" unless $verbose;
237
print "Migration complete!\n";
270
print "Completed CIRCULATION ISSUE: $converted converted, $errors errors\n\n";
271
272
# Process HOLDS CREATE logs
273
print "=== Processing HOLDS CREATE logs ===\n\n" if $holds_count > 0;
274
275
my $holds_converted = 0;
276
my $holds_errors    = 0;
277
278
$offset = 0;
279
while ( $offset < $holds_count ) {
280
    my $select_sql = q(
281
        SELECT action_id, info, object, timestamp
282
        FROM action_logs
283
        WHERE module = 'HOLDS'
284
          AND action = 'CREATE'
285
          AND info IS NOT NULL
286
          AND info NOT LIKE '{%'
287
          AND info REGEXP '^[0-9]+$'
288
        ORDER BY action_id
289
        LIMIT ? OFFSET ?
290
    );
291
292
    my $sth = $dbh->prepare($select_sql);
293
    $sth->execute( $batch_size, $offset );
294
295
    my $batch_count = 0;
296
297
    while ( my $row = $sth->fetchrow_hashref ) {
298
        my $action_id = $row->{action_id};
299
        my $info      = $row->{info};
300
301
        # The info should be a hold_id
302
        my $hold_id = $info;
303
304
        # Try to find the corresponding hold to get full details
305
        my $branchcode;
306
        my $biblionumber;
307
        my $itemnumber;
308
309
        # First check old_reserves (most likely location for old logs)
310
        my $old_hold = Koha::Old::Holds->search(
311
            { reserve_id => $hold_id },
312
            { rows       => 1 }
313
        )->next;
314
315
        if ($old_hold) {
316
            $branchcode   = $old_hold->branchcode;
317
            $biblionumber = $old_hold->biblionumber;
318
            $itemnumber   = $old_hold->itemnumber;
319
        } else {
320
321
            # Try current reserves table (unlikely but possible)
322
            my $current_hold = Koha::Holds->search( { reserve_id => $hold_id } )->next;
323
324
            if ($current_hold) {
325
                $branchcode   = $current_hold->branchcode;
326
                $biblionumber = $current_hold->biblionumber;
327
                $itemnumber   = $current_hold->itemnumber;
328
            }
329
        }
330
331
        # Build the JSON structure
332
        my $json_data = {
333
            hold          => $hold_id,
334
            branchcode    => $branchcode,
335
            biblionumber  => $biblionumber,
336
            itemnumber    => $itemnumber,
337
            confirmations => [],
338
            forced        => []
339
        };
340
341
        my $json_info = encode_json($json_data);
342
343
        # Make it pretty like the code does
344
        $json_data = decode_json($json_info);
345
        $json_info = JSON->new->pretty(1)->canonical(1)->encode($json_data);
346
347
        if ($verbose) {
348
            print "Converting action_id $action_id:\n";
349
            print "  Old: $info\n";
350
            print "  New: " . ( $json_info =~ s/\n/ /gr ) . "\n";
351
            print "  hold_id: $hold_id\n";
352
            print "  branchcode: " .   ( $branchcode   // 'NULL' ) . "\n";
353
            print "  biblionumber: " . ( $biblionumber // 'NULL' ) . "\n";
354
            print "  itemnumber: " .   ( $itemnumber   // 'NULL' ) . "\n";
355
            print "\n";
356
        }
357
358
        if ($commit) {
359
            my $update_sql = q(
360
                UPDATE action_logs
361
                SET info = ?
362
                WHERE action_id = ?
363
            );
364
            my $update_sth = $dbh->prepare($update_sql);
365
            if ( $update_sth->execute( $json_info, $action_id ) ) {
366
                $holds_converted++;
367
            } else {
368
                print STDERR "ERROR: Failed to update action_id $action_id: " . $dbh->errstr . "\n";
369
                $holds_errors++;
370
            }
371
        } else {
372
            $holds_converted++;
373
        }
374
375
        $batch_count++;
376
    }
377
378
    $offset += $batch_size;
379
380
    unless ($verbose) {
381
        print "Processed $offset / $holds_count records...\r";
382
    }
383
}
384
385
print "\n" unless $verbose;
386
print "Completed HOLDS CREATE: $holds_converted converted, $holds_errors errors\n\n";
387
388
# Final summary
389
print "=== Migration Summary ===\n";
390
print "CIRCULATION ISSUE:\n";
238
print "  Converted: $converted\n";
391
print "  Converted: $converted\n";
239
print "  Errors: $errors\n";
392
print "  Errors: $errors\n";
393
print "HOLDS CREATE:\n";
394
print "  Converted: $holds_converted\n";
395
print "  Errors: $holds_errors\n";
396
print "TOTAL:\n";
397
print "  Converted: " . ( $converted + $holds_converted ) . "\n";
398
print "  Errors: " .    ( $errors + $holds_errors ) . "\n";
240
print "\n";
399
print "\n";
241
400
242
if ( !$commit && $converted > 0 ) {
401
if ( !$commit && ( $converted > 0 || $holds_converted > 0 ) ) {
243
    print "This was a DRY RUN. Use --commit to actually update the database.\n";
402
    print "This was a DRY RUN. Use --commit to actually update the database.\n";
244
}
403
}
245
404
(-)a/t/db_dependent/Reserves.t (-6 / +9 lines)
Lines 2007-2013 subtest 'CheckReserves() item type tests' => sub { Link Here
2007
};
2007
};
2008
2008
2009
subtest 'Bug 40866: AddReserve override JSON logging' => sub {
2009
subtest 'Bug 40866: AddReserve override JSON logging' => sub {
2010
    plan tests => 8;
2010
    plan tests => 11;
2011
2011
2012
    $schema->storage->txn_begin;
2012
    $schema->storage->txn_begin;
2013
2013
Lines 2032-2038 subtest 'Bug 40866: AddReserve override JSON logging' => sub { Link Here
2032
    # Clear any existing logs
2032
    # Clear any existing logs
2033
    Koha::ActionLogs->search( { module => 'HOLDS', action => 'CREATE' } )->delete;
2033
    Koha::ActionLogs->search( { module => 'HOLDS', action => 'CREATE' } )->delete;
2034
2034
2035
    # Test 1: Normal hold without overrides - should log hold ID only
2035
    # Test 1: Normal hold without overrides - should log JSON with empty arrays
2036
    my $hold_id = C4::Reserves::AddReserve(
2036
    my $hold_id = C4::Reserves::AddReserve(
2037
        {
2037
        {
2038
            branchcode     => $library->branchcode,
2038
            branchcode     => $library->branchcode,
Lines 2052-2059 subtest 'Bug 40866: AddReserve override JSON logging' => sub { Link Here
2052
        { order_by => { -desc => 'timestamp' } }
2052
        { order_by => { -desc => 'timestamp' } }
2053
    );
2053
    );
2054
    is( $logs->count, 1, 'One log entry created for normal hold' );
2054
    is( $logs->count, 1, 'One log entry created for normal hold' );
2055
    my $log = $logs->next;
2055
    my $log      = $logs->next;
2056
    is( $log->info, $hold_id, 'Normal hold logs hold ID only' );
2056
    my $log_data = eval { from_json( $log->info ) };
2057
    ok( !$@, 'Normal hold log info is valid JSON' );
2058
    is( $log_data->{hold}, $hold_id, 'JSON contains correct hold ID' );
2059
    is_deeply( $log_data->{confirmations}, [], 'Confirmations is empty array for normal hold' );
2060
    is_deeply( $log_data->{forced},        [], 'Forced is empty array for normal hold' );
2057
2061
2058
    # Cancel the hold for next test
2062
    # Cancel the hold for next test
2059
    my $hold = Koha::Holds->find($hold_id);
2063
    my $hold = Koha::Holds->find($hold_id);
Lines 2083-2089 subtest 'Bug 40866: AddReserve override JSON logging' => sub { Link Here
2083
    is( $logs->count, 1, 'One log entry created for override hold' );
2087
    is( $logs->count, 1, 'One log entry created for override hold' );
2084
    $log = $logs->next;
2088
    $log = $logs->next;
2085
2089
2086
    my $log_data = eval { from_json( $log->info ) };
2090
    $log_data = eval { from_json( $log->info ) };
2087
    ok( !$@,                               'Log info is valid JSON' );
2091
    ok( !$@,                               'Log info is valid JSON' );
2088
    ok( exists $log_data->{confirmations}, 'JSON contains confirmations array' );
2092
    ok( exists $log_data->{confirmations}, 'JSON contains confirmations array' );
2089
    is_deeply(
2093
    is_deeply(
2090
- 

Return to bug 41358