join('purchase_order', 'purchase_order.id', '=', 'purchase_order_item.purchase_id') ->whereDate('purchase_order.purchase_date', '>=', $recon->period_start) ->whereDate('purchase_order.purchase_date', '<=', $recon->period_end) ->select('purchase_order_item.*'); if ((int) $recon->supplier_id > 0) { $itemQuery->where('purchase_order_item.supplier_id', $recon->supplier_id); } if ((int) $recon->category_id > 0) { $productIds = ProductModel::withTrashed() ->whereIn('category_id', $this->descendantCategoryIds((int) $recon->category_id)) ->pluck('id'); $itemQuery->whereIn('purchase_order_item.product_id', $productIds); } $purchaseItems = $itemQuery->get(); if ($purchaseItems->isEmpty()) { throw new RepositoryException('周期内无符合筛选条件的采购数据,无法生成对账明细'); } // 2. 分摊记录(未完成分摊的采购单拒绝,保证对账数据到门店/单品粒度) $allocations = PurchaseAllocationModel::query() ->whereIn('purchase_item_id', $purchaseItems->pluck('id')) ->get() ->groupBy('purchase_item_id'); $missing = $purchaseItems->filter(fn ($item) => ! $allocations->has($item->id)); if ($missing->isNotEmpty()) { $purchaseNos = PurchaseOrderModel::query() ->whereIn('id', $missing->pluck('purchase_id')->unique()) ->pluck('purchase_no') ->implode('、'); throw new RepositoryException('采购单 ' . $purchaseNos . ' 尚未完成金额分摊,请先执行分摊再生成对账明细'); } // 3. 溯源订货金额(公布金额) $orderAmounts = StoreOrderItemModel::query() ->whereIn('id', $allocations->flatten()->pluck('order_item_id')->unique()) ->pluck('amount', 'id'); // 4. 先清后建(幂等) ReconciliationItemModel::where('recon_id', $recon->id)->delete(); $publishTotal = '0'; $actualTotal = '0'; $rows = []; $sort = 1; $now = now(); foreach ($allocations as $purchaseItemId => $group) { $purchaseItem = $purchaseItems->firstWhere('id', $purchaseItemId); foreach ($group as $allocation) { $publish = (string) ($orderAmounts[$allocation->order_item_id] ?? '0'); $actual = (string) $allocation->amount; $publishTotal = bcadd($publishTotal, $publish, 2); $actualTotal = bcadd($actualTotal, $actual, 2); $rows[] = [ 'recon_id' => $recon->id, 'store_id' => $allocation->store_id, 'purchase_item_id' => $allocation->purchase_item_id, 'order_item_id' => $allocation->order_item_id, 'product_id' => $allocation->product_id, 'product_name' => $purchaseItem->product_name, 'quantity' => $allocation->quantity, 'weight' => $allocation->weight, 'publish_amount' => $publish, 'actual_amount' => $actual, 'diff_amount' => bcsub($publish, $actual, 2), 'is_reconciled' => ReconciliationItemModel::NOT_RECONCILED, 'store_remark' => '', 'sort' => $sort++, 'created_at' => $now, 'updated_at' => $now, ]; } } ReconciliationItemModel::insert($rows); // 5. 汇总写回头 + 状态流转 $recon->publish_amount = $publishTotal; $recon->actual_amount = $actualTotal; $recon->diff_amount = bcsub($publishTotal, $actualTotal, 2); $recon->status = ReconciliationModel::STATUS_WORKING; $recon->save(); return count($rows); }); } /** * 分类自身 + 全部子孙分类ID(多级分类下按顶级分类筛选) * * @return array */ private function descendantCategoryIds(int $categoryId): array { $parentMap = ProductCategoryModel::pluck('parent_id', 'id'); $ids = [$categoryId]; $queue = [$categoryId]; while ($queue !== []) { $current = array_shift($queue); foreach ($parentMap as $id => $parentId) { if ((int) $parentId === $current && ! in_array((int) $id, $ids, true)) { $ids[] = (int) $id; $queue[] = (int) $id; } } } return $ids; } }