summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLester Caine <lester@lsces.co.uk>2026-06-18 19:13:27 +0100
committerLester Caine <lester@lsces.co.uk>2026-06-18 19:13:27 +0100
commit88eed73b339ec4588da6aaa0851262aa65e0a83f (patch)
treea1b8e2fb6c312046e39ec2e28fb67662476631e4
parent894b67457d9272b3726790fa103332bf250d2a4b (diff)
downloadstock-88eed73b339ec4588da6aaa0851262aa65e0a83f.tar.gz
stock-88eed73b339ec4588da6aaa0851262aa65e0a83f.tar.bz2
stock-88eed73b339ec4588da6aaa0851262aa65e0a83f.zip
Add MERG group BOM stub importer
Reads merg_group_<X>.csv from storage/stock/, matches Rapid order codes against existing #SUP xrefs, and creates placeholder components (RO<code>) for any not yet in the system. Supports dry=1 preview and update=1 to backfill descriptions on existing stubs still carrying RO* titles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--import/load_merg_stubs.php149
-rw-r--r--templates/load_merg_stubs.tpl47
2 files changed, 196 insertions, 0 deletions
diff --git a/import/load_merg_stubs.php b/import/load_merg_stubs.php
new file mode 100644
index 0000000..7ed6ab1
--- /dev/null
+++ b/import/load_merg_stubs.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * Create stub components for Rapid Online parts not yet in the system.
+ *
+ * Reads a MERG group CSV (merg_group_<X>.csv in storage/stock/), finds every row
+ * with Source='R' and a non-empty order code, checks whether a #SUP xref for that
+ * Rapid code already exists, and creates a stub component titled RO<code> for any
+ * that are missing.
+ *
+ * Usage: load_merg_stubs.php?group=E
+ * Append &dry=1 to preview without writing anything.
+ *
+ * @package stock
+ */
+
+namespace Bitweaver\Stock;
+
+require_once '../../kernel/includes/setup_inc.php';
+
+global $gBitSystem, $gBitDb;
+
+$gBitSystem->verifyPackage( 'stock' );
+$gBitSystem->verifyPermission( 'p_stock_admin' );
+
+const RAPID_CONTENT_ID = 109;
+
+$group = preg_replace( '/[^A-Za-z]/', '', $_REQUEST['group'] ?? '' );
+$dryRun = !empty( $_REQUEST['dry'] );
+$doUpdate = !empty( $_REQUEST['update'] );
+
+$csvFile = STOCK_IMPORT_PATH . 'merg_group_' . strtoupper( $group ) . '.csv';
+
+$created = 0;
+$skipped = 0;
+$errors = [];
+$rows_out = [];
+
+if( empty( $group ) ) {
+ $errors[] = 'No group specified — append ?group=A through ?group=E to the URL.';
+} elseif( !file_exists( $csvFile ) ) {
+ $errors[] = 'CSV not found: ' . $csvFile;
+} else {
+ $handle = fopen( $csvFile, 'r' );
+ if( $handle === false ) {
+ $errors[] = 'Cannot open: ' . $csvFile;
+ } else {
+ $rowNum = 0;
+ while( ( $data = fgetcsv( $handle, 2000, ',', '"', '' ) ) !== false ) {
+ $rowNum++;
+ if( $rowNum <= 2 ) {
+ continue; // title + header rows
+ }
+
+ $component = trim( $data[0] ?? '' );
+ $orderCode = trim( $data[1] ?? '' );
+ $source = strtoupper( trim( $data[2] ?? '' ) );
+
+ // Skip legend rows (empty component), non-Rapid, or no order code
+ if( empty( $component ) || $source !== 'R' || empty( $orderCode ) ) {
+ continue;
+ }
+
+ // Check for existing #SUP xref for this Rapid code
+ $existingId = $gBitDb->getOne(
+ "SELECT `content_id` FROM `".BIT_DB_PREFIX."liberty_xref`
+ WHERE `item` = '#SUP' AND `xref` = ? AND `xkey` = ?",
+ [ RAPID_CONTENT_ID, $orderCode ]
+ );
+
+ if( $existingId ) {
+ // Update mode — backfill description on existing RO* stubs
+ if( $doUpdate ) {
+ $stubTitle = $gBitDb->getOne(
+ "SELECT `title` FROM `".BIT_DB_PREFIX."liberty_content` WHERE `content_id` = ?",
+ [ $existingId ]
+ );
+ if( strncmp( (string)$stubTitle, 'RO', 2 ) === 0 ) {
+ if( !$dryRun ) {
+ $stockComponent = new StockComponent( (int)$existingId );
+ $stockComponent->load();
+ $pHash = [
+ 'title' => $stubTitle,
+ 'edit' => $component,
+ 'format_guid' => 'bithtml',
+ ];
+ $stockComponent->store( $pHash );
+ }
+ $created++;
+ $rows_out[] = [ 'code' => $orderCode, 'component' => $component, 'status' => $dryRun ? 'would update' : 'updated', 'content_id' => $existingId ];
+ } else {
+ $skipped++;
+ $rows_out[] = [ 'code' => $orderCode, 'component' => $component, 'status' => 'skip (renamed)', 'content_id' => $existingId ];
+ }
+ } else {
+ $skipped++;
+ $rows_out[] = [ 'code' => $orderCode, 'component' => $component, 'status' => 'exists', 'content_id' => $existingId ];
+ }
+ continue;
+ }
+
+ $stubTitle = 'RO' . $orderCode;
+
+ if( !$dryRun ) {
+ $stockComponent = new StockComponent();
+ $pHash = [
+ 'title' => $stubTitle,
+ 'edit' => $component,
+ 'format_guid' => 'bithtml',
+ ];
+ if( !$stockComponent->store( $pHash ) ) {
+ $errors[] = "Row $rowNum ($orderCode): failed to create component.";
+ $rows_out[] = [ 'code' => $orderCode, 'component' => $component, 'status' => 'error', 'content_id' => null ];
+ continue;
+ }
+
+ $newContentId = $stockComponent->mContentId;
+
+ $gBitDb->associateInsert( BIT_DB_PREFIX.'liberty_xref', [
+ 'xref_id' => $gBitDb->GenID( 'liberty_xref_seq' ),
+ 'content_id' => $newContentId,
+ 'item' => '#SUP',
+ 'xorder' => 1,
+ 'xref' => RAPID_CONTENT_ID,
+ 'xkey' => substr( $orderCode, 0, 32 ),
+ 'last_update_date' => $gBitDb->NOW(),
+ ] );
+
+ $created++;
+ $rows_out[] = [ 'code' => $orderCode, 'component' => $component, 'status' => 'created', 'content_id' => $newContentId ];
+ } else {
+ $rows_out[] = [ 'code' => $orderCode, 'component' => $component, 'status' => 'would create', 'content_id' => null ];
+ $created++;
+ }
+ }
+ fclose( $handle );
+ }
+}
+
+global $gBitSmarty;
+$gBitSmarty->assign( 'group', $group );
+$gBitSmarty->assign( 'dryRun', $dryRun );
+$gBitSmarty->assign( 'doUpdate', $doUpdate );
+$gBitSmarty->assign( 'csvFile', $csvFile );
+$gBitSmarty->assign( 'created', $created );
+$gBitSmarty->assign( 'skipped', $skipped );
+$gBitSmarty->assign( 'errors', $errors );
+$gBitSmarty->assign( 'rows', $rows_out );
+
+$gBitSystem->display( 'bitpackage:stock/load_merg_stubs.tpl', 'MERG Stub Import — Group ' . strtoupper( $group ) );
diff --git a/templates/load_merg_stubs.tpl b/templates/load_merg_stubs.tpl
new file mode 100644
index 0000000..e55c664
--- /dev/null
+++ b/templates/load_merg_stubs.tpl
@@ -0,0 +1,47 @@
+{strip}
+<div class="display stock">
+ <div class="header">
+ <h1>MERG Stub Import — Group {$group|upper|escape}{if $dryRun} (dry run){/if}</h1>
+ </div>
+ <div class="body">
+ <p>File: <code>{$csvFile|escape}</code></p>
+ <p>
+ <strong>{$created}</strong> {if $doUpdate}{if $dryRun}would be updated{else}updated{/if}{else}{if $dryRun}would be created{else}created{/if}{/if}.
+ <strong>{$skipped}</strong> {if $doUpdate}already renamed (skipped){else}already exist (skipped){/if}.
+ </p>
+ {if $dryRun}
+ <p><a href="?group={$group|escape:'url'}{if $doUpdate}&amp;update=1{/if}">Run for real (remove &amp;dry=1)</a></p>
+ {/if}
+
+ {if $errors}
+ <h3>Errors</h3>
+ <ul>
+ {foreach $errors as $msg}<li>{$msg|escape}</li>{/foreach}
+ </ul>
+ {/if}
+
+ {if $rows}
+ <table class="table table-condensed">
+ <thead>
+ <tr>
+ <th>Order code</th>
+ <th>Component (from spreadsheet)</th>
+ <th>Status</th>
+ <th>content_id</th>
+ </tr>
+ </thead>
+ <tbody>
+ {foreach $rows as $r}
+ <tr class="{if $r.status eq 'created' or $r.status eq 'would create'}success{elseif $r.status eq 'error'}danger{/if}">
+ <td><code>{$r.code|escape}</code></td>
+ <td>{$r.component|escape}</td>
+ <td>{$r.status|escape}</td>
+ <td>{if $r.content_id}<a href="{$smarty.const.STOCK_PKG_URL}view_component.php?content_id={$r.content_id}">{$r.content_id}</a>{else}—{/if}</td>
+ </tr>
+ {/foreach}
+ </tbody>
+ </table>
+ {/if}
+ </div>
+</div>
+{/strip}