0 ? 称重×成本 : 数量×成本) */ class PurchaseEditService { /** * 门店单元格修改:数量(必填)/ 称重(可空),金额按 数量×单价 重算 */ public function updateCell(StoreOrderItemModel $item, int $quantity, ?float $weight): StoreOrderItemModel { return DB::transaction(function () use ($item, $quantity, $weight) { $item->quantity = $quantity; if ($weight !== null) { $item->weight = bcadd((string) $weight, '0', 3); } $item->amount = bcmul((string) $quantity, (string) $item->price, 2); $item->save(); $order = StoreOrderModel::find($item->order_id); if ($order !== null) { $this->recalculateOrderTotals($order); } if ($item->purchase_id > 0) { $purchase = PurchaseOrderModel::find($item->purchase_id); if ($purchase !== null) { $this->recalculatePurchaseTotals($purchase); } } return $item->refresh(); }); } /** * 商品行修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细; * 实际称重按数量比例分摊(尾差修正守恒); * syncProduct = true 时同步更新商品档案(product 表) * * @param array{product_name?: string, supplier_id?: int, product_spec?: string, unit?: string, cost_price?: float} $attrs 行属性(仅同步传入键) * @param float|null $weight 行实际称重合计,NULL 不修改 */ public function updateRow(PurchaseOrderModel $purchase, int $productId, array $attrs, ?float $weight, bool $syncProduct = false): int { return DB::transaction(function () use ($purchase, $productId, $attrs, $weight, $syncProduct) { $items = StoreOrderItemModel::query() ->where('purchase_id', $purchase->id) ->where('product_id', $productId) ->lockForUpdate() ->get(); if ($items->isEmpty()) { throw new RepositoryException('该采购单下无此商品的订货明细'); } $sync = []; if (isset($attrs['product_name'])) { $sync['product_name'] = $attrs['product_name']; } if (isset($attrs['supplier_id'])) { $sync['supplier_id'] = (int) $attrs['supplier_id']; } if (isset($attrs['product_spec'])) { $sync['product_spec'] = $attrs['product_spec']; } if (isset($attrs['unit'])) { $sync['unit'] = $attrs['unit']; } if (isset($attrs['cost_price'])) { $sync['cost_price'] = bcadd((string) $attrs['cost_price'], '0', 2); } if ($sync !== []) { foreach ($items as $item) { $item->fill($sync)->save(); } // 同步至商品档案(软删除商品不同步,直接报错回滚) if ($syncProduct) { $product = ProductModel::withTrashed()->find($productId); if ($product === null || $product->trashed()) { throw new RepositoryException('商品档案不存在或已删除,无法同步至商品'); } $productAttrs = []; if (isset($sync['product_name'])) { $productAttrs['name'] = $sync['product_name']; } if (isset($sync['supplier_id'])) { $productAttrs['supplier_id'] = $sync['supplier_id']; } if (isset($sync['product_spec'])) { $productAttrs['spec'] = $sync['product_spec']; } if (isset($sync['unit'])) { $productAttrs['unit'] = $sync['unit']; } if (isset($sync['cost_price'])) { $productAttrs['cost_price'] = $sync['cost_price']; } $product->fill($productAttrs)->save(); } } if ($weight !== null) { $totalQty = $items->reduce( static fn (string $carry, $item): string => bcadd($carry, (string) $item->quantity, 2), '0' ); $target = bcadd((string) $weight, '0', 3); $allocated = '0'; $lastIndex = $items->count() - 1; foreach ($items->values() as $index => $item) { if ($index === $lastIndex) { // 尾差修正:最后一行 = 总称重 − 已分摊,保证守恒 $rowWeight = bcsub($target, $allocated, 3); } elseif ((float) $totalQty > 0) { $ratio = bcdiv((string) $item->quantity, $totalQty, 6); $rowWeight = bcmul($target, $ratio, 3); $allocated = bcadd($allocated, $rowWeight, 3); } else { $rowWeight = '0'; } $item->weight = $rowWeight; $item->save(); } } $orderIds = $items->pluck('order_id')->unique()->all(); StoreOrderModel::whereIn('id', $orderIds)->get() ->each(fn (StoreOrderModel $order) => $this->recalculateOrderTotals($order)); $this->recalculatePurchaseTotals($purchase); return $items->count(); }); } /** * 重算订单汇总(与 StoreOrderController::recalculateOrderTotals 同口径) */ public function recalculateOrderTotals(StoreOrderModel $order): void { $totals = StoreOrderItemModel::query() ->where('order_id', $order->id) ->selectRaw('COALESCE(SUM(quantity), 0) as total_quantity') ->selectRaw('COALESCE(SUM(weight), 0) as total_weight') ->selectRaw('COALESCE(SUM(amount), 0) as product_amount') ->first(); $productAmount = bcadd((string) $totals->product_amount, '0', 2); $order->total_quantity = (int) $totals->total_quantity; $order->total_weight = bcadd((string) $totals->total_weight, '0', 3); $order->product_amount = $productAmount; $order->total_amount = bcadd($productAmount, (string) $order->added_amount, 2); $order->save(); } /** * 重算采购单头汇总:estimate_amount = Σ订货金额(销售口径); * actual_amount = Σ(称重>0 ? 称重×成本 : 数量×成本)(采购成本口径) */ public function recalculatePurchaseTotals(PurchaseOrderModel $purchase): void { $items = StoreOrderItemModel::query() ->where('purchase_id', $purchase->id) ->get(['quantity', 'weight', 'amount', 'cost_price', 'product_spec']); $totalQuantity = '0'; $totalWeight = '0'; $estimate = '0'; $actual = '0'; foreach ($items as $item) { $unitCost = self::unitCost((string) ($item->cost_price ?? '0'), (string) $item->product_spec); $totalQuantity = bcadd($totalQuantity, (string) $item->quantity, 2); $totalWeight = bcadd($totalWeight, (string) $item->weight, 3); $estimate = bcadd($estimate, (string) $item->amount, 2); $actual = bcadd($actual, self::costAmount((string) $item->quantity, (string) $item->weight, $unitCost), 2); } $purchase->total_quantity = $totalQuantity; $purchase->total_weight = $totalWeight; $purchase->estimate_amount = $estimate; $purchase->actual_amount = $actual; $purchase->save(); } /** * 采购成本金额:称重>0 按 称重×单价,否则按 数量×单价 */ public static function costAmount(string $quantity, string $weight, string $unitCost): string { return (float) $weight > 0 ? bcmul($weight, $unitCost, 2) : bcmul($quantity, $unitCost, 2); } /** * 单价 = 成本 / 包规数值(spec 解析不出正数时按 1 处理,即单价=成本) */ public static function unitCost(string $costPrice, string $spec): string { if (preg_match('/\d+(?:\.\d+)?/', $spec, $matches) === 1 && (float) $matches[0] > 0) { return bcdiv($costPrice, $matches[0], 2); } return bcadd($costPrice, '0', 2); } }