summaryrefslogtreecommitdiff
path: root/import
diff options
context:
space:
mode:
authorLester Caine <lester@lsces.co.uk>2026-05-26 19:39:53 +0100
committerLester Caine <lester@lsces.co.uk>2026-05-26 19:39:53 +0100
commitfeef58886997ee5760553cae784783cb991f2882 (patch)
tree23138d8ac6ab561e74da12c8151f8bb9a779d746 /import
parente373f82ced189ff5814e6591394b432933821dfc (diff)
downloadstock-feef58886997ee5760553cae784783cb991f2882.tar.gz
stock-feef58886997ee5760553cae784783cb991f2882.tar.bz2
stock-feef58886997ee5760553cae784783cb991f2882.zip
Add simplified component importer (title, desc, supplier, PN, price)
Looks up supplier by contact title (case-insensitive, cached). Inserts #SUP (xref=contact content_id), #PN (xkey_ext), #PR (xkey) all at xorder=1 as one supplier group. Supplier xrefs skipped with a warning if contact not found. Supports ?clear=y to wipe and reload. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'import')
-rw-r--r--import/ImportSimpleComponent.php154
-rw-r--r--import/load_simple_components.php73
2 files changed, 227 insertions, 0 deletions
diff --git a/import/ImportSimpleComponent.php b/import/ImportSimpleComponent.php
new file mode 100644
index 0000000..b4f615b
--- /dev/null
+++ b/import/ImportSimpleComponent.php
@@ -0,0 +1,154 @@
+<?php
+/**
+ * Simplified component CSV importer — title, description, supplier, PN, price.
+ *
+ * CSV column layout (0-based, header row skipped by loader):
+ * 0 title Component name
+ * 1 description Plain-text description (stored as bithtml content body)
+ * 2 supplier Supplier contact title, case-insensitive (optional)
+ * 3 supplier_pn Supplier part number → xref #PN in xkey_ext (optional)
+ * 4 supplier_price Supplier price → xref #PR in xkey (optional)
+ *
+ * Supplier name is matched against liberty_content.title for content_type_guid='contact'.
+ * #SUP stores the contact content_id in the xref column; #PN and #PR share xorder=1
+ * so they are grouped with the #SUP entry as one supplier set.
+ *
+ * Existing components (matched by title) are skipped unless cleared first.
+ *
+ * @package stock
+ */
+
+use Bitweaver\Stock\StockComponent;
+
+// Cache supplier lookups — only 4 or so suppliers in the CSV
+$_stockSupplierCache = [];
+
+function stockImportFindSupplier( string $name ): ?int {
+ global $gBitDb, $_stockSupplierCache;
+
+ $key = strtolower( trim( $name ) );
+ if( array_key_exists( $key, $_stockSupplierCache ) ) {
+ return $_stockSupplierCache[$key];
+ }
+
+ $contentId = $gBitDb->getOne(
+ "SELECT lc.`content_id`
+ FROM `".BIT_DB_PREFIX."liberty_content` lc
+ INNER JOIN `".BIT_DB_PREFIX."contact` c ON c.`content_id` = lc.`content_id`
+ WHERE UPPER( lc.`title` ) = UPPER( ? )",
+ [ trim( $name ) ]
+ );
+
+ $_stockSupplierCache[$key] = $contentId ? (int)$contentId : null;
+ return $_stockSupplierCache[$key];
+}
+
+function stockExpungeComponentByTitle( string $title ): bool {
+ global $gBitDb;
+
+ $contentId = $gBitDb->getOne(
+ "SELECT lc.`content_id`
+ FROM `".BIT_DB_PREFIX."stock_component` sc
+ INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON lc.`content_id` = sc.`content_id`
+ WHERE lc.`title` = ?",
+ [ $title ]
+ );
+ if( !$contentId ) {
+ return false;
+ }
+
+ // StockComponent::expunge() handles component_map + stock_component;
+ // LibertyContent::expunge() now handles liberty_xref + liberty_content
+ $component = new StockComponent( null, (int)$contentId );
+ $component->expunge();
+ return true;
+}
+
+function stockImportSimpleComponent( array $data, int $rowNum ): array {
+ global $gBitDb;
+
+ $result = [ 'loaded' => 0, 'skipped' => 0, 'errors' => [] ];
+
+ $title = trim( $data[0] ?? '' );
+ if( empty( $title ) ) {
+ $result['skipped']++;
+ $result['errors'][] = "Row $rowNum: empty title, skipped.";
+ return $result;
+ }
+
+ $exists = $gBitDb->getOne(
+ "SELECT lc.`content_id`
+ FROM `".BIT_DB_PREFIX."stock_component` sc
+ INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON lc.`content_id` = sc.`content_id`
+ WHERE lc.`title` = ?",
+ [ $title ]
+ );
+ if( $exists ) {
+ $result['skipped']++;
+ $result['errors'][] = "Row $rowNum: '$title' already exists, skipped.";
+ return $result;
+ }
+
+ $description = trim( $data[1] ?? '' );
+ $supplierName = trim( $data[2] ?? '' );
+ $supplierPn = trim( $data[3] ?? '' );
+ $supplierPrice = trim( $data[4] ?? '' );
+
+ $component = new StockComponent();
+ $pHash = [
+ 'title' => $title,
+ 'edit' => $description,
+ 'format_guid' => 'bithtml',
+ ];
+ if( !$component->store( $pHash ) ) {
+ $result['skipped']++;
+ $result['errors'][] = "Row $rowNum: failed to create component '$title'.";
+ return $result;
+ }
+
+ $contentId = $component->mContentId;
+
+ if( !empty( $supplierName ) ) {
+ $supplierContentId = stockImportFindSupplier( $supplierName );
+ if( !$supplierContentId ) {
+ $result['errors'][] = "Row $rowNum: '$title' — supplier '$supplierName' not found in contacts, xrefs skipped.";
+ } else {
+ $xrefId = $gBitDb->GenID( 'liberty_xref_seq' );
+ $gBitDb->associateInsert( BIT_DB_PREFIX.'liberty_xref', [
+ 'xref_id' => $xrefId,
+ 'content_id' => $contentId,
+ 'item' => '#SUP',
+ 'xorder' => 1,
+ 'xref' => $supplierContentId,
+ 'last_update_date' => $gBitDb->NOW(),
+ ] );
+
+ if( !empty( $supplierPn ) ) {
+ $xrefId = $gBitDb->GenID( 'liberty_xref_seq' );
+ $gBitDb->associateInsert( BIT_DB_PREFIX.'liberty_xref', [
+ 'xref_id' => $xrefId,
+ 'content_id' => $contentId,
+ 'item' => '#PN',
+ 'xorder' => 1,
+ 'xkey_ext' => substr( $supplierPn, 0, 250 ),
+ 'last_update_date' => $gBitDb->NOW(),
+ ] );
+ }
+
+ if( !empty( $supplierPrice ) ) {
+ $xrefId = $gBitDb->GenID( 'liberty_xref_seq' );
+ $gBitDb->associateInsert( BIT_DB_PREFIX.'liberty_xref', [
+ 'xref_id' => $xrefId,
+ 'content_id' => $contentId,
+ 'item' => '#PR',
+ 'xorder' => 1,
+ 'xkey' => substr( $supplierPrice, 0, 32 ),
+ 'last_update_date' => $gBitDb->NOW(),
+ ] );
+ }
+ }
+ }
+
+ $result['loaded']++;
+ return $result;
+}
diff --git a/import/load_simple_components.php b/import/load_simple_components.php
new file mode 100644
index 0000000..7aafe25
--- /dev/null
+++ b/import/load_simple_components.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Load components from a 5-column CSV (title, description, supplier, PN, price).
+ * First row is a header and is skipped. Existing components (by title) are skipped
+ * unless clear=y is passed.
+ *
+ * Place your CSV at: stock/import/data/simple_components.csv
+ * Append ?clear=y to the URL to delete and re-import all rows.
+ *
+ * @package stock
+ */
+
+namespace Bitweaver\Stock;
+
+require_once '../../kernel/includes/setup_inc.php';
+
+global $gBitSystem, $gBitSmarty, $gBitDb;
+
+$gBitSystem->verifyPackage( 'stock' );
+$gBitSystem->verifyPermission( 'p_stock_admin' );
+
+require_once __DIR__.'/ImportSimpleComponent.php';
+
+$csvFile = __DIR__.'/data/simple_components.csv';
+$doClear = ( ( $_REQUEST['clear'] ?? '' ) === 'y' );
+$loaded = 0;
+$skipped = 0;
+$deleted = 0;
+$errors = [];
+
+if( !file_exists( $csvFile ) ) {
+ $errors[] = 'CSV file not found: '.$csvFile;
+} else {
+ $handle = fopen( $csvFile, 'r' );
+ if( $handle === false ) {
+ $errors[] = 'Cannot open CSV file.';
+ } else {
+ $rows = [];
+ $rowNum = 0;
+ while( ( $data = fgetcsv( $handle, 1000, ',', '"', '' ) ) !== false ) {
+ $rowNum++;
+ if( $rowNum === 1 ) {
+ continue; // skip header
+ }
+ $rows[] = $data;
+ }
+ fclose( $handle );
+
+ if( $doClear ) {
+ foreach( $rows as $data ) {
+ $title = trim( $data[0] ?? '' );
+ if( !empty( $title ) && stockExpungeComponentByTitle( $title ) ) {
+ $deleted++;
+ }
+ }
+ }
+
+ foreach( $rows as $idx => $data ) {
+ $result = stockImportSimpleComponent( $data, $idx + 2 ); // +2: 1-based + header
+ $loaded += $result['loaded'];
+ $skipped += $result['skipped'];
+ $errors = array_merge( $errors, $result['errors'] );
+ }
+ }
+}
+
+$gBitSmarty->assign( 'loaded', $loaded );
+$gBitSmarty->assign( 'skipped', $skipped );
+$gBitSmarty->assign( 'deleted', $deleted );
+$gBitSmarty->assign( 'errors', $errors );
+$gBitSmarty->assign( 'csvFile', $csvFile );
+
+$gBitSystem->display( 'bitpackage:stock/import_results.tpl', 'Import Components' );