,daily:list>,per_sku:list>,daily_sku:list>} */ public function calculate(int $warehouseId, string $warehouseName, string $period): array { abort_unless( preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period) === 1, 422, '结算月份格式应为 YYYY-MM。', ); $quote = $this->quotes->effectiveFor($warehouseName, $period); abort_if($quote === null, 422, '请先在「第三方仓储报价」录入该仓库在 ' . $period . ' 生效的报价。'); $monthStart = Carbon::createFromFormat('Y-m-d', $period . '-01')->startOfMonth(); $days = (int) $monthStart->daysInMonth; $inboundRate = (float) $quote->inbound_rate_per_ton; $outboundRate = (float) $quote->outbound_rate_per_order; $piecesPerPallet = (float) $quote->pieces_per_pallet; $palletArea = (float) $quote->pallet_area_sqm; $sqmRate = (float) $quote->storage_rate_per_sqm_month; /** @var array $daily */ $daily = []; /** @var array> $skus */ $skus = []; $dailySku = []; $storagePerPiecePerDay = $days > 0 && $piecesPerPallet > 0 ? $palletArea * $sqmRate / $piecesPerPallet / $days : 0.0; foreach ($this->mirror->exportRows(['dimension' => 'month', 'period' => $period], 5000) as $row) { $date = trim((string) ($row['date'] ?? '')); $code = trim((string) ($row['sku_code'] ?? '')); if ($date === '' || $code === '') { continue; } $opening = (float) ($row['opening_qty'] ?? 0); $closing = (float) ($row['closing_qty'] ?? 0); $inboundQty = 0.0; foreach (self::INBOUND_COLUMNS as $column) { $inboundQty += abs((float) ($row[$column] ?? 0)); } $outboundQty = 0.0; foreach (self::OUTBOUND_COLUMNS as $column) { $outboundQty += abs((float) ($row[$column] ?? 0)); } if (! isset($daily[$date])) { $daily[$date] = ['closing' => 0.0, 'inbound_qty' => 0.0, 'outbound_qty' => 0.0]; } $daily[$date]['closing'] += $closing; $daily[$date]['inbound_qty'] += $inboundQty; $daily[$date]['outbound_qty'] += $outboundQty; $dailySku[] = [ 'date' => $date, 'owner' => (string) ($row['owner'] ?? ''), 'sku_code' => $code, 'product_name' => (string) ($row['product_name'] ?? ''), 'supplier' => (string) ($row['supplier'] ?? ''), 'category' => (string) ($row['category'] ?? ''), 'unit' => (string) ($row['unit'] ?? ''), 'opening' => round($opening, 2), 'net_movement' => round($closing - $opening, 2), 'closing' => round($closing, 2), 'storage_fee' => round($closing * $storagePerPiecePerDay, 6), ]; if (! isset($skus[$code])) { $skus[$code] = [ 'owner' => (string) ($row['owner'] ?? ''), 'sku_code' => $code, 'product_name' => (string) ($row['product_name'] ?? ''), 'supplier' => (string) ($row['supplier'] ?? ''), 'category' => (string) ($row['category'] ?? ''), 'unit' => (string) ($row['unit'] ?? ''), 'first_date' => $date, 'last_date' => $date, 'opening' => $opening, 'closing' => $closing, 'inbound_qty' => 0.0, 'outbound_qty' => 0.0, ]; } $sku = &$skus[$code]; if ($date <= $sku['first_date']) { $sku['first_date'] = $date; $sku['opening'] = $opening; } if ($date >= $sku['last_date']) { $sku['last_date'] = $date; $sku['closing'] = $closing; } $sku['inbound_qty'] += $inboundQty; $sku['outbound_qty'] += $outboundQty; unset($sku); } $weightMap = $this->weights->weightsBySkuCodes(array_keys($skus)); $inboundTons = 0.0; $outboundQtyTotal = 0.0; $perSku = []; foreach ($skus as $code => $sku) { $weight = (float) ($weightMap[$code] ?? 0.0); $perTon = $weight / 1000; $totalTon = $perTon * $sku['inbound_qty']; $inboundTons += $totalTon; $outboundQtyTotal += $sku['outbound_qty']; $perSku[] = [ 'owner' => $sku['owner'], 'sku_code' => $code, 'product_name' => $sku['product_name'], 'supplier' => $sku['supplier'], 'category' => $sku['category'], 'unit' => $sku['unit'], 'weight' => round($weight, 4), 'per_ton' => round($perTon, 6), 'inbound_qty' => round($sku['inbound_qty'], 2), 'total_ton' => round($totalTon, 4), 'outbound_qty' => round($sku['outbound_qty'], 2), 'opening' => round($sku['opening'], 2), 'closing' => round($sku['closing'], 2), ]; } $inboundFee = round($inboundTons * $inboundRate, 2); $outboundFee = round($outboundQtyTotal * $outboundRate, 2); ksort($daily); $dailyRows = []; $storageFee = 0.0; foreach ($daily as $date => $agg) { $dayStorage = $days > 0 && $piecesPerPallet > 0 ? $agg['closing'] / $piecesPerPallet * $palletArea * $sqmRate / $days : 0.0; $storageFee += $dayStorage; $dailyRows[] = [ 'date' => $date, 'inbound_qty' => round($agg['inbound_qty'], 2), 'outbound_qty' => round($agg['outbound_qty'], 2), 'closing' => round($agg['closing'], 2), 'storage_fee' => round($dayStorage, 4), 'outbound_fee' => round($agg['outbound_qty'] * $outboundRate, 2), 'total' => round($dayStorage + $agg['outbound_qty'] * $outboundRate, 2), ]; } $storageFee = round($storageFee, 2); $totalFee = round($inboundFee + $outboundFee + $storageFee, 2); return [ 'summary' => [ 'period' => $period, 'warehouse_id' => $warehouseId, 'warehouse_name' => $warehouseName, 'quote_id' => (int) $quote->id, 'days_in_month' => $days, 'inbound_rate_per_ton' => $inboundRate, 'outbound_rate_per_order' => $outboundRate, 'pieces_per_pallet' => $piecesPerPallet, 'pallet_area_sqm' => $palletArea, 'storage_rate_per_sqm_month' => $sqmRate, 'inbound_tons' => round($inboundTons, 4), 'inbound_fee' => $inboundFee, 'outbound_qty' => round($outboundQtyTotal, 2), 'outbound_fee' => $outboundFee, 'storage_fee' => $storageFee, 'memo_fee' => 0.0, 'loss_fee' => 0.0, 'compensation_fee' => 0.0, 'total_fee' => $totalFee, ], 'daily' => $dailyRows, 'per_sku' => $perSku, 'daily_sku' => $dailySku, ]; } }