PHP项目盈亏处理如何记录调整库存账面数据

wen PHP项目 24

本文目录导读:

PHP项目盈亏处理如何记录调整库存账面数据

  1. 核心原则
  2. 数据库表结构设计
  3. PHP 核心处理逻辑
  4. 调用示例
  5. 重要补充:成本计算与记账
  6. 常见问题与优化

在PHP项目中处理盈亏并调整库存账面数据,核心在于区分业务类型(如盘点盈亏、报损报溢、非销售出入库等)以及确保会计记账逻辑正确,下面是一个通用的、可落地的实现思路和代码示例。

核心原则

  1. 不可直接修改历史记录:不能去修改已经发生的入库单或销售单的数量,应通过调整单盘盈盘亏单来处理。
  2. 保证库存数量与账面价值一致:调整后,库存表(inventory)的quantitycost_price(或加权平均成本)要正确。
  3. 记录财务凭证:调整库存必然涉及“待处理财产损溢”科目,最终要转入“管理费用”或“营业外收支”。

数据库表结构设计

你需要至少以下核心表(以MySQL为例):

-- 1. 库存主表 (当前库存量)
CREATE TABLE `inventory` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `product_id` INT NOT NULL COMMENT '商品ID',
  `warehouse_id` INT NOT NULL COMMENT '仓库ID',
  `quantity` DECIMAL(15,3) DEFAULT 0 COMMENT '当前库存数量',
  `avg_cost` DECIMAL(15,4) DEFAULT 0.0000 COMMENT '移动加权平均成本',
  `last_updated` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY `uk_product_warehouse` (`product_id`, `warehouse_id`)
);
-- 2. 库存变动流水表 (所有库存变动的审计跟踪)
CREATE TABLE `inventory_transactions` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `transaction_type` ENUM('sale', 'purchase', 'return', 'adjustment', 'check_in', 'check_out', 'transfer_in', 'transfer_out') NOT NULL COMMENT '变动类型',
  `reference_no` VARCHAR(50) DEFAULT NULL COMMENT '关联单据号(如盘点单号、调整单号)',
  `product_id` INT NOT NULL,
  `warehouse_id` INT NOT NULL,
  `qty_change` DECIMAL(15,3) NOT NULL COMMENT '变动数量(正为增加,负为减少)',
  `unit_cost` DECIMAL(15,4) DEFAULT 0.0000 COMMENT '变动时的单位成本',
  `total_cost` DECIMAL(15,2) DEFAULT 0.00 COMMENT '变动总成本',
  `created_by` INT DEFAULT NULL COMMENT '操作人',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 3. 库存调整/盈亏单 (记录盈亏原因)
CREATE TABLE `inventory_adjustments` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `adjustment_no` VARCHAR(30) NOT NULL UNIQUE COMMENT '调整单号',
  `adjustment_type` ENUM('profit', 'loss', 'correction') NOT NULL COMMENT '盈亏或纠正',
  `reason` VARCHAR(255) DEFAULT NULL COMMENT '原因说明',
  `status` TINYINT DEFAULT 0 COMMENT '0=草稿, 1=已审核, 2=已过账',
  `total_profit_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '溢余总金额',
  `total_loss_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '亏损总金额',
  `created_by` INT DEFAULT NULL,
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `approved_at` DATETIME DEFAULT NULL
);
-- 4. 调整单明细表
CREATE TABLE `inventory_adjustment_items` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `adjustment_id` INT NOT NULL,
  `product_id` INT NOT NULL,
  `warehouse_id` INT NOT NULL,
  `system_qty` DECIMAL(15,3) NOT NULL COMMENT '账面数量',
  `actual_qty` DECIMAL(15,3) NOT NULL COMMENT '实际盘点/调整数量',
  `diff_qty` DECIMAL(15,3) NOT NULL COMMENT '差异数量(实际 - 账面)',
  `unit_cost` DECIMAL(15,4) NOT NULL COMMENT '调整时的单位成本(用于计算金额)',
  `total_diff_cost` DECIMAL(15,2) NOT NULL COMMENT '差异总金额',
  FOREIGN KEY (`adjustment_id`) REFERENCES `inventory_adjustments`(`id`)
);

PHP 核心处理逻辑

这里模拟一个“库存盘点盈亏”场景,生成调整单并同步库存。

<?php
class InventoryAdjustmentService
{
    private $pdo;
    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }
    /**
     * 执行库存调整(盘盈/盘亏)
     * @param array $adjustmentData ['items' => [['product_id'=>1, 'warehouse_id'=>1, 'actual_qty'=>100], ...], 'reason' => '...']
     * @param int $userId 操作人ID
     * @return array ['success' => bool, 'message' => string, 'adjustment_no' => string]
     * @throws Exception
     */
    public function executeAdjustment(array $adjustmentData, int $userId): array
    {
        // 开启事务(关键!)
        $this->pdo->beginTransaction();
        try {
            // 1. 生成调整单号
            $adjustmentNo = $this->generateAdjustmentNo();
            // 2. 创建调整单主表记录
            $adjustmentId = $this->createAdjustment($adjustmentNo, $adjustmentData['reason'], $userId);
            // 3. 计算差异并插入明细
            $totalProfit = 0; // 盘盈总金额
            $totalLoss   = 0; // 盘亏总金额
            $items       = [];
            foreach ($adjustmentData['items'] as $item) {
                // 获取账面数据和当前成本价
                $inventory = $this->getCurrentInventory($item['product_id'], $item['warehouse_id']);
                $systemQty  = $inventory ? $inventory['quantity'] : 0;
                $actualQty  = $item['actual_qty'];
                $diffQty    = $actualQty - $systemQty;
                if ($diffQty == 0) continue; // 无差异,跳过
                // 核心:获取调整时使用的单位成本
                // 策略:取当前加权平均成本 或 最新采购价(根据业务选择)
                $unitCost = $inventory ? $inventory['avg_cost'] : $this->getLatestPurchaseCost($item['product_id']);
                // 如果没有成本(新品),可考虑用0或报警
                $unitCost = $unitCost ?: 0;
                $totalDiffCost = abs($diffQty) * $unitCost;
                // 区分盈亏类型
                if ($diffQty > 0) {
                    $totalProfit += $totalDiffCost;
                } else {
                    $totalLoss += $totalDiffCost;
                }
                // 插入明细
                $this->createAdjustmentItem($adjustmentId, $item['product_id'], $item['warehouse_id'],
                                            $systemQty, $actualQty, $diffQty, $unitCost, $totalDiffCost);
                $items[] = [
                    'product_id'   => $item['product_id'],
                    'warehouse_id' => $item['warehouse_id'],
                    'diff_qty'     => $diffQty,
                    'unit_cost'    => $unitCost,
                    'total_cost'   => $totalDiffCost,
                ];
            }
            if (empty($items)) {
                throw new Exception('盘点差异为零,无需调整');
            }
            // 更新调整单主表的汇总金额
            $this->updateAdjustmentSummary($adjustmentId, $totalProfit, $totalLoss);
            // 4. 提交审核(自动审核示例)
            $this->approveAdjustment($adjustmentId, $userId);
            // 5. 核心:更新库存表 & 写入流水
            foreach ($items as $item) {
                $this->updateInventory($item['product_id'], $item['warehouse_id'], $item['diff_qty'], $item['unit_cost']);
                $this->logTransaction(
                    'adjustment',           // 类型
                    $adjustmentNo,          // 参考单号
                    $item['product_id'],
                    $item['warehouse_id'],
                    $item['diff_qty'],      // 正=增加,负=减少
                    $item['unit_cost'],
                    $item['total_cost'],
                    $userId
                );
            }
            // 6. 记账(可选,这里仅记录摘要)
            // 实际企业级应生成会计凭证
            // 盘盈: 借:库存商品 贷:待处理财产损溢 -> 借:待处理财产损溢 贷:管理费用
            // 盘亏: 借:待处理财产损溢 贷:库存商品 -> 借:管理费用 贷:待处理财产损溢
            $this->pdo->commit();
            return [
                'success' => true,
                'message' => '库存调整成功',
                'adjustment_no' => $adjustmentNo,
            ];
        } catch (Exception $e) {
            $this->pdo->rollBack();
            return [
                'success' => false,
                'message' => '调整失败: ' . $e->getMessage(),
            ];
        }
    }
    // ---------- 私有辅助方法 (数据库操作) ----------
    private function generateAdjustmentNo(): string
    {
        return 'ADJ-' . date('Ymd') . '-' . strtoupper(substr(uniqid(), -6));
    }
    private function createAdjustment(string $no, string $reason, int $userId): int
    {
        $stmt = $this->pdo->prepare("INSERT INTO inventory_adjustments (adjustment_no, reason, created_by) VALUES (?, ?, ?)");
        $stmt->execute([$no, $reason, $userId]);
        return (int)$this->pdo->lastInsertId();
    }
    private function getCurrentInventory(int $productId, int $warehouseId): ?array
    {
        $stmt = $this->pdo->prepare("SELECT quantity, avg_cost FROM inventory WHERE product_id = ? AND warehouse_id = ?");
        $stmt->execute([$productId, $warehouseId]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }
    private function getLatestPurchaseCost(int $productId): float
    {
        // 示例:从采购单取最新采购价(可根据实际业务替换)
        $stmt = $this->pdo->prepare("SELECT unit_cost FROM purchase_items WHERE product_id = ? ORDER BY id DESC LIMIT 1");
        $stmt->execute([$productId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ? (float)$row['unit_cost'] : 0;
    }
    private function createAdjustmentItem(int $adjustmentId, int $productId, int $warehouseId,
                                          float $systemQty, float $actualQty, float $diffQty,
                                          float $unitCost, float $totalCost): void
    {
        $stmt = $this->pdo->prepare(
            "INSERT INTO inventory_adjustment_items 
            (adjustment_id, product_id, warehouse_id, system_qty, actual_qty, diff_qty, unit_cost, total_diff_cost)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
        );
        $stmt->execute([$adjustmentId, $productId, $warehouseId, $systemQty, $actualQty, $diffQty, $unitCost, $totalCost]);
    }
    private function updateAdjustmentSummary(int $adjustmentId, float $profit, float $loss): void
    {
        $stmt = $this->pdo->prepare("UPDATE inventory_adjustments SET total_profit_amount = ?, total_loss_amount = ?, status = 1, approved_at = NOW() WHERE id = ?");
        $stmt->execute([$profit, $loss, $adjustmentId]);
    }
    private function approveAdjustment(int $adjustmentId, int $userId): void
    {
        // 这里可扩展审批流程,简单示例直接审核
        $stmt = $this->pdo->prepare("UPDATE inventory_adjustments SET status = 1, approved_at = NOW(), approved_by = ? WHERE id = ?");
        $stmt->execute([$userId, $adjustmentId]);
    }
    private function updateInventory(int $productId, int $warehouseId, float $qtyChange, float $unitCost): void
    {
        // 存在则更新(注意:这里没有处理加权平均成本的重新计算,实际业务应增加成本更新逻辑)
        // 简单版本:直接增减数量,成本保持原值(不够严谨)
        $stmt = $this->pdo->prepare(
            "INSERT INTO inventory (product_id, warehouse_id, quantity, avg_cost)
             VALUES (?, ?, ?, ?)
             ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)"
        );
        $stmt->execute([$productId, $warehouseId, $qtyChange, $unitCost]);
        // 注意:如果盘盈盘亏涉及成本变化,应重新计算加权平均成本
        // 例如盘盈入库的商品,可能拉低平均成本,这里省略成本重算,实际项目需实现
    }
    private function logTransaction(string $type, string $referenceNo, int $productId, int $warehouseId,
                                    float $qtyChange, float $unitCost, float $totalCost, int $userId): void
    {
        $stmt = $this->pdo->prepare(
            "INSERT INTO inventory_transactions 
            (transaction_type, reference_no, product_id, warehouse_id, qty_change, unit_cost, total_cost, created_by)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
        );
        $stmt->execute([$type, $referenceNo, $productId, $warehouseId, $qtyChange, $unitCost, $totalCost, $userId]);
    }
}

调用示例

// 假设有一个盘点单提交
$data = [
    'items' => [
        ['product_id' => 1, 'warehouse_id' => 1, 'actual_qty' => 120], // 账面100,盘盈20
        ['product_id' => 2, 'warehouse_id' => 1, 'actual_qty' => 50],  // 账面55,盘亏5
    ],
    'reason' => '月度盘点,发现差异'
];
$service = new InventoryAdjustmentService($pdo);
$result = $service->executeAdjustment($data, 1);
if ($result['success']) {
    echo "调整单号: " . $result['adjustment_no'];
    // 可在此处生成会计凭证
} else {
    echo "错误: " . $result['message'];
}

重要补充:成本计算与记账

上述示例中的updateInventory方法未处理平均成本重算,在生产环境中必须实现,推荐的做法:

  • 盘盈(增加库存):商品入库,成本取当前平均成本或最新采购价,然后重新计算加权平均成本
  • 盘亏(减少库存):商品出库,成本取当前平均成本,不影响后续平均成本。

会计凭证示例(PHP层面可生成并插入会计凭证表):

// 盘盈:
// 借:库存商品         (diff_qty * unit_cost)
// 贷:待处理财产损溢   (diff_qty * unit_cost)
// 借:待处理财产损溢  贷:管理费用
// 盘亏:
// 借:待处理财产损溢   (diff_qty * unit_cost)
// 贷:库存商品         (diff_qty * unit_cost)
// 借:管理费用      贷:待处理财产损溢

常见问题与优化

  • 并发问题:使用SELECT ... FOR UPDATE锁定库存行,防止同时调整。
  • 大额盘点:不要一次性处理所有商品,可分批次提交事务。
  • 负数库存处理:如果允许负库存(如暂估入库前),调整逻辑需额外验证。
  • 零成本商品:成本为0时调整应给出警告,避免财务数据失真。

这个方案提供了一个可追溯、可审计的库存盈亏处理流程,关键是:

  1. 通过调整单记录差异原因和审批状态。
  2. 通过事务保证库存表、流水表、调整单表数据一致。
  3. 通过单位成本计算盈亏金额,为后续记账提供依据。

抱歉,评论功能暂时关闭!