Laravel POS系统多包装酒类库存管理实现技术问询
Laravel POS多包装规格酒类库存管理实现方案
一、核心设计思路
将同一款商品的不同包装规格,通过**基础单位(如单罐)**建立关联,所有库存变动最终以基础单位为基准核算,不同包装仅作为基础单位的"组合形式"。以此解决包装间的库存联动问题:
- 整包/整箱的库存操作,自动换算为对应数量的基础单位库存变动
- 拆包/合并包装时,同步调整对应包装和基础单位的库存
二、数据库结构调整
1. 修改products表
添加关联字段和换算比例,标记包装间的从属关系:
Schema::table('products', function (Blueprint $table) { // 关联父商品(基础单位商品) $table->foreignId('parent_id')->nullable()->constrained('products'); // 该包装包含的基础单位数量,比如12Pack填12,单罐填1 $table->unsignedInteger('base_unit_quantity')->default(1); // 标记是否为基础单位(如单罐) $table->boolean('is_base_unit')->default(false); });
说明:基础单位商品(如单罐)的
parent_id设为null,is_base_unit设为true;其他包装(如12Pack、24罐箱装)的parent_id指向对应基础单位商品,base_unit_quantity设为对应数量。
2. 新增库存操作日志表(可选但推荐)
记录所有库存变动明细,方便追溯包装间的联动操作:
Schema::create('stock_movements', function (Blueprint $table) { $table->id(); $table->foreignId('product_id')->constrained('products'); $table->integer('quantity_change'); // 正数为增加,负数为减少 $table->string('type'); // 枚举值:sale, restock, split_pack, merge_pack, convert_pack $table->foreignId('related_product_id')->nullable()->constrained('products'); // 联动的其他商品ID $table->text('description')->nullable(); $table->timestamps(); });
三、模型关联与方法扩展
修改Product模型
添加关联关系和核心业务方法:
class Product extends Model { use HasFactory; protected $fillable = [ 'name', 'old_price', 'price', 'cost', 'quantity', 'security_stock', 'size', 'department_id', 'barcode', 'parent_id', 'base_unit_quantity', 'is_base_unit' ]; // 关联父商品(基础单位) public function parent() { return $this->belongsTo(Product::class, 'parent_id'); } // 关联子包装商品 public function children() { return $this->hasMany(Product::class, 'parent_id'); } // 获取当前商品对应的基础单位商品 public function getBaseUnitProduct() { return $this->is_base_unit ? $this : $this->parent; } // 将当前包装数量换算为基础单位数量 public function toBaseUnitQuantity($quantity = 1) { return $quantity * $this->base_unit_quantity; } // 将基础单位数量换算为当前包装数量 public function fromBaseUnitQuantity($baseQuantity) { return intdiv($baseQuantity, $this->base_unit_quantity); } }
四、核心业务逻辑实现
1. 销售时的库存联动处理
顾客购买非基础单位包装(如12Pack)时,同步扣减对应基础单位库存:
// 可封装到OrderService或StockService类中 public function deductStock(OrderItem $item) { $product = $item->product; $baseProduct = $product->getBaseUnitProduct(); DB::beginTransaction(); try { // 扣减当前包装库存 $product->decrement('quantity', $item->quantity); // 换算为基础单位数量,扣减基础单位库存 $baseQuantity = $product->toBaseUnitQuantity($item->quantity); $baseProduct->decrement('quantity', $baseQuantity); // 记录库存变动日志 \App\Models\StockMovement::create([ 'product_id' => $product->id, 'quantity_change' => -$item->quantity, 'type' => 'sale', 'related_product_id' => $baseProduct->id, 'description' => "销售{$product->name},联动扣减基础单位库存" ]); \App\Models\StockMovement::create([ 'product_id' => $baseProduct->id, 'quantity_change' => -$baseQuantity, 'type' => 'sale', 'related_product_id' => $product->id, 'description' => "因销售{$product->name},扣减基础单位库存" ]); DB::commit(); } catch (\Exception $e) { DB::rollBack(); throw $e; } }
2. 拆包上架场景处理
将整箱/整包拆分为基础单位上架时,调整对应包装和基础单位库存:
public function splitPack(Product $packProduct, $splitQuantity) { $baseProduct = $packProduct->getBaseUnitProduct(); $totalBaseQuantity = $packProduct->toBaseUnitQuantity($splitQuantity); DB::beginTransaction(); try { if ($packProduct->quantity < $splitQuantity) { throw new \Exception('包装库存不足'); } // 减少整包库存 $packProduct->decrement('quantity', $splitQuantity); // 增加基础单位库存 $baseProduct->increment('quantity', $totalBaseQuantity); // 记录日志 \App\Models\StockMovement::create([ 'product_id' => $packProduct->id, 'quantity_change' => -$splitQuantity, 'type' => 'split_pack', 'related_product_id' => $baseProduct->id, 'description' => "拆分{$splitQuantity}个{$packProduct->name}为基础单位" ]); \App\Models\StockMovement::create([ 'product_id' => $baseProduct->id, 'quantity_change' => $totalBaseQuantity, 'type' => 'split_pack', 'related_product_id' => $packProduct->id, 'description' => "由拆分{$packProduct->name}获得基础单位库存" ]); DB::commit(); } catch (\Exception $e) { DB::rollBack(); throw $e; } }
3. 促销包装等价换算处理
比如促销的24罐箱装等价于4组6罐装,直接进行包装间的库存换算:
public function convertPack(Product $sourcePack, Product $targetPack, $sourceQuantity) { $sourceBaseQty = $sourcePack->toBaseUnitQuantity($sourceQuantity); $targetQuantity = $targetPack->fromBaseUnitQuantity($sourceBaseQty); // 校验两个包装是否属于同一款商品 if ($sourcePack->getBaseUnitProduct()->id !== $targetPack->getBaseUnitProduct()->id) { throw new \Exception('两个包装不属于同一款商品'); } DB::beginTransaction(); try { if ($sourcePack->quantity < $sourceQuantity) { throw new \Exception('源包装库存不足'); } $sourcePack->decrement('quantity', $sourceQuantity); $targetPack->increment('quantity', $targetQuantity); // 记录日志 \App\Models\StockMovement::create([ 'product_id' => $sourcePack->id, 'quantity_change' => -$sourceQuantity, 'type' => 'convert_pack', 'related_product_id' => $targetPack->id, 'description' => "将{$sourceQuantity}个{$sourcePack->name}转换为{$targetQuantity}个{$targetPack->name}" ]); \App\Models\StockMovement::create([ 'product_id' => $targetPack->id, 'quantity_change' => $targetQuantity, 'type' => 'convert_pack', 'related_product_id' => $sourcePack->id, 'description' => "由{$sourcePack->name}转换获得库存" ]); DB::commit(); } catch (\Exception $e) { DB::rollBack(); throw $e; } }
五、条码扫描适配
每个包装有独立条码,POS扫描时的处理逻辑:
- 通过条码查询对应的
Product记录 - 根据商品是否为基础单位,自动触发对应库存联动逻辑
- 销售界面显示该包装对应的基础单位数量,方便店员确认
六、库存查询优化
添加模型方法,查看某款商品的总库存(所有包装换算为基础单位):
public function getTotalBaseUnitStock() { $baseProduct = $this->getBaseUnitProduct(); // 基础单位自身库存换算,加上所有子包装的库存换算 $total = $baseProduct->toBaseUnitQuantity($baseProduct->quantity); foreach ($baseProduct->children as $child) { $total += $child->toBaseUnitQuantity($child->quantity); } return $total; }
内容的提问来源于stack exchange,提问作者Sheldon Knight
相关产品推荐
相关产品推荐

