本文目录导读:

在PHP项目中处理盈亏并调整库存账面数据,核心在于区分业务类型(如盘点盈亏、报损报溢、非销售出入库等)以及确保会计记账逻辑正确,下面是一个通用的、可落地的实现思路和代码示例。
核心原则
- 不可直接修改历史记录:不能去修改已经发生的入库单或销售单的数量,应通过调整单或盘盈盘亏单来处理。
- 保证库存数量与账面价值一致:调整后,库存表(
inventory)的quantity和cost_price(或加权平均成本)要正确。 - 记录财务凭证:调整库存必然涉及“待处理财产损溢”科目,最终要转入“管理费用”或“营业外收支”。
数据库表结构设计
你需要至少以下核心表(以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时调整应给出警告,避免财务数据失真。
这个方案提供了一个可追溯、可审计的库存盈亏处理流程,关键是:
- 通过调整单记录差异原因和审批状态。
- 通过事务保证库存表、流水表、调整单表数据一致。
- 通过单位成本计算盈亏金额,为后续记账提供依据。