本文目录导读:

在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时调整应给出警告,避免财务数据失真。
这个方案提供了一个可追溯、可审计的库存盈亏处理流程,关键是:
- 通过调整单记录差异原因和审批状态。
- 通过事务保证库存表、流水表、调整单表数据一致。
- 通过单位成本计算盈亏金额,为后续记账提供依据。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播