diff --git a/db/migrations/20260421120000_create_sizes_sizegroup_table.php b/db/migrations/20260421120000_create_sizes_sizegroup_table.php new file mode 100644 index 000000000..12b5e5f63 --- /dev/null +++ b/db/migrations/20260421120000_create_sizes_sizegroup_table.php @@ -0,0 +1,86 @@ +execute('CREATE TABLE `sizes_sizegroup` ( + `size_id` INT UNSIGNED NOT NULL, + `sizegroup_id` INT UNSIGNED NOT NULL, + `seq` INT DEFAULT NULL, + PRIMARY KEY (`size_id`, `sizegroup_id`), + KEY `idx_sizegroup_id` (`sizegroup_id`), + CONSTRAINT `fk_ss_size_id` + FOREIGN KEY (`size_id`) REFERENCES `sizes` (`id`) + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_ss_sizegroup_id` + FOREIGN KEY (`sizegroup_id`) REFERENCES `sizegroup` (`id`) + ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC'); + + // ---------------------------------------------------------------- + // 2. Populate from existing sizes.sizegroup_id (one row per size, + // including rows for the duplicates that DeduplicateSizes will + // later remove along with their sizes_sizegroup entries) + // ---------------------------------------------------------------- + $this->execute('INSERT INTO `sizes_sizegroup` (`size_id`, `sizegroup_id`, `seq`) + SELECT `id`, `sizegroup_id`, `seq` + FROM `sizes` + WHERE `sizegroup_id` IS NOT NULL'); + + // ---------------------------------------------------------------- + // 3. Drop the now-redundant columns from sizes + // ---------------------------------------------------------------- + $this->table('sizes')->dropForeignKey('sizegroup_id')->save(); + $this->table('sizes')->removeColumn('sizegroup_id')->save(); + $this->table('sizes')->removeColumn('seq')->save(); + } + + public function down(): void + { + // Restore sizegroup_id and seq columns on sizes + $this->execute('ALTER TABLE `sizes` + ADD COLUMN `sizegroup_id` INT UNSIGNED DEFAULT NULL AFTER `label`, + ADD COLUMN `seq` INT DEFAULT NULL AFTER `sizegroup_id`, + ADD KEY `sizegroup_id` (`sizegroup_id`), + ADD CONSTRAINT `sizes_ibfk_4` + FOREIGN KEY (`sizegroup_id`) REFERENCES `sizegroup` (`id`) + ON UPDATE CASCADE'); + + // Re-populate from the cross-reference table. + // Where a size belongs to multiple sizegroups, pick the row with the + // smallest sizegroup_id as the "primary" one (mirrors the original data). + $this->execute('UPDATE `sizes` s + JOIN ( + SELECT size_id, sizegroup_id, seq + FROM sizes_sizegroup ss1 + WHERE sizegroup_id = ( + SELECT MIN(sizegroup_id) + FROM sizes_sizegroup ss2 + WHERE ss2.size_id = ss1.size_id + ) + ) best ON best.size_id = s.id + SET s.sizegroup_id = best.sizegroup_id, + s.seq = best.seq'); + + $this->execute('DROP TABLE `sizes_sizegroup`'); + } +} diff --git a/db/migrations/20260421130000_deduplicate_sizes.php b/db/migrations/20260421130000_deduplicate_sizes.php new file mode 100644 index 000000000..020f56ce2 --- /dev/null +++ b/db/migrations/20260421130000_deduplicate_sizes.php @@ -0,0 +1,314 @@ +execute("UPDATE stock + SET size_id = {$case} + WHERE size_id IN ({$ids})"); + + // --- shipment_detail --------------------------------------------- + $sourceCaseExpr = str_replace('size_id', 'source_size_id', $case); + $targetCaseExpr = str_replace('size_id', 'target_size_id', $case); + + $this->execute("UPDATE shipment_detail + SET source_size_id = {$sourceCaseExpr} + WHERE source_size_id IN ({$ids})"); + + $this->execute("UPDATE shipment_detail + SET target_size_id = {$targetCaseExpr} + WHERE target_size_id IN ({$ids})"); + + // --- history (size_id changes on stock rows) --------------------- + $fromCaseExpr = str_replace('size_id', 'from_int', $case); + $toCaseExpr = str_replace('size_id', 'to_int', $case); + + $this->execute("UPDATE history + SET from_int = {$fromCaseExpr} + WHERE tablename = 'stock' AND changes = 'size_id' + AND from_int IN ({$ids})"); + + $this->execute("UPDATE history + SET to_int = {$toCaseExpr} + WHERE tablename = 'stock' AND changes = 'size_id' + AND to_int IN ({$ids})"); + + // --- itemsout ---------------------------------------------------- + if ($this->hasTable('itemsout')) { + $this->execute("UPDATE itemsout + SET size_id = {$case} + WHERE size_id IN ({$ids})"); + } + + // --- distro_events_* tables with size_id ------------------------- + foreach (['distro_events_packing_list_entries', 'distro_events_tracking_logs', 'distro_events_unboxed_item_collections'] as $dTable) { + if ($this->hasTable($dTable)) { + $this->execute("UPDATE `{$dTable}` + SET size_id = {$case} + WHERE size_id IN ({$ids})"); + } + } + + // --- sizes_sizegroup --------------------------------------------- + // Remap cross-reference rows: canonical sizes inherit all sizegroup + // memberships from their duplicates. INSERT IGNORE handles the rare + // case where the canonical already belongs to the same sizegroup. + // The ON DELETE CASCADE on fk_ss_size_id then removes the old rows + // for the duplicate size IDs when they are deleted below. + $ssCaseExpr = str_replace('size_id', 'ss.size_id', $case); + $this->execute("INSERT IGNORE INTO `sizes_sizegroup` (`size_id`, `sizegroup_id`, `seq`) + SELECT {$ssCaseExpr}, ss.`sizegroup_id`, ss.`seq` + FROM `sizes_sizegroup` ss + WHERE ss.`size_id` IN ({$ids})"); + + // --- delete duplicate sizes (CASCADE removes their sizes_sizegroup rows) --- + $this->execute("DELETE FROM sizes WHERE id IN ({$ids})"); + } + + public function down(): void + { + // Re-insert the duplicate size rows that were removed by up(). + // Note: sizegroup_id and seq columns no longer exist on sizes (they were + // dropped by the preceding CreateSizesSizegroupTable migration), so only + // id and label are restored here. Foreign-key references in stock etc. + // that were remapped cannot be reconstructed. + $this->execute("INSERT IGNORE INTO sizes (id, label) VALUES + (53,'S'), + (54,'M'), + (55,'L'), + (57,'39'), + (58,'40'), + (59,'41'), + (65,'35'), + (67,'34'), + (70,'Mixed'), + (71,'Mixed'), + (103,'All ages'), + (120,'7-12 months'), + (121,'13-18 months'), + (122,'19-24 months'), + (123,'All ages'), + (124,'6-10 years'), + (125,'11-15 years'), + (130,'2-5 years'), + (138,'0-6 months'), + (140,'All ages'), + (141,'2-3 years'), + (142,'4-5 years'), + (148,'All ages'), + (163,'All ages'), + (171,'24'), + (172,'25'), + (173,'26'), + (174,'27'), + (175,'28'), + (176,'29'), + (177,'30'), + (178,'31'), + (179,'32'), + (180,'33'), + (181,'34'), + (182,'35'), + (183,'36'), + (184,'37'), + (185,'38'), + (186,'39'), + (187,'40'), + (188,'41'), + (189,'42'), + (190,'43'), + (191,'44'), + (192,'45'), + (204,'Mixed'), + (205,'Mixed'), + (206,'Mixed'), + (207,'Mixed'), + (208,'Mixed'), + (209,'Mixed'), + (210,'Mixed'), + (211,'Mixed'), + (212,'Mixed'), + (213,'Mixed'), + (214,'Mixed'), + (215,'Mixed'), + (216,'Mixed'), + (217,'Mixed'), + (218,'Mixed'), + (219,'Mixed'), + (220,'Mixed'), + (221,'Mixed'), + (222,'Mixed'), + (223,'Mixed')"); + + // Re-insert the sizes_sizegroup rows for the restored sizes + // (one row per size, matching the original sizes.sizegroup_id / sizes.seq values + // from init.sql that were inserted by CreateSizesSizegroupTable) + $this->execute('INSERT IGNORE INTO sizes_sizegroup (size_id, sizegroup_id, seq) VALUES + (53, 5, 50), + (54, 5, 51), + (55, 5, 52), + (57, 8, 81), + (58, 8, 82), + (59, 8, 83), + (65, 9, 118), + (67, 9, 117), + (70, 5, 53), + (71, 1, 6), + (103, 4, 205), + (120, 2, 41), + (121, 2, 42), + (122, 2, 43), + (123, 2, 44), + (124, 18, 20), + (125, 18, 21), + (130, 20, 30), + (138, 22, 180), + (140, 4, 182), + (141, 23, 200), + (142, 23, 201), + (148, 23, 204), + (163, 24, 234), + (171, 26, 251), + (172, 26, 252), + (173, 26, 253), + (174, 26, 254), + (175, 26, 255), + (176, 26, 256), + (177, 26, 257), + (178, 26, 258), + (179, 26, 259), + (180, 26, 260), + (181, 26, 261), + (182, 26, 262), + (183, 26, 263), + (184, 26, 264), + (185, 26, 265), + (186, 26, 266), + (187, 26, 267), + (188, 26, 268), + (189, 26, 269), + (190, 26, 270), + (191, 26, 271), + (192, 26, 272), + (204, 2, 999), + (205, 3, 999), + (206, 4, 999), + (207, 7, 999), + (208, 8, 999), + (209, 9, 999), + (210, 12, 999), + (211, 13, 999), + (212, 16, 999), + (213, 17, 999), + (214, 18, 999), + (215, 19, 999), + (216, 20, 999), + (217, 21, 999), + (218, 22, 999), + (219, 23, 999), + (220, 24, 999), + (221, 25, 999), + (222, 26, 999), + (223, 27, 999)'); + + // Remove the sizegroup memberships that canonical sizes inherited + // from their duplicates during up(). Each pair below is + // (canonical_size_id, sizegroup_id_of_the_duplicate). + $this->execute('DELETE FROM `sizes_sizegroup` + WHERE (`size_id`, `sizegroup_id`) IN ( + ( 1, 5), ( 2, 5), ( 3, 5), + (33, 8), (34, 8), (35, 8), + (28, 9), (29, 9), + (52, 1), (52, 5), + (97, 2), (97, 4), (97, 23), (97, 24), + (44, 2), (47, 2), (48, 2), + (117, 18), (118, 18), + (116, 20), + (119, 22), + (10, 23), (11, 23), + (18, 26), (19, 26), (20, 26), (21, 26), (22, 26), (23, 26), + (24, 26), (25, 26), (26, 26), (27, 26), + (28, 26), (29, 26), (30, 26), (31, 26), (32, 26), + (33, 26), (34, 26), (35, 26), + (60, 26), (61, 26), (62, 26), (63, 26), + (52, 2), (52, 3), (52, 4), (52, 7), (52, 8), (52, 9), + (52, 12), (52, 13), (52, 16), (52, 17), (52, 18), (52, 19), + (52, 20), (52, 21), (52, 22), (52, 23), (52, 24), (52, 25), + (52, 26), (52, 27) + )'); + } +} diff --git a/include/container-stock.php b/include/container-stock.php index d6201c442..04374588f 100644 --- a/include/container-stock.php +++ b/include/container-stock.php @@ -28,8 +28,9 @@ s3.box_state_id = 1 AND l2.camp_id='.$_SESSION['camp']['id'].')-IFNULL(COUNT(s2.id),0) AS totalboxes FROM - (products AS p, - sizes AS s) + products AS p + INNER JOIN sizes_sizegroup AS ss ON ss.sizegroup_id = p.sizegroup_id + INNER JOIN sizes AS s ON ss.size_id = s.id LEFT OUTER JOIN genders AS g ON p.gender_id = g.id LEFT OUTER JOIN stock AS s2 @@ -40,7 +41,6 @@ s2.location_id IN ('.$container_stock_locations.') WHERE (NOT p.deleted OR p.deleted IS NULL) AND - s.sizegroup_id = p.sizegroup_id AND p.camp_id = '.$_SESSION['camp']['id'].' '.($_SESSION['search']['container-stock'] ? 'AND p.name LIKE "%'.$_SESSION['search']['container-stock'].'%"' : '').' GROUP BY diff --git a/include/stock_edit.php b/include/stock_edit.php index 50890de0e..db2b47607 100644 --- a/include/stock_edit.php +++ b/include/stock_edit.php @@ -206,7 +206,7 @@ addfield('select', 'Product', 'product_id', ['disabled' => $disabled, 'test_id' => 'product_id', 'required' => true, 'multiple' => false, 'query' => 'SELECT p.id AS value, CONCAT(p.name, " " ,IFNULL(g.label,"")) AS label FROM products AS p LEFT OUTER JOIN genders AS g ON p.gender_id = g.id WHERE (NOT p.deleted OR p.deleted IS NULL) AND p.camp_id = '.$_SESSION['camp']['id'].($_SESSION['camp']['separateshopandwhproducts'] ? ' AND NOT p.stockincontainer' : '').' ORDER BY name', 'onchange' => 'getSizes()']); -addfield('select', 'Size', 'size_id', ['disabled' => $disabled, 'required' => true, 'width' => 2, 'multiple' => false, 'query' => 'SELECT *, id AS value FROM sizes WHERE sizegroup_id = '.intval(db_value('SELECT sizegroup_id FROM products WHERE id = :id', ['id' => $data['product_id']])).' ORDER BY seq', 'tooltip' => 'If the right size for your box is not here, don\'t put it in comments, but first double check if you have the right product. For example: Long sleeves for babies, we call them tops.']); +addfield('select', 'Size', 'size_id', ['disabled' => $disabled, 'required' => true, 'width' => 2, 'multiple' => false, 'query' => 'SELECT s.id, s.label, s.id AS value FROM sizes AS s JOIN sizes_sizegroup AS ss ON ss.size_id = s.id WHERE ss.sizegroup_id = '.intval(db_value('SELECT sizegroup_id FROM products WHERE id = :id', ['id' => $data['product_id']])).' ORDER BY ss.seq', 'tooltip' => 'If the right size for your box is not here, don\'t put it in comments, but first double check if you have the right product. For example: Long sleeves for babies, we call them tops.']); addfield('number', 'Items', 'items', ['testid' => 'items_id', 'readonly' => $disabled]); diff --git a/library/ajax/getsizes.php b/library/ajax/getsizes.php index aae50f600..97327f99e 100644 --- a/library/ajax/getsizes.php +++ b/library/ajax/getsizes.php @@ -4,7 +4,7 @@ $product_id = $_POST['product_id']; $size_id = intval($_GET['size']); -$result = db_array('SELECT s.* FROM sizes AS s, sizegroup AS sg, products AS p WHERE s.sizegroup_id = sg.id AND p.id = :id AND p.sizegroup_id = sg.id ORDER BY s.seq', ['id' => $product_id]); +$result = db_array('SELECT s.id, s.label FROM sizes AS s JOIN sizes_sizegroup AS ss ON ss.size_id = s.id JOIN products AS p ON p.sizegroup_id = ss.sizegroup_id AND p.id = :id ORDER BY ss.seq', ['id' => $product_id]); $html = ''; if (1 == count($result)) {