在「第三方仓储报价」页新增仓储费核算:选月份一键按当期生效报价核算 (入库管理费/出库费用/每日仓储费),结果存档并可导出汇总/每日/逐商品三表。 - 新增计算服务 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
145 lines
5.2 KiB
PHP
145 lines
5.2 KiB
PHP
<?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],
|
||
];
|
||
}
|
||
}
|