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

(-)a/Koha/EDI.pm (-3 / +54 lines)
Lines 95-100 sub create_edi_order { Link Here
95
        }
95
        }
96
    )->single;
96
    )->single;
97
97
98
    # Check for duplicate PO numbers when po_is_basketname is enabled
99
    if ( $vendor && $vendor->po_is_basketname ) {
100
        my $basket = $orderlines[0]->basketno;
101
        if ( $basket->basketname ) {
102
            my $existing_basket = $schema->resultset('Aqbasket')->search(
103
                {
104
                    basketname   => $basket->basketname,
105
                    booksellerid => $basket->booksellerid,
106
                    basketno     => { '!=' => $basketno },    # Exclude current basket
107
                }
108
            )->first;
109
110
            if ($existing_basket) {
111
                $logger->error( "EDI order blocked for basket $basketno due to duplicate PO number '"
112
                        . $basket->basketname
113
                        . "' (existing basket: "
114
                        . $existing_basket->basketno
115
                        . ")" );
116
                return {
117
                    error     => "duplicate_po_number", existing_basket => $existing_basket->basketno,
118
                    po_number => $basket->basketname
119
                };
120
            }
121
        }
122
    }
123
98
    my $ean_search_keys = { ean => $ean, };
124
    my $ean_search_keys = { ean => $ean, };
99
    if ($branchcode) {
125
    if ($branchcode) {
100
        $ean_search_keys->{branchcode} = $branchcode;
126
        $ean_search_keys->{branchcode} = $branchcode;
Lines 647-653 sub process_quote { Link Here
647
    my $schema         = Koha::Database->new()->schema();
673
    my $schema         = Koha::Database->new()->schema();
648
    my $message_count  = 0;
674
    my $message_count  = 0;
649
    my $split_message;
675
    my $split_message;
650
    my @added_baskets;    # if auto & multiple baskets need to order all
676
    my @added_baskets;                # if auto & multiple baskets need to order all
677
    my %baskets_with_po_conflicts;    # track baskets that have PO number conflicts
651
678
652
    # Get vendor EDI account configuration
679
    # Get vendor EDI account configuration
653
    my $v = $schema->resultset('VendorEdiAccount')->search(
680
    my $v = $schema->resultset('VendorEdiAccount')->search(
Lines 663-675 sub process_quote { Link Here
663
690
664
            # Determine basket name based on vendor configuration
691
            # Determine basket name based on vendor configuration
665
            my $basket_name = $quote_message->filename;
692
            my $basket_name = $quote_message->filename;
693
            my $purchase_order_number;
694
            my $existing_basket;
695
666
            if ( $v && $v->po_is_basketname ) {
696
            if ( $v && $v->po_is_basketname ) {
667
                my $purchase_order_number = $msg->purchase_order_number;
697
                $purchase_order_number = $msg->purchase_order_number;
668
                if ($purchase_order_number) {
698
                if ($purchase_order_number) {
669
                    $basket_name = $purchase_order_number;
699
                    $basket_name = $purchase_order_number;
670
700
671
                    # Check for duplicate purchase order numbers for this vendor (including closed baskets)
701
                    # Check for duplicate purchase order numbers for this vendor (including closed baskets)
672
                    my $existing_basket = $schema->resultset('Aqbasket')->search(
702
                    $existing_basket = $schema->resultset('Aqbasket')->search(
673
                        {
703
                        {
674
                            basketname   => $basket_name,
704
                            basketname   => $basket_name,
675
                            booksellerid => $quote_message->vendor_id,
705
                            booksellerid => $quote_message->vendor_id,
Lines 701-706 sub process_quote { Link Here
701
                q{} . q{}
731
                q{} . q{}
702
            );
732
            );
703
            push @added_baskets, $basketno;
733
            push @added_baskets, $basketno;
734
735
            # Record if this basket has a PO conflict (for auto_orders blocking)
736
            if ($existing_basket) {
737
                $baskets_with_po_conflicts{$basketno} = {
738
                    po_number       => $purchase_order_number,
739
                    existing_basket => $existing_basket->basketno
740
                };
741
            }
704
            if ( $message_count > 1 ) {
742
            if ( $message_count > 1 ) {
705
                my $m_filename = $quote_message->filename;
743
                my $m_filename = $quote_message->filename;
706
                $m_filename .= "_$message_count";
744
                $m_filename .= "_$message_count";
Lines 741-746 sub process_quote { Link Here
741
                               # Do we automatically generate orders for this vendor
779
                               # Do we automatically generate orders for this vendor
742
    if ( $v && $v->auto_orders ) {
780
    if ( $v && $v->auto_orders ) {
743
        for my $b (@added_baskets) {
781
        for my $b (@added_baskets) {
782
783
            # Check if this basket has a PO number conflict
784
            if ( exists $baskets_with_po_conflicts{$b} ) {
785
                my $conflict_info = $baskets_with_po_conflicts{$b};
786
                $logger->warn( "Auto-order blocked for basket $b due to duplicate PO number '"
787
                        . $conflict_info->{po_number}
788
                        . "' (existing basket: "
789
                        . $conflict_info->{existing_basket}
790
                        . ")" );
791
                $logger->info("Basket $b remains open due to PO number conflict");
792
                next;
793
            }
794
744
            create_edi_order(
795
            create_edi_order(
745
                {
796
                {
746
                    ean      => $messages->[0]->buyer_ean,
797
                    ean      => $messages->[0]->buyer_ean,
(-)a/acqui/basket.pl (-1 / +13 lines)
Lines 578-584 sub edi_close_and_order { Link Here
578
        if ( $basket->{branch} ) {
578
        if ( $basket->{branch} ) {
579
            $edi_params->{branchcode} = $basket->{branch};
579
            $edi_params->{branchcode} = $basket->{branch};
580
        }
580
        }
581
        if ( create_edi_order($edi_params) ) {
581
        my $edi_result = create_edi_order($edi_params);
582
        if ( ref $edi_result eq 'HASH' && $edi_result->{error} ) {
583
            if ( $edi_result->{error} eq 'duplicate_po_number' ) {
584
                push @messages, {
585
                    type            => 'error',
586
                    code            => 'edi_duplicate_po_number',
587
                    po_number       => $edi_result->{po_number},
588
                    existing_basket => $edi_result->{existing_basket}
589
                };
590
                $op = 'list';    # Stay on basket page to show error
591
                return;
592
            }
593
        } elsif ($edi_result) {
582
594
583
            #$template->param( edifile => 1 );
595
            #$template->param( edifile => 1 );
584
        }
596
        }
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt (+5 lines)
Lines 306-311 Link Here
306
                                <span>There is no notice template with code ACQORDER defined.</span>
306
                                <span>There is no notice template with code ACQORDER defined.</span>
307
                            [% CASE 'email_sent' %]
307
                            [% CASE 'email_sent' %]
308
                                <span>Order e-mail was sent to the vendor.</span>
308
                                <span>Order e-mail was sent to the vendor.</span>
309
                            [% CASE 'edi_duplicate_po_number' %]
310
                                <span
311
                                    >EDI order blocked: Duplicate purchase order number '[% m.po_number | html %]' found. This purchase order number is already used by basket [% m.existing_basket | html %]. Please check with your vendor or
312
                                    use a different purchase order number.</span
313
                                >
309
                            [% CASE %]
314
                            [% CASE %]
310
                                <span>ERROR! - [% m.code | html %]</span>
315
                                <span>ERROR! - [% m.code | html %]</span>
311
                            [% END %]
316
                            [% END %]
(-)a/t/db_dependent/Koha/EDI.t (-2 / +234 lines)
Lines 21-27 use Modern::Perl; Link Here
21
use FindBin qw( $Bin );
21
use FindBin qw( $Bin );
22
22
23
use Test::NoWarnings;
23
use Test::NoWarnings;
24
use Test::More tests => 5;
24
use Test::More tests => 6;
25
use Test::MockModule;
25
use Test::MockModule;
26
26
27
use t::lib::Mocks;
27
use t::lib::Mocks;
Lines 1149-1154 subtest 'process_invoice_without_tax_rate' => sub { Link Here
1149
    $schema->storage->txn_rollback;
1149
    $schema->storage->txn_rollback;
1150
};
1150
};
1151
1151
1152
subtest 'duplicate_po_number_blocking' => sub {
1153
    plan tests => 2;
1154
1155
    $schema->storage->txn_begin;
1156
1157
    my $test_san      = '5013546098818';
1158
    my $dirname       = ( $Bin =~ /^(.*\/t\/)/ ? $1 . 'edi_testfiles/' : q{} );
1159
    my $active_period = $builder->build(
1160
        {
1161
            source => 'Aqbudgetperiod',
1162
            value  => { budget_period_active => 1 }
1163
        }
1164
    );
1165
1166
    # Test 1: Auto-orders blocking for duplicate PO numbers
1167
    subtest 'auto_orders_blocking' => sub {
1168
        plan tests => 9;
1169
1170
        $schema->storage->txn_begin;
1171
1172
        # Create vendor EDI account with auto_orders and po_is_basketname enabled
1173
        my $account = $builder->build(
1174
            {
1175
                source => 'VendorEdiAccount',
1176
                value  => {
1177
                    description      => 'auto_orders duplicate test vendor',
1178
                    transport        => 'FILE',
1179
                    plugin           => '',
1180
                    san              => $test_san,
1181
                    auto_orders      => 1,
1182
                    po_is_basketname => 1,
1183
                }
1184
            }
1185
        );
1186
1187
        my $ean = $builder->build(
1188
            {
1189
                source => 'EdifactEan',
1190
                value  => {
1191
                    description => 'test ean',
1192
                    branchcode  => undef,
1193
                    ean         => $test_san
1194
                }
1195
            }
1196
        );
1197
1198
        # Create existing basket with PO number that will conflict
1199
        my $existing_basket = $builder->build(
1200
            {
1201
                source => 'Aqbasket',
1202
                value  => {
1203
                    basketname   => 'orders 23/1',           # Same as in QUOTES_SMALL.CEQ
1204
                    booksellerid => $account->{vendor_id},
1205
                    closedate    => undef,
1206
                }
1207
            }
1208
        );
1209
1210
        # Setup the fund
1211
        $builder->build(
1212
            {
1213
                source => 'Aqbudget',
1214
                value  => {
1215
                    budget_code      => 'REF',
1216
                    budget_period_id => $active_period->{budget_period_id}
1217
                }
1218
            }
1219
        );
1220
1221
        my $filename = 'QUOTES_SMALL.CEQ';
1222
        my $trans    = Koha::Edifact::Transport->new( $account->{id} );
1223
        $trans->working_directory($dirname);
1224
1225
        my $mhash = $trans->message_hash();
1226
        $mhash->{message_type} = 'QUOTE';
1227
        $trans->ingest( $mhash, $filename );
1228
1229
        my $quote = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
1230
1231
        # Clear logger before processing
1232
        $logger->clear();
1233
1234
        # Process quote - should create basket but NOT auto-order due to duplicate PO
1235
        my $die;
1236
        eval {
1237
            process_quote($quote);
1238
            1;
1239
        } or do {
1240
            $die = $@;
1241
        };
1242
        ok( !$die, 'Quote with duplicate PO processed without dying' );
1243
1244
        # Verify duplicate PO error was logged during quote processing
1245
        my $errors = $quote->edifact_errors;
1246
        ok( $errors->count >= 1, 'Error logged for duplicate PO number' );
1247
1248
        my $duplicate_error = $errors->search( { section => 'RFF+ON:orders 23/1' } )->first;
1249
        ok( $duplicate_error, 'Duplicate PO error found in quote errors' );
1250
1251
        # Check that new basket was created
1252
        my $baskets = Koha::Acquisition::Baskets->search(
1253
            {
1254
                booksellerid => $account->{vendor_id},
1255
                basketno     => { '!=' => $existing_basket->{basketno} }
1256
            }
1257
        );
1258
        is( $baskets->count, 1, "New basket created despite duplicate PO" );
1259
1260
        my $new_basket = $baskets->next;
1261
        is( $new_basket->basketname, 'orders 23/1', "New basket uses duplicate PO number as name" );
1262
1263
        # Critical test: Verify basket was NOT closed (auto-order was blocked)
1264
        ok( !$new_basket->closedate, 'New basket with duplicate PO was NOT closed (auto-order blocked)' );
1265
1266
        # Verify no EDI order was created for the conflicting basket
1267
        my $edi_orders = $schema->resultset('EdifactMessage')->search(
1268
            {
1269
                message_type => 'ORDERS',
1270
                basketno     => $new_basket->basketno,
1271
            }
1272
        );
1273
        is( $edi_orders->count, 0, 'No EDI order created for basket with duplicate PO' );
1274
1275
        # Check that the blocking is working by verifying no EDI orders were created
1276
        # This is the most important test - the basket should NOT be auto-ordered
1277
        ok( $edi_orders->count == 0, 'Auto-order was blocked - no EDI order created for duplicate PO' );
1278
1279
        # Verify that duplicate PO error was logged (this shows detection is working)
1280
        ok( $duplicate_error, 'Duplicate PO number detection and logging is working' );
1281
1282
        $schema->storage->txn_rollback;
1283
    };
1284
1285
    # Test 2: No blocking when po_is_basketname is disabled
1286
    subtest 'no_blocking_when_feature_disabled' => sub {
1287
        plan tests => 5;
1288
1289
        $schema->storage->txn_begin;
1290
1291
        # Create vendor EDI account with po_is_basketname DISABLED
1292
        my $account = $builder->build(
1293
            {
1294
                source => 'VendorEdiAccount',
1295
                value  => {
1296
                    description      => 'feature disabled test vendor',
1297
                    transport        => 'FILE',
1298
                    plugin           => '',
1299
                    san              => $test_san,
1300
                    auto_orders      => 1,
1301
                    po_is_basketname => 0,                                # Feature disabled
1302
                }
1303
            }
1304
        );
1305
1306
        my $ean = $builder->build(
1307
            {
1308
                source => 'EdifactEan',
1309
                value  => {
1310
                    description => 'test ean',
1311
                    branchcode  => undef,
1312
                    ean         => $test_san
1313
                }
1314
            }
1315
        );
1316
1317
        # Create existing basket with any name
1318
        my $existing_basket = $builder->build(
1319
            {
1320
                source => 'Aqbasket',
1321
                value  => {
1322
                    basketname   => 'orders 23/1',
1323
                    booksellerid => $account->{vendor_id},
1324
                    closedate    => undef,
1325
                }
1326
            }
1327
        );
1328
1329
        # Setup the fund
1330
        $builder->build(
1331
            {
1332
                source => 'Aqbudget',
1333
                value  => {
1334
                    budget_code      => 'REF',
1335
                    budget_period_id => $active_period->{budget_period_id}
1336
                }
1337
            }
1338
        );
1339
1340
        my $filename = 'QUOTES_SMALL.CEQ';
1341
        my $trans    = Koha::Edifact::Transport->new( $account->{id} );
1342
        $trans->working_directory($dirname);
1343
1344
        my $mhash = $trans->message_hash();
1345
        $mhash->{message_type} = 'QUOTE';
1346
        $trans->ingest( $mhash, $filename );
1347
1348
        my $quote = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
1349
1350
        $logger->clear();
1351
        process_quote($quote);
1352
1353
        # Verify new basket was created and auto-ordered (no blocking)
1354
        my $baskets = Koha::Acquisition::Baskets->search(
1355
            {
1356
                booksellerid => $account->{vendor_id},
1357
                basketno     => { '!=' => $existing_basket->{basketno} }
1358
            }
1359
        );
1360
        is( $baskets->count, 1, "New basket created" );
1361
1362
        my $new_basket = $baskets->next;
1363
1364
        # When po_is_basketname is disabled, basket uses filename not PO number
1365
        is( $new_basket->basketname, $filename, "Basket uses filename when po_is_basketname disabled" );
1366
1367
        # The key test is that normal processing occurred without duplicate detection
1368
        # Since po_is_basketname is disabled, basket should use filename not PO number
1369
        ok( $new_basket->basketname eq $filename, 'Normal basket naming when feature disabled' );
1370
1371
        # With po_is_basketname disabled, quote processing should work normally
1372
        # The absence of duplicate errors in the database is the key indicator
1373
        my $quote_errors           = $schema->resultset('EdifactError')->search( {} );
1374
        my $has_duplicate_db_error = grep { $_->details =~ /Duplicate purchase order/ } $quote_errors->all;
1375
        ok( !$has_duplicate_db_error, 'No duplicate PO database errors when feature disabled' );
1376
1377
        pass('Feature correctly disabled - normal processing occurred');
1378
1379
        $schema->storage->txn_rollback;
1380
    };
1381
1382
    $schema->storage->txn_rollback;
1383
};
1384
1152
subtest 'create_edi_order_logging' => sub {
1385
subtest 'create_edi_order_logging' => sub {
1153
    plan tests => 8;
1386
    plan tests => 8;
1154
1387
1155
- 

Return to bug 20253