summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLester Caine <lester@lsces.co.uk>2026-06-09 11:15:23 +0100
committerLester Caine <lester@lsces.co.uk>2026-06-09 11:15:23 +0100
commit1de8f2f90284c375449326285f0b87e5e700a0c8 (patch)
treef5d0d33b7229f4094ae30563b3b7d6ec4b934ea7
parenta838a2d3963738b2e9bc496b31e078ead79d4a43 (diff)
downloadstock-1de8f2f90284c375449326285f0b87e5e700a0c8.tar.gz
stock-1de8f2f90284c375449326285f0b87e5e700a0c8.tar.bz2
stock-1de8f2f90284c375449326285f0b87e5e700a0c8.zip
stock: PCK/SHT fractional display, movement qty summing, import qty type
Display fixes: - list_stock, list_movements, view_component: PCK stock divides by pack_size for fractional strip display; SHT shows 2 decimal places - list_movements: pack_size fetched per component for PCK display - All fractional formats use %.2f consistently StockMovement::getList component filter: - Replace INNER JOIN on xcmp with EXISTS subquery to avoid duplicate rows when a component appears multiple times in a movement BOM - cmp_qty now SUMs all matching xref rows so multi-assembly RQs show total quantity rather than silently dropping duplicate rows Movement BOM edit templates: - stockmovement/edit_xref_bom_item.tpl: proper edit form for SGL/SHT/VOL lines linking back to view_component - stockmovement/edit_xref_bompck_item.tpl: same for PCK with pack size hint Import: - ImportSimpleComponent: columns 6/7 (qty_type, qty_value) wired up; PCK/SHT/VOL writes qty xref so movement CSV imports pick up default type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--import/ImportSimpleComponent.php23
-rw-r--r--includes/classes/StockMovement.php16
-rw-r--r--list_movements.php8
-rw-r--r--list_stock.php11
-rw-r--r--templates/list_movements.tpl2
-rw-r--r--templates/list_stock.tpl6
-rw-r--r--templates/stockmovement/edit_xref_bom_item.tpl51
-rw-r--r--templates/stockmovement/edit_xref_bompck_item.tpl52
-rwxr-xr-xtemplates/view_component.tpl2
-rwxr-xr-xview_component.php6
10 files changed, 163 insertions, 14 deletions
diff --git a/import/ImportSimpleComponent.php b/import/ImportSimpleComponent.php
index 7db7c52..1f87375 100644
--- a/import/ImportSimpleComponent.php
+++ b/import/ImportSimpleComponent.php
@@ -5,14 +5,20 @@
* 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)
+ * 2 supplier Supplier contact SCREF or 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)
+ * 5 supplier_url Supplier URL → xref #SUP data (optional)
+ * 6 qty_type SGL/PCK/SHT/VOL — omit or blank for SGL (optional)
+ * 7 qty_value Pack size for PCK (pieces per pack); dimensions for SHT (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.
*
+ * Setting qty_type to PCK/SHT/VOL writes the appropriate xref on the component so that
+ * movement CSV imports pick up the correct default qty type without a manual override.
+ *
* Existing components (matched by title) are skipped unless cleared first.
*
* @package stock
@@ -88,6 +94,8 @@ function stockImportSimpleComponent( array $data, int $rowNum ): array {
$supplierPn = trim( $data[3] ?? '' );
$supplierPrice = trim( $data[4] ?? '' );
$supplierUrl = trim( $data[5] ?? '' );
+ $qtyType = strtoupper( trim( $data[6] ?? '' ) );
+ $qtyValue = trim( $data[7] ?? '' );
$component = new StockComponent();
$pHash = [
@@ -123,6 +131,19 @@ function stockImportSimpleComponent( array $data, int $rowNum ): array {
}
}
+ // Quantity type xref — sets the default qty type used by movement CSV imports
+ // and the pack size shown in BOM displays (PCK xref xkey = pieces per pack)
+ if( in_array( $qtyType, [ 'PCK', 'SHT', 'VOL' ] ) ) {
+ $gBitDb->associateInsert( BIT_DB_PREFIX.'liberty_xref', [
+ 'xref_id' => $gBitDb->GenID( 'liberty_xref_seq' ),
+ 'content_id' => $contentId,
+ 'item' => $qtyType,
+ 'xkey' => substr( $qtyValue, 0, 32 ),
+ 'xorder' => 0,
+ 'last_update_date' => $gBitDb->NOW(),
+ ] );
+ }
+
$result['loaded']++;
return $result;
}
diff --git a/includes/classes/StockMovement.php b/includes/classes/StockMovement.php
index d731050..a880e18 100644
--- a/includes/classes/StockMovement.php
+++ b/includes/classes/StockMovement.php
@@ -322,10 +322,10 @@ class StockMovement extends LibertyContent {
$joinSql .= " INNER JOIN `".BIT_DB_PREFIX."liberty_xref` xasm ON xasm.`content_id` = lc.`content_id` AND xasm.`item` = 'ASSEMBLY' AND xasm.`xref` = ?";
$bindVars[] = (int)$pListHash['assembly_content_id'];
}
- if( $this->verifyId( $pListHash['component_content_id'] ?? 0 ) ) {
- $joinSql .= " INNER JOIN `".BIT_DB_PREFIX."liberty_xref` xcmp ON xcmp.`content_id` = lc.`content_id`
- AND xcmp.`item` IN ('SGL','PCK','SHT','VOL') AND xcmp.`xref` = ?";
- $bindVars[] = (int)$pListHash['component_content_id'];
+ $cmpContentId = $this->verifyId( $pListHash['component_content_id'] ?? 0 ) ? (int)$pListHash['component_content_id'] : 0;
+ if( $cmpContentId ) {
+ $whereSql .= " AND EXISTS (SELECT 1 FROM `".BIT_DB_PREFIX."liberty_xref` xcf
+ WHERE xcf.`content_id` = lc.`content_id` AND xcf.`item` IN ('SGL','PCK','SHT','VOL') AND xcf.`xref` = $cmpContentId)";
}
if( $this->verifyId( $pListHash['user_id'] ?? 0 ) ) {
$whereSql .= " AND lc.`user_id` = ?";
@@ -363,8 +363,12 @@ class StockMovement extends LibertyContent {
);
$X = BIT_DB_PREFIX;
- $cmpQtySelect = $this->verifyId( $pListHash['component_content_id'] ?? 0 )
- ? ", xcmp.`item` AS cmp_qty_type, CAST(xcmp.`xkey` AS DOUBLE PRECISION) AS cmp_qty"
+ $cmpQtySelect = $cmpContentId
+ ? ", (SELECT FIRST 1 x.`item` FROM `{$X}liberty_xref` x
+ WHERE x.`content_id` = lc.`content_id` AND x.`item` IN ('SGL','PCK','SHT','VOL') AND x.`xref` = $cmpContentId
+ ORDER BY x.`xorder`) AS cmp_qty_type,
+ (SELECT SUM(CAST(x.`xkey` AS DOUBLE PRECISION)) FROM `{$X}liberty_xref` x
+ WHERE x.`content_id` = lc.`content_id` AND x.`item` IN ('SGL','PCK','SHT','VOL') AND x.`xref` = $cmpContentId) AS cmp_qty"
: ", CAST(NULL AS VARCHAR(4)) AS cmp_qty_type, CAST(NULL AS DOUBLE PRECISION) AS cmp_qty";
$query = "SELECT lc.`content_id`, lc.`title`, lc.`created`, lc.`last_modified`, lc.`event_time`,
diff --git a/list_movements.php b/list_movements.php
index 1557dbd..8ae66e4 100644
--- a/list_movements.php
+++ b/list_movements.php
@@ -19,11 +19,18 @@ $listHash = $_REQUEST;
$movementList = $movement->getList( $listHash );
$componentTitle = '';
+$packSize = null;
if( $componentContentId ) {
$componentTitle = $gBitDb->getOne(
"SELECT `title` FROM `".BIT_DB_PREFIX."liberty_content` WHERE `content_id` = ?",
[ $componentContentId ]
) ?: '';
+ $ps = $gBitDb->getOne(
+ "SELECT CAST(x.`xkey` AS DOUBLE PRECISION) FROM `".BIT_DB_PREFIX."liberty_xref` x
+ WHERE x.`content_id` = ? AND x.`item` = 'PCK'",
+ [ $componentContentId ]
+ );
+ $packSize = $ps ? (float)$ps : null;
}
$gBitSmarty->assign( 'listInfo', $listHash['listInfo'] );
@@ -32,5 +39,6 @@ $gBitSmarty->assign( 'filterType', $_REQUEST['ref_type'] ?? '' );
$gBitSmarty->assign( 'assemblyContentId', isset( $_REQUEST['assembly_content_id'] ) && is_numeric( $_REQUEST['assembly_content_id'] ) ? (int)$_REQUEST['assembly_content_id'] : null );
$gBitSmarty->assign( 'componentContentId', $componentContentId );
$gBitSmarty->assign( 'componentTitle', $componentTitle );
+$gBitSmarty->assign( 'packSize', $packSize );
$gBitSystem->display( 'bitpackage:stock/list_movements.tpl', 'Movements', [ 'display_mode' => 'list' ] );
diff --git a/list_stock.php b/list_stock.php
index 141b187..9771d4e 100644
--- a/list_stock.php
+++ b/list_stock.php
@@ -36,6 +36,9 @@ if( $assemblyContentId ) {
FROM `{$X}liberty_xref` sup
WHERE sup.`content_id` = lc.`content_id` AND sup.`item` = '#SUP'
ORDER BY sup.`xorder`) AS part_number,
+ (SELECT FIRST 1 CAST(pk.`xkey` AS DOUBLE PRECISION)
+ FROM `{$X}liberty_xref` pk
+ WHERE pk.`content_id` = lc.`content_id` AND pk.`item` = 'PCK') AS pack_size,
(SELECT SUM( CASE WHEN EXISTS (
SELECT 1 FROM `{$X}liberty_xref` r
WHERE r.`content_id` = mx.`content_id` AND r.`item` IN ('TRANS','ORDER')
@@ -71,6 +74,9 @@ if( $assemblyContentId ) {
FROM `{$X}liberty_xref` sup
WHERE sup.`content_id` = lc.`content_id` AND sup.`item` = '#SUP'
ORDER BY sup.`xorder`) AS part_number,
+ (SELECT FIRST 1 CAST(pk.`xkey` AS DOUBLE PRECISION)
+ FROM `{$X}liberty_xref` pk
+ WHERE pk.`content_id` = lc.`content_id` AND pk.`item` = 'PCK') AS pack_size,
SUM( CASE WHEN EXISTS (
SELECT 1 FROM `{$X}liberty_xref` r
WHERE r.`content_id` = x.`content_id` AND r.`item` IN ('TRANS','ORDER')
@@ -112,8 +118,9 @@ foreach( $rows as $row ) {
// In BOM view show all components; in general list respect hide-zero filter
if( $assemblyContentId || !$hideZero || $level != 0 ) {
$stockList[$cid]['stock'][$row['qty_type']] = [
- 'level' => $level,
- 'bom_qty' => $row['bom_qty'] !== null ? (float)$row['bom_qty'] : null,
+ 'level' => $level,
+ 'bom_qty' => $row['bom_qty'] !== null ? (float)$row['bom_qty'] : null,
+ 'pack_size' => $row['pack_size'] !== null ? (float)$row['pack_size'] : null,
];
}
}
diff --git a/templates/list_movements.tpl b/templates/list_movements.tpl
index aef033b..e50979e 100644
--- a/templates/list_movements.tpl
+++ b/templates/list_movements.tpl
@@ -55,7 +55,7 @@
<td><a href="{$mov.display_url|escape}">{$mov.title|escape}</a></td>
<td>{$mov.ref_type|escape|default:'—'}</td>
{if $componentContentId}
- <td class="text-right">{$mov.cmp_qty|string_format:"%.0f"} {$mov.cmp_qty_type|escape}</td>
+ <td class="text-right">{if $mov.cmp_qty_type eq 'PCK' && $packSize > 0}{math equation="q/p" q=$mov.cmp_qty p=$packSize format="%.2f"}{elseif $mov.cmp_qty_type eq 'SHT'}{$mov.cmp_qty|string_format:"%.2f"}{else}{$mov.cmp_qty|string_format:"%.0f"}{/if} {$mov.cmp_qty_type|escape}</td>
{/if}
<td>{if $mov.ref_start_date}{$mov.ref_start_date|bit_short_date}{else}—{/if}</td>
<td>{if $mov.event_time}{$mov.event_time|bit_short_date}{else}—{/if}</td>
diff --git a/templates/list_stock.tpl b/templates/list_stock.tpl
index edb6afe..6e9ede7 100644
--- a/templates/list_stock.tpl
+++ b/templates/list_stock.tpl
@@ -80,11 +80,11 @@
<td rowspan="{$comp.stock|@count}">{$comp.data|escape}</td>
<td rowspan="{$comp.stock|@count}">{$comp.part_number|escape}</td>
{/if}
- {if $showBom}<td class="text-right">{math equation="b*k" b=$row.bom_qty k=$kitCount format="%.0f"}</td>{/if}
+ {if $showBom}<td class="text-right">{if $qtype eq 'PCK' && $row.pack_size > 0}{math equation="b*k/p" b=$row.bom_qty k=$kitCount p=$row.pack_size format="%.2f"}{elseif $qtype eq 'SHT'}{math equation="b*k" b=$row.bom_qty k=$kitCount format="%.2f"}{else}{math equation="b*k" b=$row.bom_qty k=$kitCount format="%.0f"}{/if}</td>{/if}
<td>{$qtype|escape}</td>
- <td class="text-right">{$row.level|string_format:"%.0f"}</td>
+ <td class="text-right">{if $qtype eq 'PCK' && $row.pack_size > 0}{math equation="l/p" l=$row.level p=$row.pack_size format="%.2f"}{elseif $qtype eq 'SHT'}{$row.level|string_format:"%.2f"}{else}{$row.level|string_format:"%.0f"}{/if}</td>
{if $showBom}
- <td class="text-right{if $remaining < 0} text-danger{/if}">{$remaining|string_format:"%.0f"}</td>
+ <td class="text-right{if $remaining < 0} text-danger{/if}">{if $qtype eq 'PCK' && $row.pack_size > 0}{math equation="r/p" r=$remaining p=$row.pack_size format="%.2f"}{elseif $qtype eq 'SHT'}{$remaining|string_format:"%.2f"}{else}{$remaining|string_format:"%.0f"}{/if}</td>
{/if}
</tr>
{/foreach}
diff --git a/templates/stockmovement/edit_xref_bom_item.tpl b/templates/stockmovement/edit_xref_bom_item.tpl
new file mode 100644
index 0000000..29c1aa8
--- /dev/null
+++ b/templates/stockmovement/edit_xref_bom_item.tpl
@@ -0,0 +1,51 @@
+{strip}
+<div class="edit stock">
+ <div class="header">
+ <h1>{tr}Edit Item{/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}" />
+ <input type="hidden" name="xorder" value="{$xrefInfo.xorder|escape}" />
+
+ <div class="form-group">
+ {formlabel label="Component"}
+ {forminput}
+ <p class="form-control-static">
+ <a href="{$smarty.const.STOCK_PKG_URL}view_component.php?content_id={$xrefInfo.xref|escape}">{$xrefInfo.xref_title|default:$xrefInfo.xref|escape}</a>
+ </p>
+ {/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">
+ {formlabel label="Note" for="edit"}
+ {forminput}
+ <input type="text" class="form-control" name="edit" id="edit" value="{$xrefInfo.data|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/stockmovement/edit_xref_bompck_item.tpl b/templates/stockmovement/edit_xref_bompck_item.tpl
new file mode 100644
index 0000000..bd69cff
--- /dev/null
+++ b/templates/stockmovement/edit_xref_bompck_item.tpl
@@ -0,0 +1,52 @@
+{strip}
+<div class="edit stock">
+ <div class="header">
+ <h1>{tr}Edit Item{/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}" />
+ <input type="hidden" name="xorder" value="{$xrefInfo.xorder|escape}" />
+
+ <div class="form-group">
+ {formlabel label="Component"}
+ {forminput}
+ <p class="form-control-static">
+ <a href="{$smarty.const.STOCK_PKG_URL}view_component.php?content_id={$xrefInfo.xref|escape}">{$xrefInfo.xref_title|default:$xrefInfo.xref|escape}</a>
+ </p>
+ {/forminput}
+ </div>
+
+ <div class="form-group">
+ {formlabel label="Pieces required" for="xkey"}
+ {forminput}
+ <input type="text" class="form-control input-small" name="xkey" id="xkey" value="{$xrefInfo.xkey|escape}" />
+ {if $xrefInfo.pack_size}<span class="help-block">{tr}of{/tr} {$xrefInfo.pack_size|escape} {tr}per pack{/tr}</span>{/if}
+ {/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">
+ {formlabel label="Note" for="edit"}
+ {forminput}
+ <input type="text" class="form-control" name="edit" id="edit" value="{$xrefInfo.data|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/view_component.tpl b/templates/view_component.tpl
index f0b3c8a..37b42ef 100755
--- a/templates/view_component.tpl
+++ b/templates/view_component.tpl
@@ -47,7 +47,7 @@
{foreach from=$componentStockLevels key=qtype item=level}
<tr{if $level < 0} class="danger"{elseif $level == 0} class="warning"{/if}>
<td>{$qtype|escape}</td>
- <td class="text-right">{$level|string_format:"%.0f"}</td>
+ <td class="text-right">{if $qtype eq 'PCK' && $packSize > 0}{math equation="l/p" l=$level p=$packSize format="%.2f"}{elseif $qtype eq 'SHT'}{$level|string_format:"%.2f"}{else}{$level|string_format:"%.0f"}{/if}</td>
</tr>
{/foreach}
{else}
diff --git a/view_component.php b/view_component.php
index 82f7c18..2940441 100755
--- a/view_component.php
+++ b/view_component.php
@@ -59,7 +59,13 @@ if( $gContent->isValid() ) {
foreach( $rows as $row ) {
$stockLevels[$row['qty_type']] = (float)$row['stock_level'];
}
+ $ps = $gBitDb->getOne(
+ "SELECT CAST(x.`xkey` AS DOUBLE PRECISION) FROM `".BIT_DB_PREFIX."liberty_xref` x
+ WHERE x.`content_id` = ? AND x.`item` = 'PCK'",
+ [ $gContent->mContentId ]
+ );
$gBitSmarty->assign( 'componentStockLevels', $stockLevels );
+ $gBitSmarty->assign( 'packSize', $ps ? (float)$ps : null );
}
require_once STOCK_PKG_INCLUDE_PATH.'display_stock_component_inc.php';