CaiWuXiTong/app/Business/ThirdPartyWarehouseQuoteRepository.php
limeng 0b00f01566 feat: 三方仓仓储费一键核算+导出核算明细+每期存档
在「第三方仓储报价」页新增仓储费核算:选月份一键按当期生效报价核算
(入库管理费/出库费用/每日仓储费),结果存档并可导出汇总/每日/逐商品三表。

- 新增计算服务 ThirdPartyWarehouseStorageFeeCalculator(口径与报价页公式一致,
  重量取普通商品资料 ops_jst_sku.weight)
- 新增存档表 business_third_party_warehouse_storage_settlements + 模型
- 新增 maatwebsite/excel 三表导出 ThirdPartyWarehouseStorageSettlementExport
- 报价仓库新增 effectiveFor(按仓库+结算月取当期生效报价)
- Livewire 组件与报价页新增核算/导出/历史 UI
- 新增计算服务测试(附 2026-08 真实验收基准 1538.20/6099.60/8376.48/16014.28)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVQ6NsLsbtxm5Ng7Eew3s9
2026-09-15 09:21:05 +08:00

145 lines
5.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Business;
use App\Models\Business\ThirdPartyWarehouseQuote;
use Carbon\Carbon;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Schema;
/**
* 三方仓仓储报价的唯一读写入口。
*
* 结算只需按仓库和结算期查找生效日不晚于该期间的最新启用记录,
* 因此每次价格变化都新增版本,不覆盖旧报价。
*/
class ThirdPartyWarehouseQuoteRepository
{
public function ready(): bool
{
try {
return Schema::hasTable((new ThirdPartyWarehouseQuote())->getTable());
} catch (QueryException) {
return false;
}
}
/** @return array{ready:bool,rows:Collection,summary:array<string,int>} */
public function pageData(): array
{
if (! $this->ready()) {
return $this->emptyPageData();
}
try {
$quotes = ThirdPartyWarehouseQuote::query()
->with('owner')
->orderByDesc('effective_at')
->orderByDesc('id')
->get();
} catch (QueryException) {
return $this->emptyPageData();
}
return [
'ready' => true,
'rows' => $quotes->map(fn (ThirdPartyWarehouseQuote $quote): array => $this->present($quote)),
'summary' => [
'total' => $quotes->count(),
'active' => $quotes->where('status', ThirdPartyWarehouseQuote::STATUS_ACTIVE)->count(),
'warehouses' => $quotes->pluck('warehouse_name')->filter()->unique()->count(),
],
];
}
/**
* 取某仓库在结算月生效的最新启用报价:生效日 ≤ 结算月月末,按生效日、id 倒序取一条。
*/
public function effectiveFor(string $warehouseName, string $period): ?ThirdPartyWarehouseQuote
{
if (! $this->ready()) {
return null;
}
if (preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period) !== 1) {
return null;
}
$monthEnd = Carbon::createFromFormat('Y-m-d', $period . '-01')->endOfMonth()->toDateString();
try {
return ThirdPartyWarehouseQuote::query()
->where('warehouse_name', trim($warehouseName))
->where('status', ThirdPartyWarehouseQuote::STATUS_ACTIVE)
->whereDate('effective_at', '<=', $monthEnd)
->orderByDesc('effective_at')
->orderByDesc('id')
->first();
} catch (QueryException) {
return null;
}
}
/** @param array<string,mixed> $data */
public function create(array $data, int $userId): ThirdPartyWarehouseQuote
{
return ThirdPartyWarehouseQuote::query()->create([
'company_id' => company_id(),
'warehouse_name' => trim((string) $data['warehouse_name']),
'inbound_rate_per_ton' => $data['inbound_rate_per_ton'],
'outbound_rate_per_order' => $data['outbound_rate_per_order'],
'pieces_per_pallet' => $data['pieces_per_pallet'],
'pallet_area_sqm' => $data['pallet_area_sqm'],
'storage_rate_per_sqm_month' => $data['storage_rate_per_sqm_month'],
'effective_at' => $data['effective_at'],
'status' => ThirdPartyWarehouseQuote::STATUS_ACTIVE,
'remark' => trim((string) ($data['remark'] ?? '')) ?: null,
'created_by' => $userId,
'updated_by' => $userId,
]);
}
public function setStatus(ThirdPartyWarehouseQuote $quote, string $status, int $userId): ThirdPartyWarehouseQuote
{
abort_unless(in_array($status, [
ThirdPartyWarehouseQuote::STATUS_ACTIVE,
ThirdPartyWarehouseQuote::STATUS_INACTIVE,
], true), 422, '报价状态不合法。');
$quote->forceFill(['status' => $status, 'updated_by' => $userId])->save();
return $quote->fresh();
}
/** @return array<string,mixed> */
private function present(ThirdPartyWarehouseQuote $quote): array
{
return [
'id' => 'WHQ-' . str_pad((string) $quote->id, 4, '0', STR_PAD_LEFT),
'db_id' => $quote->id,
'warehouse_name' => $quote->warehouse_name,
'inbound_rate_per_ton' => (float) $quote->inbound_rate_per_ton,
'outbound_rate_per_order' => (float) $quote->outbound_rate_per_order,
'pieces_per_pallet' => (float) $quote->pieces_per_pallet,
'pallet_area_sqm' => (float) $quote->pallet_area_sqm,
'storage_rate_per_sqm_month' => (float) $quote->storage_rate_per_sqm_month,
'effective_at' => optional($quote->effective_at)->format('Y-m-d'),
'status_code' => $quote->status,
'status' => $quote->statusLabel(),
'remark' => $quote->remark,
'creator' => optional($quote->owner)->name ?: '-',
];
}
/** @return array{ready:false,rows:Collection,summary:array<string,int>} */
private function emptyPageData(): array
{
return [
'ready' => false,
'rows' => collect(),
'summary' => ['total' => 0, 'active' => 0, 'warehouses' => 0],
];
}
}