summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLester Caine <lester@lsces.co.uk>2026-05-29 13:57:43 +0100
committerLester Caine <lester@lsces.co.uk>2026-05-29 13:57:43 +0100
commite5f051c2b27424acd773a51d45fd775fbf4c0547 (patch)
tree8fd4b3cc106ce992e8f696a0ea3d301bce965c56
parent037372043f22a1fc5949694c0053cdaaa15f460b (diff)
downloadstock-e5f051c2b27424acd773a51d45fd775fbf4c0547.tar.gz
stock-e5f051c2b27424acd773a51d45fd775fbf4c0547.tar.bz2
stock-e5f051c2b27424acd773a51d45fd775fbf4c0547.zip
BOM parts list upload and display via liberty_xref
- StockAssembly: loadXrefList() override sorts quantity rows by xorder and batch-fetches component title, description and pack size in one JOIN query - StockComponent: getEditUrl() returns edit_component.php; new components redirect to edit after save so xrefs can be added immediately - edit.php: BOM CSV upload handler (ITEM/XORDER/XREF/XKEY/XKEY_EXT format) writing to liberty_xref via LibertyXref::store(); removed old component upload and single-add handlers - edit_component.php: removed spurious assembly/component guard on save - schema_inc.php: quantity group template='bom' for stockassembly; KLSGL kitlocker stock item; bom template on stockassembly quantity xref items - Templates: edit_assembly.tpl and view_assembly.tpl use getXrefListTemplate(); new list_xref_bom.tpl, list_xref_bompck.tpl, view_xref_bom_record.tpl, edit_xref_bom.tpl for BOM display and editing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rwxr-xr-xadmin/schema_inc.php14
-rwxr-xr-xedit.php116
-rwxr-xr-xedit_component.php12
-rwxr-xr-xincludes/classes/StockAssembly.php28
-rwxr-xr-xincludes/classes/StockComponent.php7
-rwxr-xr-xtemplates/edit_assembly.tpl25
-rw-r--r--templates/edit_xref_bom.tpl51
-rw-r--r--templates/list_xref_bom.tpl63
-rw-r--r--templates/list_xref_bompck.tpl64
-rwxr-xr-xtemplates/view_assembly.tpl2
-rw-r--r--templates/view_xref_bom_record.tpl24
11 files changed, 326 insertions, 80 deletions
diff --git a/admin/schema_inc.php b/admin/schema_inc.php
index 68df2c4..3dcb9b4 100755
--- a/admin/schema_inc.php
+++ b/admin/schema_inc.php
@@ -138,7 +138,8 @@ $xrefItems = [];
foreach( [ 'stockcomponent', 'stockassembly' ] as $guid ) {
$xrefTypes[] = "INSERT INTO `{$X}liberty_xref_group` (`x_group`,`content_type_guid`,`title`,`sort_order`,`role_id`,`type_href`) VALUES ('supplier','{$guid}','Supplier', 1,3,'')";
- $xrefTypes[] = "INSERT INTO `{$X}liberty_xref_group` (`x_group`,`content_type_guid`,`title`,`sort_order`,`role_id`,`type_href`) VALUES ('quantity','{$guid}','Quantity', 2,3,'')";
+ $qtyGrpTpl = ( $guid === 'stockassembly' ) ? 'bom' : '';
+ $xrefTypes[] = "INSERT INTO `{$X}liberty_xref_group` (`x_group`,`content_type_guid`,`title`,`sort_order`,`role_id`,`type_href`,`template`) VALUES ('quantity','{$guid}','Quantity', 2,3,'','{$qtyGrpTpl}')";
$xrefTypes[] = "INSERT INTO `{$X}liberty_xref_group` (`x_group`,`content_type_guid`,`title`,`sort_order`,`role_id`,`type_href`) VALUES ('values', '{$guid}','Values', 3,3,'')";
// Supplier sources — multi=1 (multiple suppliers per item)
@@ -149,10 +150,12 @@ foreach( [ 'stockcomponent', 'stockassembly' ] as $guid ) {
// Quantity sources — selectable types multi=0, movement multi=1
// entry_date on liberty_xref timestamps each MOV row automatically
- $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('SGL','{$guid}','quantity','Single unit', 0,3,'','text',NULL)";
- $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('PCK','{$guid}','quantity','Pack', 0,3,'','text',NULL)";
- $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('SHT','{$guid}','quantity','Sheet (H x W)', 0,3,'','text',NULL)";
- $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('VOL','{$guid}','quantity','Volume', 0,3,'','text',NULL)";
+ // stockassembly uses 'bom' template (stock package override); stockcomponent uses 'text'
+ $qtyTpl = ( $guid === 'stockassembly' ) ? 'bom' : 'text';
+ $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('SGL','{$guid}','quantity','Single unit', 0,3,'','{$qtyTpl}',NULL)";
+ $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('PCK','{$guid}','quantity','Pack', 0,3,'','{$qtyTpl}',NULL)";
+ $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('SHT','{$guid}','quantity','Sheet (H x W)', 0,3,'','{$qtyTpl}',NULL)";
+ $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('VOL','{$guid}','quantity','Volume', 0,3,'','{$qtyTpl}',NULL)";
$xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('MOV','{$guid}','quantity','Stock movement', 1,3,'','text',NULL)";
// Values sources — starter catalogue, all multi=0, add more via admin
@@ -174,6 +177,7 @@ $xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,
$xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('KLPR', 'stockassembly','kitlocker','Kitlocker Price', 2,3,'','text',NULL)";
$xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('KL3M', 'stockassembly','kitlocker','Kitlocker 3 Month Sales', 3,3,'','text',NULL)";
$xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('KLURL','stockassembly','kitlocker','Details URL', 4,3,'','text',NULL)";
+$xrefItems[] = "INSERT INTO `{$X}liberty_xref_item` (`item`,`content_type_guid`,`x_group`,`cross_ref_title`,`multiple`,`role_id`,`cross_ref_href`,`template`,`data`) VALUES ('KLSGL','stockassembly','kitlocker','Kitlocker Stock', 2,3,'','text',NULL)";
// stockmovement xref — requisition reference links
$xrefTypes[] = "INSERT INTO `{$X}liberty_xref_group` (`x_group`,`content_type_guid`,`title`,`sort_order`,`role_id`,`type_href`) VALUES ('reference','stockmovement','Reference',1,3,'')";
diff --git a/edit.php b/edit.php
index 74655d9..99fc78c 100755
--- a/edit.php
+++ b/edit.php
@@ -62,62 +62,85 @@ if( !empty( $_REQUEST['savegallery'] ) ) {
header( 'Location: '.$gContent->getDisplayUrl() );
die();
-} elseif( $gContent->isValid() && !empty( $_REQUEST['add_component'] ) ) {
- $title = trim( $_REQUEST['component_title'] ?? '' );
- if( $title !== '' ) {
- $row = $gBitDb->getRow(
- "SELECT lc.`content_id` FROM `".BIT_DB_PREFIX."liberty_content` lc
- WHERE UPPER(lc.`title`) = UPPER(?) AND lc.`content_type_guid` = 'stockcomponent'",
- [ $title ]
- );
- if( $row ) {
- $nextPos = ((int)$gBitDb->getOne(
- "SELECT MAX(`item_position`) FROM `".BIT_DB_PREFIX."stock_assembly_component_map` WHERE `assembly_content_id`=?",
- [ $gContent->mContentId ]
- )) + 1;
- if( !$gContent->addItem( $row['content_id'], $nextPos ) ) {
- $stockErrors[] = KernelTools::tra('Component already in this assembly:').' '.htmlspecialchars($title);
- }
- } else {
- $stockErrors[] = KernelTools::tra('Component not found:').' '.htmlspecialchars($title);
- }
- }
- if( empty( $stockErrors ) ) {
- header( 'Location: '.STOCK_PKG_URL.'edit.php?content_id='.$gContent->mContentId );
- die();
- }
-
-} elseif( $gContent->isValid() && !empty( $_REQUEST['upload_components_csv'] ) ) {
+} elseif( $gContent->isValid() && !empty( $_REQUEST['upload_bom_csv'] ) ) {
$csvLoaded = $csvSkipped = 0;
$csvErrors = [];
if( !empty( $_FILES['csv_file']['tmp_name'] ) && is_uploaded_file( $_FILES['csv_file']['tmp_name'] ) ) {
- $nextPos = ((int)$gBitDb->getOne(
- "SELECT MAX(`item_position`) FROM `".BIT_DB_PREFIX."stock_assembly_component_map` WHERE `assembly_content_id`=?",
- [ $gContent->mContentId ]
- )) + 1;
+ // Valid BOM unit types (MOV is for movements, not BOM lines)
+ $validItems = [ 'SGL', 'PCK', 'SHT', 'VOL' ];
if( ($fh = fopen( $_FILES['csv_file']['tmp_name'], 'r' )) !== false ) {
- while( ($cols = fgetcsv($fh)) !== false ) {
- $title = trim($cols[0]);
- if( $title === '' || strtolower($title) === 'title' ) continue;
- $row = $gBitDb->getRow(
- "SELECT lc.`content_id` FROM `".BIT_DB_PREFIX."liberty_content` lc
- WHERE UPPER(lc.`title`) = UPPER(?) AND lc.`content_type_guid` = 'stockcomponent'",
- [ $title ]
+ $rowNum = 0;
+ while( ($cols = fgetcsv($fh, 0, ',', '"', '')) !== false ) {
+ $rowNum++;
+ $item = strtoupper( trim( $cols[0] ?? '' ) );
+ if( $item === '' || $item === 'ITEM' ) continue;
+
+ if( !in_array( $item, $validItems ) ) {
+ $csvErrors[] = KernelTools::tra('Row')." $rowNum: ".KernelTools::tra('unknown item type')." '$item'";
+ $csvSkipped++;
+ continue;
+ }
+
+ $xorder = (int)( $cols[1] ?? 0 );
+ $componentTitle = trim( $cols[2] ?? '' );
+ $xkey = trim( $cols[3] ?? '' );
+ $xkeyExt = trim( $cols[4] ?? '' ) ?: null;
+ $data = trim( $cols[5] ?? '' ) ?: null;
+
+ if( empty( $componentTitle ) ) {
+ $csvErrors[] = KernelTools::tra('Row')." $rowNum: ".KernelTools::tra('empty component name');
+ $csvSkipped++;
+ continue;
+ }
+
+ $compId = $gBitDb->getOne(
+ "SELECT sc.`component_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` = ?",
+ [ $componentTitle ]
+ );
+ if( !$compId ) {
+ $csvErrors[] = KernelTools::tra('Row')." $rowNum: ".KernelTools::tra('component not found').' — '.htmlspecialchars( $componentTitle );
+ $csvSkipped++;
+ continue;
+ }
+
+ $xrefObj = new \Bitweaver\Liberty\LibertyXref();
+ $xrefObj->mContentTypeGuid = STOCKASSEMBLY_CONTENT_TYPE_GUID;
+ $pHash = [
+ 'content_id' => $gContent->mContentId,
+ 'item' => $item,
+ 'xorder' => $xorder,
+ 'xref' => $compId,
+ 'xkey' => $xkey,
+ 'xkey_ext' => $xkeyExt,
+ 'edit' => $data,
+ ];
+ $existingId = $gBitDb->getOne(
+ "SELECT `xref_id` FROM `".BIT_DB_PREFIX."liberty_xref`
+ WHERE `content_id`=? AND `item`=? AND `xorder`=?",
+ [ $gContent->mContentId, $item, $xorder ]
);
- if( $row ) {
- if( $gContent->addItem( $row['content_id'], $nextPos ) ) {
- $nextPos++;
- $csvLoaded++;
- } else {
- $csvSkipped++;
- }
+ if( $existingId ) {
+ $xrefObj->load( $existingId );
+ $pHash['xref_id'] = $existingId;
+ }
+ if( $xrefObj->store( $pHash ) ) {
+ $csvLoaded++;
} else {
- $csvErrors[] = KernelTools::tra('Not found:').' '.htmlspecialchars($title);
+ $csvErrors[] = KernelTools::tra('Row')." $rowNum: ".KernelTools::tra('failed to store').' '.htmlspecialchars($componentTitle);
+ $csvSkipped++;
}
}
fclose($fh);
}
}
+ // Clear stale xref buckets so loadXrefList() re-reads from DB for the display below
+ foreach( [ 'supplier', 'quantity', 'values', 'kitlocker', 'history' ] as $_xg ) {
+ unset( $gContent->mInfo[$_xg] );
+ }
+ $gContent->loadXrefList();
+
$gBitSmarty->assign( 'csvLoaded', $csvLoaded );
$gBitSmarty->assign( 'csvSkipped', $csvSkipped );
$gBitSmarty->assign( 'csvErrors', $csvErrors );
@@ -135,9 +158,6 @@ if( !empty( $_REQUEST['savegallery'] ) ) {
// Initalize the errors list which contains any errors which occured during storage
$errors = !empty($gContent->mErrors) ? $gContent->mErrors : [];
$gBitSmarty->assign('errors', $errors);
-if( !empty($stockErrors) ) {
- $gBitSmarty->assign('stockWarnings', $stockErrors);
-}
if( $gContent->isValid() ) {
$sortMode = $_REQUEST['sort_mode'] ?? 'item_position_asc';
diff --git a/edit_component.php b/edit_component.php
index 053c043..9a6d34a 100755
--- a/edit_component.php
+++ b/edit_component.php
@@ -21,19 +21,17 @@ if( $gContent->isValid() ) {
}
if( !empty($_REQUEST['save']) ) {
- if( empty($_REQUEST['assembly_id']) && empty($_REQUEST['component_id']) ) {
- $gBitSmarty->assign( 'msg', KernelTools::tra('No assembly or component was specified') );
- $gBitSystem->display( 'error.tpl', null, [ 'display_mode' => 'edit' ] );
- die;
- }
-
+ $isNew = !$gContent->isValid();
if( $gContent->store( $_REQUEST ) ) {
$gContent->load();
if( !empty( $_REQUEST['gallery_additions'] ) ) {
$gContent->addToAssemblies( $_REQUEST['gallery_additions'] );
}
if( empty( $gContent->mErrors ) ) {
- header( 'Location: '.$gContent->getDisplayUrl() );
+ $url = $isNew
+ ? STOCK_PKG_URL.'edit_component.php?component_id='.$gContent->mComponentId
+ : $gContent->getDisplayUrl();
+ header( 'Location: '.$url );
die;
}
}
diff --git a/includes/classes/StockAssembly.php b/includes/classes/StockAssembly.php
index 4c0b27c..630f3ca 100755
--- a/includes/classes/StockAssembly.php
+++ b/includes/classes/StockAssembly.php
@@ -73,6 +73,34 @@ class StockAssembly extends StockBase {
return @$this->verifyId( $this->mAssemblyId ) || @$this->verifyId( $this->mContentId );
}
+ public function loadXrefList(): void {
+ parent::loadXrefList();
+ if( !empty( $this->mInfo['quantity'] ) ) {
+ usort( $this->mInfo['quantity'], fn($a,$b) => ($a['xorder'] <=> $b['xorder']) ?: strcmp($a['item'], $b['item']) );
+
+ $componentIds = array_values( array_unique( array_filter( array_column( $this->mInfo['quantity'], 'xref' ) ) ) );
+ if( $componentIds ) {
+ $placeholders = implode( ',', array_fill( 0, count( $componentIds ), '?' ) );
+ $components = $this->mDb->getAssoc(
+ "SELECT sc.`component_id`, lc.`title`, lc.`data`, pck.`xkey` AS `pack_size`
+ FROM `".BIT_DB_PREFIX."stock_component` sc
+ INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON lc.`content_id` = sc.`content_id`
+ LEFT JOIN `".BIT_DB_PREFIX."liberty_xref` pck ON pck.`content_id` = sc.`content_id` AND pck.`item` = 'PCK'
+ WHERE sc.`component_id` IN ($placeholders)",
+ $componentIds
+ );
+ foreach( $this->mInfo['quantity'] as &$row ) {
+ if( !empty( $row['xref'] ) && isset( $components[$row['xref']] ) ) {
+ $row['xref_title'] = $components[$row['xref']]['title'];
+ $row['xref_data'] = $components[$row['xref']]['data'];
+ $row['pack_size'] = $components[$row['xref']]['pack_size'];
+ }
+ }
+ unset( $row );
+ }
+ }
+ }
+
public static function lookup( $pLookupHash, $pLoadFromCache=true ) {
global $gBitDb;
$ret = null;
diff --git a/includes/classes/StockComponent.php b/includes/classes/StockComponent.php
index 8555d23..3bff170 100755
--- a/includes/classes/StockComponent.php
+++ b/includes/classes/StockComponent.php
@@ -272,6 +272,13 @@ class StockComponent extends StockBase {
return static::getDisplayUrlFromHash( $info );
}
+ public function getEditUrl( $pContentId = null, $pMixed = null ): string {
+ if( $this->verifyId( $this->mComponentId ) ) {
+ return STOCK_PKG_URL.'edit_component.php?component_id='.$this->mComponentId;
+ }
+ return STOCK_PKG_URL.'edit_component.php';
+ }
+
public static function getDisplayLinkFromHash( &$pParamHash, $pTitle='', $pAnchor=null ) {
global $gBitSystem;
$pTitle = trim( $pTitle );
diff --git a/templates/edit_assembly.tpl b/templates/edit_assembly.tpl
index 4adec06..66d815e 100755
--- a/templates/edit_assembly.tpl
+++ b/templates/edit_assembly.tpl
@@ -12,7 +12,7 @@
<div class="body">
{form id="editAssemblyForm" ipackage="stock" ifile="edit.php"}
- {formfeedback error=$errors warning=$stockWarnings success=$stockSuccess}
+ {formfeedback error=$errors success=$stockSuccess}
<input type="hidden" name="content_id" value="{$gContent->mContentId|escape}"/>
@@ -33,7 +33,7 @@
{if $gContent->mInfo.stockassembly_types}
{jstabs}
{section name=xrefGroup loop=$gContent->mInfo.stockassembly_types}
- {include file="bitpackage:liberty/list_xref.tpl"
+ {include file=$gContent->getXrefListTemplate($gContent->mInfo.stockassembly_types[xrefGroup].template)
source=$gContent->mInfo.stockassembly_types[xrefGroup].source
source_title=$gContent->mInfo.stockassembly_types[xrefGroup].title
group=$gContent->mInfo.stockassembly_types[xrefGroup].sort_order
@@ -98,27 +98,14 @@
<p class="muted">{tr}No components yet.{/tr}</p>
{/if}
- {* ── Add single component ── *}
- <h4>{tr}Add Component{/tr}</h4>
- {form ipackage="stock" ifile="edit.php"}
- <input type="hidden" name="content_id" value="{$gContent->mContentId|escape}"/>
- <div class="form-inline">
- <div class="form-group">
- <input type="text" class="form-control" name="component_title"
- placeholder="{tr}Component title{/tr}" size="40"/>
- </div>
- <input type="submit" class="btn btn-default" name="add_component" value="{tr}Add{/tr}"/>
- </div>
- {/form}
-
- {* ── Upload CSV ── *}
- <h4>{tr}Upload Components CSV{/tr}</h4>
- <p class="help-block">{tr}One component title per line (or comma-separated with title in first column).{/tr}</p>
+ {* ── Upload BOM CSV ── *}
+ <h4>{tr}Upload Parts List (BOM){/tr}</h4>
+ <p class="help-block">{tr}Columns: ITEM, XORDER, XREF (component title), XKEY (quantity), XKEY_EXT (ref designators), DATA{/tr}</p>
{form enctype="multipart/form-data" ipackage="stock" ifile="edit.php"}
<input type="hidden" name="content_id" value="{$gContent->mContentId|escape}"/>
<div class="form-inline">
<input type="file" name="csv_file" accept=".csv,text/csv"/>
- <input type="submit" class="btn btn-default" name="upload_components_csv" value="{tr}Upload{/tr}"/>
+ <input type="submit" class="btn btn-default" name="upload_bom_csv" value="{tr}Upload BOM{/tr}"/>
</div>
{/form}
{/if}
diff --git a/templates/edit_xref_bom.tpl b/templates/edit_xref_bom.tpl
new file mode 100644
index 0000000..dba0015
--- /dev/null
+++ b/templates/edit_xref_bom.tpl
@@ -0,0 +1,51 @@
+{strip}
+<div class="edit liberty">
+ <div class="header">
+ <h1>{tr}Edit BOM Entry{/tr}: {$gContent->getTitle()|escape}</h1>
+ </div>
+
+ <div class="body">
+ {formfeedback error=$errors}
+
+ {form id="editXrefForm"}
+ <input type="hidden" name="content_id" value="{$xrefInfo.content_id|escape}" />
+ <input type="hidden" name="xref_id" value="{$xrefInfo.xref_id|escape}" />
+ <input type="hidden" name="item" value="{$xrefInfo.item|escape}" />
+
+ <div class="form-group">
+ {formlabel label="Type"}
+ {forminput}
+ <p class="form-control-static">{$xrefInfo.source_title|escape}</p>
+ {/forminput}
+ </div>
+
+ <div class="form-group">
+ {formlabel label="Component ID" for="xref"}
+ {forminput}
+ <input type="text" class="form-control input-small" name="xref" id="xref" value="{$xrefInfo.xref|escape}" />
+ {formhelp note="component_id of the stock component"}
+ {/forminput}
+ </div>
+
+ <div class="form-group">
+ {formlabel label="Quantity" for="xkey"}
+ {forminput}
+ <input type="text" class="form-control input-small" name="xkey" id="xkey" value="{$xrefInfo.xkey|escape}" />
+ {/forminput}
+ </div>
+
+ <div class="form-group">
+ {formlabel label="Ref designators" for="xkey_ext"}
+ {forminput}
+ <input type="text" class="form-control" name="xkey_ext" id="xkey_ext" value="{$xrefInfo.xkey_ext|escape}" />
+ {/forminput}
+ </div>
+
+ <div class="form-group submit">
+ <input type="submit" class="btn btn-default" name="fCancel" value="{tr}Cancel{/tr}" />
+ <input type="submit" class="btn btn-primary" name="fSaveXref" value="{tr}Save{/tr}" />
+ </div>
+ {/form}
+ </div>
+</div>
+{/strip}
diff --git a/templates/list_xref_bom.tpl b/templates/list_xref_bom.tpl
new file mode 100644
index 0000000..e03388e
--- /dev/null
+++ b/templates/list_xref_bom.tpl
@@ -0,0 +1,63 @@
+{assign var=xrefcnt value=$gContent->mInfo.$source|default:[]|@count}
+{assign var=xrefAllowEdit value=$allow_edit|default:true}
+{jstab title="$source_title ($xrefcnt)"}
+{legend legend=$source_title}
+<div class="form-group table-responsive">
+ <table>
+ <thead>
+ <tr>
+ <th>{tr}Component{/tr}</th>
+ <th>{tr}Description{/tr}</th>
+ <th>{tr}Qty{/tr}</th>
+ <th>{tr}Ref{/tr}</th>
+ {if $xrefAllowEdit}<th>{tr}Added{/tr}</th><th>{tr}Updated{/tr}</th><th>{tr}Edit{/tr}</th>{/if}
+ </tr>
+ </thead>
+ <tbody>
+ {section name=xref loop=$gContent->mInfo.$source}
+ <tr class="{cycle values="even,odd"}">
+ <td>
+ {if $gContent->mInfo.$source[xref].xref > 0}
+ <a href="{$smarty.const.STOCK_PKG_URL}view_component.php?component_id={$gContent->mInfo.$source[xref].xref|escape}">{$gContent->mInfo.$source[xref].xref_title|default:$gContent->mInfo.$source[xref].xref|escape}</a>
+ {else}
+ &nbsp;
+ {/if}
+ </td>
+ <td>{$gContent->mInfo.$source[xref].xref_data|escape}</td>
+ <td>{$gContent->mInfo.$source[xref].xkey|escape}</td>
+ <td>{$gContent->mInfo.$source[xref].xkey_ext|escape}</td>
+ {if $xrefAllowEdit}
+ <td>{$gContent->mInfo.$source[xref].start_date|bit_short_date}</td>
+ <td>{$gContent->mInfo.$source[xref].last_update_date|bit_short_date}</td>
+ <td>
+ <span class="actionicon">
+ {if $gContent->hasUpdatePermission() && $source ne 'history'}
+ {smartlink ititle="Edit" ipackage="liberty" ifile="edit_xref.php" booticon="icon-note-edit" content_id=$gContent->mInfo.content_id xref_id=$gContent->mInfo.$source[xref].xref_id}
+ {/if}
+ {if $gContent->hasExpungePermission()}
+ {smartlink ititle="Delete" ipackage="liberty" ifile="edit_xref.php" booticon="icon-note-delete" content_id=$gContent->mInfo.content_id xref_id=$gContent->mInfo.$source[xref].xref_id expunge=1}
+ {/if}
+ </span>
+ </td>
+ {/if}
+ </tr>
+ {if $gContent->mInfo.$source[xref].data}
+ <tr>
+ <td colspan="7" class="xref-note">{$gContent->mInfo.$source[xref].data|escape}</td>
+ </tr>
+ {/if}
+ {sectionelse}
+ <tr class="norecords">
+ <td colspan="7">{tr}No parts list found{/tr}</td>
+ </tr>
+ {/section}
+ </tbody>
+ </table>
+</div>
+{if $allow_add && $gContent->isValid() && $gContent->hasUpdatePermission() && $source ne 'history'}
+ <div>
+ {smartlink ititle="Add record" ipackage="liberty" ifile="add_xref.php" booticon="icon-note-add" content_id=$gContent->mInfo.content_id group=$group}
+ </div>
+{/if}
+{/legend}
+{/jstab}
diff --git a/templates/list_xref_bompck.tpl b/templates/list_xref_bompck.tpl
new file mode 100644
index 0000000..42732d3
--- /dev/null
+++ b/templates/list_xref_bompck.tpl
@@ -0,0 +1,64 @@
+{assign var=xrefcnt value=$gContent->mInfo.$source|default:[]|@count}
+{assign var=xrefAllowEdit value=$allow_edit|default:true}
+{jstab title="$source_title ($xrefcnt)"}
+{legend legend=$source_title}
+<div class="form-group table-responsive">
+ <table>
+ <thead>
+ <tr>
+ <th>{tr}Component{/tr}</th>
+ <th>{tr}Description{/tr}</th>
+ <th>{tr}Qty{/tr}</th>
+ <th>{tr}Pack Size{/tr}</th>
+ <th>{tr}Ref{/tr}</th>
+ {if $xrefAllowEdit}<th>{tr}Added{/tr}</th><th>{tr}Updated{/tr}</th><th>{tr}Edit{/tr}</th>{/if}
+ </tr>
+ </thead>
+ <tbody>
+ {section name=xref loop=$gContent->mInfo.$source}
+ <tr class="{cycle values="even,odd"}">
+ <td>
+ {if $gContent->mInfo.$source[xref].xref > 0}
+ <a href="{$smarty.const.STOCK_PKG_URL}view_component.php?component_id={$gContent->mInfo.$source[xref].xref|escape}">{$gContent->mInfo.$source[xref].xref_title|default:$gContent->mInfo.$source[xref].xref|escape}</a>
+ {else}
+ &nbsp;
+ {/if}
+ </td>
+ <td>{$gContent->mInfo.$source[xref].xref_data|escape}</td>
+ <td>{$gContent->mInfo.$source[xref].xkey|escape} of {$gContent->mInfo.$source[xref].pack_size|escape}</td>
+ <td>{$gContent->mInfo.$source[xref].xkey_ext|escape}</td>
+ {if $xrefAllowEdit}
+ <td>{$gContent->mInfo.$source[xref].start_date|bit_short_date}</td>
+ <td>{$gContent->mInfo.$source[xref].last_update_date|bit_short_date}</td>
+ <td>
+ <span class="actionicon">
+ {if $gContent->hasUpdatePermission() && $source ne 'history'}
+ {smartlink ititle="Edit" ipackage="liberty" ifile="edit_xref.php" booticon="icon-note-edit" content_id=$gContent->mInfo.content_id xref_id=$gContent->mInfo.$source[xref].xref_id}
+ {/if}
+ {if $gContent->hasExpungePermission()}
+ {smartlink ititle="Delete" ipackage="liberty" ifile="edit_xref.php" booticon="icon-note-delete" content_id=$gContent->mInfo.content_id xref_id=$gContent->mInfo.$source[xref].xref_id expunge=1}
+ {/if}
+ </span>
+ </td>
+ {/if}
+ </tr>
+ {if $gContent->mInfo.$source[xref].data}
+ <tr>
+ <td colspan="8" class="xref-note">{$gContent->mInfo.$source[xref].data|escape}</td>
+ </tr>
+ {/if}
+ {sectionelse}
+ <tr class="norecords">
+ <td colspan="8">{tr}No parts list found{/tr}</td>
+ </tr>
+ {/section}
+ </tbody>
+ </table>
+</div>
+{if $allow_add && $gContent->isValid() && $gContent->hasUpdatePermission() && $source ne 'history'}
+ <div>
+ {smartlink ititle="Add record" ipackage="liberty" ifile="add_xref.php" booticon="icon-note-add" content_id=$gContent->mInfo.content_id group=$group}
+ </div>
+{/if}
+{/legend}
+{/jstab}
diff --git a/templates/view_assembly.tpl b/templates/view_assembly.tpl
index bba6f74..c9ab832 100755
--- a/templates/view_assembly.tpl
+++ b/templates/view_assembly.tpl
@@ -9,7 +9,7 @@
{if $gContent->mInfo.stockassembly_types}
{jstabs}
{section name=xrefGroup loop=$gContent->mInfo.stockassembly_types}
- {include file="bitpackage:liberty/list_xref.tpl"
+ {include file=$gContent->getXrefListTemplate($gContent->mInfo.stockassembly_types[xrefGroup].template)
source=$gContent->mInfo.stockassembly_types[xrefGroup].source
source_title=$gContent->mInfo.stockassembly_types[xrefGroup].title
group=$gContent->mInfo.stockassembly_types[xrefGroup].sort_order
diff --git a/templates/view_xref_bom_record.tpl b/templates/view_xref_bom_record.tpl
new file mode 100644
index 0000000..d779bf4
--- /dev/null
+++ b/templates/view_xref_bom_record.tpl
@@ -0,0 +1,24 @@
+{strip}
+<td></td>
+<td>
+ {if $gContent->mInfo.$source[xref].xref > 0}
+ <a href="{$smarty.const.STOCK_PKG_URL}view_component.php?component_id={$gContent->mInfo.$source[xref].xref|escape}">{$gContent->mInfo.$source[xref].xref_title|default:$gContent->mInfo.$source[xref].xref|escape}</a>
+ {else}
+ &nbsp;
+ {/if}
+</td>
+<td>{$gContent->mInfo.$source[xref].xkey|escape}</td>
+<td>{$gContent->mInfo.$source[xref].xkey_ext|escape}</td>
+<td>{if $gContent->hasUpdatePermission()}{$gContent->mInfo.$source[xref].start_date|bit_short_date}{/if}</td>
+<td>{if $gContent->hasUpdatePermission()}{$gContent->mInfo.$source[xref].last_update_date|bit_short_date}{/if}</td>
+<td>
+ <span class="actionicon">
+ {if $xrefAllowEdit|default:true && $gContent->hasUpdatePermission() && $source ne 'history'}
+ {smartlink ititle="Edit" ipackage="liberty" ifile="edit_xref.php" booticon="icon-note-edit" content_id=$gContent->mInfo.content_id xref_id=$gContent->mInfo.$source[xref].xref_id}
+ {/if}
+ {if $xrefAllowEdit|default:true && $gContent->hasExpungePermission()}
+ {smartlink ititle="Delete" ipackage="liberty" ifile="edit_xref.php" booticon="icon-note-delete" content_id=$gContent->mInfo.content_id xref_id=$gContent->mInfo.$source[xref].xref_id expunge=1}
+ {/if}
+ </span>
+</td>
+{/strip}