库存成本核算——移动加权平均法实战
引言 在跨境电商业务中,库存成本核算是一个看似简单实则复杂的问题。同一个SKU,不同批次的采购价格可能差异很大——汇率波动、供应商调价、运费变化都会影响入库成本。当这个SKU出库时,应该按什么成本计算? 这个问题直接影响: 毛利计算:成本算错,毛利就错 库存估值:影响资产负债表 定价决策:成本不准,定价就没有依据 税务合规:成本核算方法需要符合会计准则 本文将深入讲解库存成本核算的核心方法,重点介绍移动加权平均法的算法实现、特殊场景处理和月结流程设计。 一、成本核算方法对比 1.1 三种主流方法 方法 原理 优点 缺点 适用场景 先进先出法(FIFO) 先入库的先出库 符合实物流转,成本反映真实 计算复杂,需追踪批次 有保质期的商品 移动加权平均法 每次入库重新计算平均成本 成本平滑,计算相对简单 无法追溯具体批次成本 标准化商品 个别计价法 每个商品单独计价 成本最准确 管理成本高 高价值、可识别商品 1.2 跨境电商的选择 对于年营收5-7亿的跨境电商,移动加权平均法是最佳选择: 选择理由: SKU数量大:通常有数万SKU,FIFO管理成本太高 标准化商品:大部分是标准化产品,无需追溯批次 价格波动频繁:汇率、运费变化大,平均法能平滑波动 财务软件兼容:金蝶、用友等主流财务软件都支持 不适用场景: 有保质期的商品(食品、化妆品)→ 建议FIFO 高价值单品(珠宝、艺术品)→ 建议个别计价 有序列号管理需求的商品 → 建议个别计价 二、移动加权平均算法详解 2.1 核心公式 新单位成本 = (原库存金额 + 本次入库金额) / (原库存数量 + 本次入库数量) 示例: 原库存:100件,单位成本10元,库存金额1000元 本次入库:50件,单位成本12元,入库金额600元 新单位成本 = (1000 + 600) / (100 + 50) = 10.67元 2.2 数据模型设计 -- 库存成本主表 CREATE TABLE inventory_cost ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_code VARCHAR(50) NOT NULL COMMENT 'SKU编码', warehouse_code VARCHAR(50) NOT NULL COMMENT '仓库编码', quantity DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '当前库存数量', unit_cost DECIMAL(18,6) NOT NULL DEFAULT 0 COMMENT '单位成本(6位小数)', total_cost DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '库存总成本', last_in_cost DECIMAL(18,6) COMMENT '最近入库成本', last_in_time DATETIME COMMENT '最近入库时间', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_sku_warehouse (sku_code, warehouse_code) ) COMMENT '库存成本主表'; -- 成本变动流水表 CREATE TABLE inventory_cost_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_code VARCHAR(50) NOT NULL COMMENT 'SKU编码', warehouse_code VARCHAR(50) NOT NULL COMMENT '仓库编码', biz_type VARCHAR(30) NOT NULL COMMENT '业务类型:PURCHASE_IN/SALE_OUT/RETURN_IN/TRANSFER/ADJUST', biz_no VARCHAR(50) NOT NULL COMMENT '业务单号', direction TINYINT NOT NULL COMMENT '方向:1入库,-1出库', quantity DECIMAL(18,4) NOT NULL COMMENT '变动数量', unit_cost DECIMAL(18,6) NOT NULL COMMENT '本次单位成本', amount DECIMAL(18,4) NOT NULL COMMENT '本次金额', before_quantity DECIMAL(18,4) NOT NULL COMMENT '变动前数量', before_unit_cost DECIMAL(18,6) NOT NULL COMMENT '变动前单位成本', before_total_cost DECIMAL(18,4) NOT NULL COMMENT '变动前总成本', after_quantity DECIMAL(18,4) NOT NULL COMMENT '变动后数量', after_unit_cost DECIMAL(18,6) NOT NULL COMMENT '变动后单位成本', after_total_cost DECIMAL(18,4) NOT NULL COMMENT '变动后总成本', created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_sku_warehouse (sku_code, warehouse_code), INDEX idx_biz_no (biz_no), INDEX idx_created_time (created_time) ) COMMENT '成本变动流水表'; 2.3 核心算法实现 /** * 库存成本服务 * 实现移动加权平均法 */ @Service @Slf4j public class InventoryCostService { @Autowired private InventoryCostMapper costMapper; @Autowired private InventoryCostLogMapper logMapper; /** * 入库成本计算 * 核心公式:新单位成本 = (原库存金额 + 本次入库金额) / (原库存数量 + 本次入库数量) */ @Transactional(rollbackFor = Exception.class) public void processInbound(CostChangeRequest request) { String skuCode = request.getSkuCode(); String warehouseCode = request.getWarehouseCode(); BigDecimal inQuantity = request.getQuantity(); BigDecimal inUnitCost = request.getUnitCost(); // 1. 获取当前库存成本(加锁) InventoryCost cost = costMapper.selectForUpdate(skuCode, warehouseCode); // 2. 记录变动前状态 CostSnapshot before = createSnapshot(cost); // 3. 计算新成本 BigDecimal beforeQuantity = cost != null ? cost.getQuantity() : BigDecimal.ZERO; BigDecimal beforeTotalCost = cost != null ? cost.getTotalCost() : BigDecimal.ZERO; BigDecimal inAmount = inQuantity.multiply(inUnitCost); BigDecimal afterQuantity = beforeQuantity.add(inQuantity); BigDecimal afterTotalCost = beforeTotalCost.add(inAmount); // 移动加权平均计算 BigDecimal afterUnitCost; if (afterQuantity.compareTo(BigDecimal.ZERO) > 0) { afterUnitCost = afterTotalCost.divide(afterQuantity, 6, RoundingMode.HALF_UP); } else { afterUnitCost = inUnitCost; // 库存为0时,使用入库成本 } // 4. 更新库存成本 if (cost == null) { cost = new InventoryCost(); cost.setSkuCode(skuCode); cost.setWarehouseCode(warehouseCode); cost.setQuantity(afterQuantity); cost.setUnitCost(afterUnitCost); cost.setTotalCost(afterTotalCost); cost.setLastInCost(inUnitCost); cost.setLastInTime(new Date()); costMapper.insert(cost); } else { cost.setQuantity(afterQuantity); cost.setUnitCost(afterUnitCost); cost.setTotalCost(afterTotalCost); cost.setLastInCost(inUnitCost); cost.setLastInTime(new Date()); int rows = costMapper.updateWithVersion(cost); if (rows == 0) { throw new ConcurrentModificationException("库存成本更新冲突,请重试"); } } // 5. 记录成本流水 CostSnapshot after = createSnapshot(cost); saveCostLog(request, before, after, 1); log.info("入库成本计算完成: sku={}, warehouse={}, 入库数量={}, 入库成本={}, " + "新单位成本={}, 新库存数量={}", skuCode, warehouseCode, inQuantity, inUnitCost, afterUnitCost, afterQuantity); } /** * 出库成本计算 * 出库时按当前单位成本计算 */ @Transactional(rollbackFor = Exception.class) public BigDecimal processOutbound(CostChangeRequest request) { String skuCode = request.getSkuCode(); String warehouseCode = request.getWarehouseCode(); BigDecimal outQuantity = request.getQuantity(); // 1. 获取当前库存成本(加锁) InventoryCost cost = costMapper.selectForUpdate(skuCode, warehouseCode); if (cost == null || cost.getQuantity().compareTo(outQuantity) < 0) { throw new BusinessException("库存不足,无法出库"); } // 2. 记录变动前状态 CostSnapshot before = createSnapshot(cost); // 3. 计算出库金额(使用当前单位成本) BigDecimal outUnitCost = cost.getUnitCost(); BigDecimal outAmount = outQuantity.multiply(outUnitCost); // 4. 更新库存 BigDecimal afterQuantity = cost.getQuantity().subtract(outQuantity); BigDecimal afterTotalCost = cost.getTotalCost().subtract(outAmount); // 单位成本保持不变(出库不改变单位成本) cost.setQuantity(afterQuantity); cost.setTotalCost(afterTotalCost); int rows = costMapper.updateWithVersion(cost); if (rows == 0) { throw new ConcurrentModificationException("库存成本更新冲突,请重试"); } // 5. 记录成本流水 CostSnapshot after = createSnapshot(cost); request.setUnitCost(outUnitCost); // 设置出库成本 saveCostLog(request, before, after, -1); log.info("出库成本计算完成: sku={}, warehouse={}, 出库数量={}, 出库成本={}, " + "出库金额={}", skuCode, warehouseCode, outQuantity, outUnitCost, outAmount); return outAmount; // 返回出库金额,用于计算销售成本 } /** * 创建成本快照 */ private CostSnapshot createSnapshot(InventoryCost cost) { CostSnapshot snapshot = new CostSnapshot(); if (cost != null) { snapshot.setQuantity(cost.getQuantity()); snapshot.setUnitCost(cost.getUnitCost()); snapshot.setTotalCost(cost.getTotalCost()); } else { snapshot.setQuantity(BigDecimal.ZERO); snapshot.setUnitCost(BigDecimal.ZERO); snapshot.setTotalCost(BigDecimal.ZERO); } return snapshot; } /** * 保存成本流水 */ private void saveCostLog(CostChangeRequest request, CostSnapshot before, CostSnapshot after, int direction) { InventoryCostLog log = new InventoryCostLog(); log.setSkuCode(request.getSkuCode()); log.setWarehouseCode(request.getWarehouseCode()); log.setBizType(request.getBizType()); log.setBizNo(request.getBizNo()); log.setDirection(direction); log.setQuantity(request.getQuantity()); log.setUnitCost(request.getUnitCost()); log.setAmount(request.getQuantity().multiply(request.getUnitCost())); log.setBeforeQuantity(before.getQuantity()); log.setBeforeUnitCost(before.getUnitCost()); log.setBeforeTotalCost(before.getTotalCost()); log.setAfterQuantity(after.getQuantity()); log.setAfterUnitCost(after.getUnitCost()); log.setAfterTotalCost(after.getTotalCost()); logMapper.insert(log); } } 2.4 并发控制 库存成本计算必须保证并发安全,否则会导致成本计算错误。 ...