You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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扫描时的处理逻辑:

  1. 通过条码查询对应的Product记录
  2. 根据商品是否为基础单位,自动触发对应库存联动逻辑
  3. 销售界面显示该包装对应的基础单位数量,方便店员确认

六、库存查询优化

添加模型方法,查看某款商品的总库存(所有包装换算为基础单位):

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 17:43:17