diff options
| author | Lester Caine <lester@lsces.co.uk> | 2026-06-01 09:21:30 +0100 |
|---|---|---|
| committer | Lester Caine <lester@lsces.co.uk> | 2026-06-01 09:21:30 +0100 |
| commit | 54efaa8253c48fb5597b5e477b20a4127365d7c7 (patch) | |
| tree | 326dfb7310575392e18e235d9aebff7a6727a561 /edit_movement.php | |
| parent | ac18ebb5879a49a63e0bf624fc25c8e9d36820b0 (diff) | |
| download | stock-54efaa8253c48fb5597b5e477b20a4127365d7c7.tar.gz stock-54efaa8253c48fb5597b5e477b20a4127365d7c7.tar.bz2 stock-54efaa8253c48fb5597b5e477b20a4127365d7c7.zip | |
Rewrite StockMovement as pure LibertyContent; CSV import; bom grid
stock_movement/stock_movement_item tables retired. StockMovement now a
pure LibertyContent subclass — direction from reference xref (REQN=out,
TRANS/ORDER=in), received state from lc.event_time, items as quantity
xrefs with explicit xorder. loadXrefList() enriches component names.
schema_inc.php: full stockmovement xref seed — reference group
(REQN/TRANS/ORDER) and quantity group (SGL/PCK/SHT/VOL, bom/bompck
templates). MOV item removed from stockcomponent/stockassembly seed.
edit_movement.php: type selector on create from DB; CSV upload parses
header (from/ref/date) and component lines using component xref for
default qty type.
list_movements.php/tpl: rebuilt; ref_type filter; ref_type/ref_key/
received columns via xref subquery.
view_movement.php/tpl: cleaned up; xref group tabs, allow_edit=false.
stockmovement/ bom templates added. movement_lookup_inc simplified.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'edit_movement.php')
| -rw-r--r-- | edit_movement.php | 233 |
1 files changed, 131 insertions, 102 deletions
diff --git a/edit_movement.php b/edit_movement.php index 7a3146f..239b7f5 100644 --- a/edit_movement.php +++ b/edit_movement.php @@ -1,16 +1,16 @@ <?php /** - * Create or edit a stock movement. Handles header save, item add/remove, - * CSV upload, and process. - * * @package stock + * @subpackage functions */ namespace Bitweaver\Stock; +use Bitweaver\KernelTools; + require_once '../kernel/includes/setup_inc.php'; -global $gBitSystem, $gBitSmarty, $gBitDb; +global $gBitSystem, $gBitSmarty, $gBitUser, $gBitDb; include_once STOCK_PKG_INCLUDE_PATH.'movement_lookup_inc.php'; @@ -20,167 +20,196 @@ if( $gContent->isValid() ) { $gBitSystem->verifyPermission( 'p_stock_create' ); } -$errors = []; +// TODO: derive from liberty_xref_item WHERE content_type_guid='stockcomponent' AND x_group='quantity' +$qtyTypes = [ 'SGL', 'PCK', 'SHT', 'VOL' ]; + +// Movement reference types from DB — drives the type selector on create +$refTypes = $gBitDb->getAssoc( + "SELECT xi.`item`, xi.`cross_ref_title` + FROM `".BIT_DB_PREFIX."liberty_xref_item` xi + JOIN `".BIT_DB_PREFIX."liberty_xref_group` xg ON xg.`x_group` = xi.`x_group` AND xg.`content_type_guid` = xi.`content_type_guid` + WHERE xi.`content_type_guid` = '".STOCKMOVEMENT_CONTENT_TYPE_GUID."' AND xi.`x_group` = 'reference' + ORDER BY xi.`item`" +); -if( !empty( $_REQUEST['save'] ) ) { - $isNew = empty( $_REQUEST['movement_id'] ); +if( !empty( $_REQUEST['fSave'] ) ) { + $isNew = !$gContent->isValid(); if( $gContent->store( $_REQUEST ) ) { - if( $isNew ) { - header( 'Location: '.STOCK_PKG_URL.'edit_movement.php?movement_id='.$gContent->mMovementId ); - } else { - header( 'Location: '.STOCK_PKG_URL.'list_movements.php' ); + // On creation, immediately store the chosen movement type xref + if( $isNew && !empty( $_REQUEST['movement_type'] ) && isset( $refTypes[$_REQUEST['movement_type']] ) ) { + $typeHash = [ + 'content_id' => $gContent->mContentId, + 'item' => $_REQUEST['movement_type'], + 'fAddXref' => 1, + ]; + $gContent->storeXref( $typeHash ); } + header( 'Location: '.STOCK_PKG_URL.'edit_movement.php?content_id='.$gContent->mContentId ); die; } - $errors = $gContent->mErrors; - -} elseif( !empty( $_REQUEST['add_item'] ) && $gContent->isValid() ) { - $componentTitle = trim( $_REQUEST['component_title'] ?? '' ); - $qty = is_numeric( $_REQUEST['qty'] ?? '' ) ? (float)$_REQUEST['qty'] : 0; - $qtySrc = in_array( $_REQUEST['qty_src'] ?? '', [ 'SGL', 'PCK', 'SHT', 'VOL' ] ) - ? $_REQUEST['qty_src'] : 'SGL'; - if( empty( $componentTitle ) ) { - $errors[] = 'Component title is required.'; - } elseif( $qty < 0 ) { - $errors[] = 'Quantity cannot be negative.'; - } else { - $contentId = $gBitDb->getOne( - "SELECT lc.`content_id` - FROM `".BIT_DB_PREFIX."liberty_content` lc - WHERE lc.`content_type_guid` = '".STOCKCOMPONENT_CONTENT_TYPE_GUID."' AND lc.`title` = ?", - [ $componentTitle ] - ); - if( !$contentId ) { - $errors[] = "Component '$componentTitle' not found."; - } else { - $existingItems = $gContent->mInfo['items'] ?? []; - $nextPos = empty( $existingItems ) ? 1 - : max( array_map( fn( $i ) => (int)( $i['item_position'] ?? 0 ), $existingItems ) ) + 1; - $gContent->addItem( (int)$contentId, $qty, $qtySrc, (float)$nextPos ); - header( 'Location: '.STOCK_PKG_URL.'edit_movement.php?movement_id='.$gContent->mMovementId ); - die; - } - } +} elseif( !empty( $_REQUEST['fReceived'] ) && $gContent->isValid() ) { + $gContent->markReceived(); + header( 'Location: '.STOCK_PKG_URL.'edit_movement.php?content_id='.$gContent->mContentId ); + die; } elseif( !empty( $_REQUEST['upload_csv'] ) && $gContent->isValid() ) { $file = $_FILES['csv_file'] ?? null; if( !$file || $file['error'] !== UPLOAD_ERR_OK ) { - $errors[] = 'No file uploaded or upload error.'; + $gContent->mErrors[] = KernelTools::tra( 'No file uploaded or upload error.' ); } else { $handle = fopen( $file['tmp_name'], 'r' ); if( $handle === false ) { - $errors[] = 'Could not read uploaded file.'; + $gContent->mErrors[] = KernelTools::tra( 'Could not read uploaded file.' ); } else { $csvLoaded = 0; $csvSkipped = 0; $csvErrors = []; $rowNum = 0; - $existingItems = $gContent->mInfo['items'] ?? []; - $nextPos = empty( $existingItems ) ? 1 - : max( array_map( fn( $i ) => (int)( $i['item_position'] ?? 0 ), $existingItems ) ) + 1; + + // Seed xorder from any existing movement items so re-uploads append cleanly + $nextXorder = (int)$gBitDb->getOne( + "SELECT COALESCE( MAX(x.`xorder`) + 1, 1 ) FROM `".BIT_DB_PREFIX."liberty_xref` x + WHERE x.`content_id` = ? AND x.`item` IN ('SGL','PCK','SHT','VOL')", + [ $gContent->mContentId ] + ) ?: 1; + while( ( $data = fgetcsv( $handle, 1000, ',', '"', '' ) ) !== false ) { $rowNum++; - $componentTitle = trim( $data[0] ?? '' ); - $qty = isset( $data[2] ) && is_numeric( trim( $data[2] ) ) ? (float)trim( $data[2] ) : null; - if( empty( $componentTitle ) ) { + if( $rowNum === 1 ) { + // Header: from, ref, start_date (dd/mm/yy) + $from = trim( $data[0] ?? '' ); + $ref = trim( $data[1] ?? '' ); + $dateStr = trim( $data[2] ?? '' ); + + if( $ref !== '' ) { + // Update existing reference xref if already set (type selected on create), else insert + $existingXrefId = $gBitDb->getOne( + "SELECT `xref_id` FROM `".BIT_DB_PREFIX."liberty_xref` + WHERE `content_id` = ? AND `item` IN ('REQN','TRANS','ORDER') + ORDER BY `xorder`", + [ $gContent->mContentId ] + ); + $refHash = [ + 'content_id' => $gContent->mContentId, + 'item' => 'TRANS', + 'xkey' => $ref, + 'edit' => $from, + ]; + if( $existingXrefId ) { + $refHash['xref_id'] = $existingXrefId; + } else { + $refHash['fAddXref'] = 1; + } + $gContent->storeXref( $refHash ); + } + + if( $dateStr !== '' ) { + $parts = explode( '/', $dateStr ); + if( count( $parts ) === 3 ) { + $year = (int)$parts[2] < 100 ? 2000 + (int)$parts[2] : (int)$parts[2]; + $ts = mktime( 0, 0, 0, (int)$parts[1], (int)$parts[0], $year ); + if( $ts ) { + $gBitDb->query( + "UPDATE `".BIT_DB_PREFIX."liberty_content` SET `event_time` = ? WHERE `content_id` = ?", + [ $ts, $gContent->mContentId ] + ); + } + } + } + continue; + } + + // Data rows: component name, quantity, [optional qty type] + $componentName = trim( $data[0] ?? '' ); + $rawQty = trim( $data[1] ?? '' ); + $qtyOverride = strtoupper( trim( $data[2] ?? '' ) ); + + if( $componentName === '' ) { $csvSkipped++; continue; } - if( $qty === null || $qty < 0 ) { + + // Take numeric part before any space + $qty = (float)$rawQty; + if( $qty <= 0 ) { + $csvErrors[] = "Row $rowNum: '$componentName' — invalid quantity '$rawQty', skipped."; $csvSkipped++; - $csvErrors[] = "Row $rowNum: '$componentTitle' — invalid quantity, skipped."; continue; } $contentId = $gBitDb->getOne( "SELECT lc.`content_id` FROM `".BIT_DB_PREFIX."liberty_content` lc - WHERE lc.`content_type_guid` = '".STOCKCOMPONENT_CONTENT_TYPE_GUID."' AND lc.`title` = ?", - [ $componentTitle ] + WHERE lc.`content_type_guid` = 'stockcomponent' + AND lc.`title` = ?", + [ $componentName ] ); if( !$contentId ) { + $csvErrors[] = "Row $rowNum: '$componentName' not found, skipped."; $csvSkipped++; - $csvErrors[] = "Row $rowNum: '$componentTitle' not found, skipped."; continue; } - $gContent->addItem( (int)$contentId, $qty, 'SGL', (float)$nextPos ); - $nextPos++; + // Qty type: use CSV column 3 if valid, else read from component's existing xref + $qtySrc = in_array( $qtyOverride, $qtyTypes ) ? $qtyOverride : null; + if( !$qtySrc ) { + $placeholders = implode( ',', array_fill( 0, count( $qtyTypes ), '?' ) ); + $qtySrc = $gBitDb->getOne( + "SELECT x.`item` FROM `".BIT_DB_PREFIX."liberty_xref` x + WHERE x.`content_id` = ? AND x.`item` IN ($placeholders) + ORDER BY x.`xorder`", + array_merge( [ (int)$contentId ], $qtyTypes ) + ) ?: 'SGL'; + } + + $itemHash = [ + 'content_id' => $gContent->mContentId, + 'item' => $qtySrc, + 'xref' => (int)$contentId, + 'xkey' => $qty, + 'xorder' => $nextXorder, + ]; + $gContent->storeXref( $itemHash ); + $nextXorder++; $csvLoaded++; } fclose( $handle ); + $gContent->load(); $gBitSmarty->assign( 'csvLoaded', $csvLoaded ); $gBitSmarty->assign( 'csvSkipped', $csvSkipped ); $gBitSmarty->assign( 'csvErrors', $csvErrors ); - $gContent->load(); } } -} elseif( !empty( $_REQUEST['process'] ) && $gContent->isValid() ) { - if( !$gContent->processMovement() ) { - $errors = $gContent->mErrors; - } else { - $gContent->load(); - } - } elseif( !empty( $_REQUEST['delete'] ) ) { $gBitSystem->verifyPermission( 'p_stock_admin' ); - if( !empty( $_REQUEST['cancel'] ) ) { - header( 'Location: '.STOCK_PKG_URL.'edit_movement.php?movement_id='.$gContent->mMovementId ); + header( 'Location: '.STOCK_PKG_URL.'edit_movement.php?content_id='.$gContent->mContentId ); die; } elseif( empty( $_REQUEST['confirm'] ) ) { - $formHash['delete'] = true; - $formHash['movement_id'] = $gContent->mMovementId; - $gBitSystem->confirmDialog( $formHash, + $gBitSystem->confirmDialog( + [ 'delete' => true, 'content_id' => $gContent->mContentId ], [ 'confirm_item' => $gContent->getTitle(), - 'warning' => 'Are you sure you want to delete this movement? ('.$gContent->getTitle().')', - 'error' => 'This cannot be undone!', - ], + 'warning' => KernelTools::tra( 'Are you sure you want to delete this movement?' ).' ('.$gContent->getTitle().')', + 'error' => KernelTools::tra( 'This cannot be undone!' ), + ] ); } else { $gContent->expunge(); header( 'Location: '.STOCK_PKG_URL.'list_movements.php' ); die; } - -} else { - foreach( $_REQUEST as $key => $val ) { - if( str_starts_with( $key, 'remove_item_' ) ) { - $itemContentId = (int)substr( $key, 12 ); - $gContent->removeItem( $itemContentId ); - header( 'Location: '.STOCK_PKG_URL.'edit_movement.php?movement_id='.$gContent->mMovementId ); - die; - } - } } -$isComplete = ( ( $gContent->mInfo['status'] ?? '' ) === 'complete' ); -$isPending = ( ( $gContent->mInfo['status'] ?? '' ) === 'pending' ); - -$sortMode = $_REQUEST['sort_mode'] ?? 'item_position_asc'; if( $gContent->isValid() ) { - $gContent->mInfo['items'] = $gContent->loadItems( $sortMode ); + $gContent->mInfo['movement_xref_groups'] = $gContent->getXrefGroupList(); } -$gBitSmarty->assign( 'sortMode', $sortMode ); -$gBitSmarty->assign( 'errors', $errors ); -$gBitSmarty->assign( 'isComplete', $isComplete ); -$gBitSmarty->assign( 'isPending', $isPending ); -$gBitSmarty->assign( 'statusOptions', [ - 'draft' => 'Draft', - 'pending' => 'Pending', - 'cancelled' => 'Cancelled', -] ); -$gBitSmarty->assign( 'qtySrcOptions', [ - 'SGL' => 'Single unit', - 'PCK' => 'Pack', - 'SHT' => 'Sheet', - 'VOL' => 'Volume', -] ); +$gBitSmarty->assign( 'refTypes', $refTypes ); +$gBitSmarty->assign( 'errors', $gContent->mErrors ); -$gBitSystem->display( 'bitpackage:stock/edit_movement.tpl', 'Edit Movement: '.$gContent->getTitle(), [ 'display_mode' => 'edit' ] ); +$gBitSystem->display( 'bitpackage:stock/edit_movement.tpl', KernelTools::tra( 'Edit Movement: ' ).$gContent->getTitle(), [ 'display_mode' => 'edit' ] ); |
