diff options
| author | Lester Caine <lester@lsces.co.uk> | 2026-06-09 11:15:23 +0100 |
|---|---|---|
| committer | Lester Caine <lester@lsces.co.uk> | 2026-06-09 11:15:23 +0100 |
| commit | 1de8f2f90284c375449326285f0b87e5e700a0c8 (patch) | |
| tree | f5d0d33b7229f4094ae30563b3b7d6ec4b934ea7 | |
| parent | a838a2d3963738b2e9bc496b31e078ead79d4a43 (diff) | |
| download | stock-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.php | 23 | ||||
| -rw-r--r-- | includes/classes/StockMovement.php | 16 | ||||
| -rw-r--r-- | list_movements.php | 8 | ||||
| -rw-r--r-- | list_stock.php | 11 | ||||
| -rw-r--r-- | templates/list_movements.tpl | 2 | ||||
| -rw-r--r-- | templates/list_stock.tpl | 6 | ||||
| -rw-r--r-- | templates/stockmovement/edit_xref_bom_item.tpl | 51 | ||||
| -rw-r--r-- | templates/stockmovement/edit_xref_bompck_item.tpl | 52 | ||||
| -rwxr-xr-x | templates/view_component.tpl | 2 | ||||
| -rwxr-xr-x | view_component.php | 6 |
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'; |
