401 lines
15 KiB
PHP
401 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Livewire\Business;
|
|
|
|
use App\Business\ThirdPartyWarehouseQuoteRepository;
|
|
use App\Business\ThirdPartyWarehouseStorageFeeCalculator;
|
|
use App\Exports\ThirdPartyWarehouseStorageDetailExport;
|
|
use App\Exports\ThirdPartyWarehouseStorageSettlementExport;
|
|
use App\Models\Business\ThirdPartyWarehouseQuote;
|
|
use App\Models\Business\ThirdPartyWarehouseStorageSettlement;
|
|
use Illuminate\Contracts\View\View;
|
|
use Livewire\Component;
|
|
use Maatwebsite\Excel\Facades\Excel;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
|
|
class ThirdPartyWarehouseQuotes extends Component
|
|
{
|
|
/** 固定核算「三方-南昌樵舍仓」,与镜像仓库一致。 */
|
|
public const WAREHOUSE_ID = 15045364;
|
|
|
|
public string $warehouseName = '南昌樵舍仓';
|
|
|
|
public string $effectiveAt = '';
|
|
|
|
public string $inboundRatePerTon = '20';
|
|
|
|
public string $outboundRatePerOrder = '0.85';
|
|
|
|
public string $piecesPerPallet = '23';
|
|
|
|
public string $palletAreaSqm = '3';
|
|
|
|
public string $storageRatePerSqmMonth = '10.5';
|
|
|
|
public string $remark = '';
|
|
|
|
public ?string $message = null;
|
|
|
|
/** 仓储费核算 */
|
|
public string $settlementPeriod = '';
|
|
|
|
/** @var array<string,mixed>|null */
|
|
public ?array $settlementSummary = null;
|
|
|
|
public ?string $settlementMessage = null;
|
|
|
|
public ?string $settlementError = null;
|
|
|
|
public ?string $settlementDetailPeriod = null;
|
|
|
|
public string $settlementDetailDate = '';
|
|
|
|
/** @var list<string> */
|
|
public array $settlementDetailDates = [];
|
|
|
|
public ?string $settlementDetailMessage = null;
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->effectiveAt = now()->toDateString();
|
|
$this->settlementPeriod = now()->startOfMonth()->subMonth()->format('Y-m');
|
|
}
|
|
|
|
public function save(): void
|
|
{
|
|
$this->ensureAuthenticated();
|
|
abort_unless($this->repository()->ready(), 503, '仓储报价数据表尚未完成迁移,请联系管理员。');
|
|
|
|
$this->warehouseName = trim($this->warehouseName);
|
|
$data = $this->validate();
|
|
$this->repository()->create([
|
|
'warehouse_name' => $data['warehouseName'],
|
|
'effective_at' => $data['effectiveAt'],
|
|
'inbound_rate_per_ton' => $data['inboundRatePerTon'],
|
|
'outbound_rate_per_order' => $data['outboundRatePerOrder'],
|
|
'pieces_per_pallet' => $data['piecesPerPallet'],
|
|
'pallet_area_sqm' => $data['palletAreaSqm'],
|
|
'storage_rate_per_sqm_month' => $data['storageRatePerSqmMonth'],
|
|
'remark' => $data['remark'],
|
|
], (int) user_id());
|
|
|
|
$this->remark = '';
|
|
$this->message = '仓储报价已保存,新报价不会覆盖历史版本。';
|
|
}
|
|
|
|
public function setStatus(int $quoteId, string $status): void
|
|
{
|
|
$this->ensureAuthenticated();
|
|
|
|
$quote = ThirdPartyWarehouseQuote::query()->findOrFail($quoteId);
|
|
abort_unless((int) $quote->company_id === (int) company_id(), 404, '仓储报价不存在。');
|
|
|
|
$this->repository()->setStatus($quote, $status, (int) user_id());
|
|
$this->message = '仓储报价状态已更新。';
|
|
}
|
|
|
|
public function calculateStorageFee(): void
|
|
{
|
|
$this->ensureAuthenticated();
|
|
$this->settlementMessage = null;
|
|
$this->settlementError = null;
|
|
|
|
$this->validate([
|
|
'settlementPeriod' => ['required', 'regex:/^\d{4}-(0[1-9]|1[0-2])$/'],
|
|
], [
|
|
'settlementPeriod.regex' => '结算月份格式应为 YYYY-MM。',
|
|
]);
|
|
|
|
try {
|
|
$result = app(ThirdPartyWarehouseStorageFeeCalculator::class)
|
|
->calculate(self::WAREHOUSE_ID, $this->warehouseName, $this->settlementPeriod);
|
|
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
|
|
$this->settlementError = $e->getMessage();
|
|
|
|
return;
|
|
}
|
|
|
|
$summary = $result['summary'];
|
|
ThirdPartyWarehouseStorageSettlement::query()->updateOrCreate(
|
|
[
|
|
'company_id' => (int) company_id(),
|
|
'warehouse_id' => self::WAREHOUSE_ID,
|
|
'period' => $this->settlementPeriod,
|
|
],
|
|
[
|
|
'warehouse_name' => $this->warehouseName,
|
|
'quote_id' => $summary['quote_id'] ?? null,
|
|
'inbound_rate_per_ton' => $summary['inbound_rate_per_ton'],
|
|
'outbound_rate_per_order' => $summary['outbound_rate_per_order'],
|
|
'pieces_per_pallet' => $summary['pieces_per_pallet'],
|
|
'pallet_area_sqm' => $summary['pallet_area_sqm'],
|
|
'storage_rate_per_sqm_month' => $summary['storage_rate_per_sqm_month'],
|
|
'days_in_month' => $summary['days_in_month'],
|
|
'inbound_tons' => $summary['inbound_tons'],
|
|
'outbound_qty' => $summary['outbound_qty'],
|
|
'inbound_fee' => $summary['inbound_fee'],
|
|
'outbound_fee' => $summary['outbound_fee'],
|
|
'storage_fee' => $summary['storage_fee'],
|
|
'memo_fee' => $summary['memo_fee'],
|
|
'loss_fee' => $summary['loss_fee'],
|
|
'compensation_fee' => $summary['compensation_fee'],
|
|
'total_fee' => $summary['total_fee'],
|
|
'detail_json' => [
|
|
'daily' => $result['daily'],
|
|
'per_sku' => $result['per_sku'],
|
|
'daily_sku' => $result['daily_sku'],
|
|
],
|
|
'calculated_by' => (int) user_id(),
|
|
],
|
|
);
|
|
|
|
$this->settlementSummary = $summary;
|
|
$this->settlementMessage = $this->settlementPeriod . ' 仓储费核算完成,已存档。';
|
|
}
|
|
|
|
public function exportSettlement(string $period): ?BinaryFileResponse
|
|
{
|
|
$this->ensureAuthenticated();
|
|
|
|
$settlement = ThirdPartyWarehouseStorageSettlement::query()
|
|
->where('company_id', (int) company_id())
|
|
->where('warehouse_id', self::WAREHOUSE_ID)
|
|
->where('period', $period)
|
|
->first();
|
|
|
|
if ($settlement === null) {
|
|
$this->settlementError = '未找到 ' . $period . ' 的核算存档,请先核算。';
|
|
|
|
return null;
|
|
}
|
|
|
|
$detail = $settlement->detail_json ?: ['daily' => [], 'per_sku' => [], 'daily_sku' => []];
|
|
$result = [
|
|
'summary' => $this->summaryFromSettlement($settlement),
|
|
'daily' => $detail['daily'] ?? [],
|
|
'per_sku' => $detail['per_sku'] ?? [],
|
|
'daily_sku' => $detail['daily_sku'] ?? [],
|
|
];
|
|
|
|
$fileName = $settlement->warehouse_name . '仓储费用_' . $period . '.xlsx';
|
|
|
|
return Excel::download(new ThirdPartyWarehouseStorageSettlementExport($result), $fileName);
|
|
}
|
|
|
|
public function exportSettlementDetails(string $period): ?BinaryFileResponse
|
|
{
|
|
$this->ensureAuthenticated();
|
|
abort_unless(preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period) === 1, 422);
|
|
|
|
$settlement = $this->settlementForPeriod($period);
|
|
if ($settlement === null) {
|
|
$this->settlementError = '未找到 ' . $period . ' 的核算存档,请先核算。';
|
|
|
|
return null;
|
|
}
|
|
|
|
$dailySku = ($settlement->detail_json ?: [])['daily_sku'] ?? [];
|
|
if ($dailySku === []) {
|
|
$this->showMissingSettlementDetails($period);
|
|
|
|
return null;
|
|
}
|
|
|
|
$fileName = $settlement->warehouse_name . '每日商品仓储费明细_' . $period . '.xlsx';
|
|
|
|
return Excel::download(new ThirdPartyWarehouseStorageDetailExport($dailySku), $fileName);
|
|
}
|
|
|
|
public function viewSettlementDetails(string $period): void
|
|
{
|
|
$this->ensureAuthenticated();
|
|
abort_unless(preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period) === 1, 422);
|
|
|
|
$this->settlementDetailPeriod = $period;
|
|
$this->settlementDetailDate = '';
|
|
$this->settlementDetailDates = [];
|
|
$this->settlementDetailMessage = null;
|
|
|
|
$settlement = $this->settlementForPeriod($period);
|
|
if ($settlement === null) {
|
|
$this->settlementDetailMessage = '未找到 ' . $period . ' 的核算存档。';
|
|
$this->dispatch('warehouse-settlement-details-opened');
|
|
|
|
return;
|
|
}
|
|
|
|
$detail = $settlement->detail_json ?: [];
|
|
$dailySku = $detail['daily_sku'] ?? [];
|
|
if ($dailySku === []) {
|
|
$this->showMissingSettlementDetails($period);
|
|
|
|
return;
|
|
}
|
|
|
|
$dates = array_values(array_unique(array_column($dailySku, 'date')));
|
|
sort($dates);
|
|
$this->settlementDetailDates = $dates;
|
|
$this->settlementDetailDate = $dates[0] ?? '';
|
|
$this->dispatch('warehouse-settlement-details-opened');
|
|
}
|
|
|
|
public function closeSettlementDetails(): void
|
|
{
|
|
$this->settlementDetailPeriod = null;
|
|
$this->settlementDetailDate = '';
|
|
$this->settlementDetailDates = [];
|
|
$this->settlementDetailMessage = null;
|
|
}
|
|
|
|
public function render(): View
|
|
{
|
|
$data = $this->repository()->pageData();
|
|
|
|
$history = ThirdPartyWarehouseStorageSettlement::query()
|
|
->where('company_id', (int) company_id())
|
|
->where('warehouse_id', self::WAREHOUSE_ID)
|
|
->orderByDesc('period')
|
|
->limit(24)
|
|
->get()
|
|
->map(fn (ThirdPartyWarehouseStorageSettlement $row): array => [
|
|
'period' => $row->period,
|
|
'total_fee' => (float) $row->total_fee,
|
|
'inbound_fee' => (float) $row->inbound_fee,
|
|
'outbound_fee' => (float) $row->outbound_fee,
|
|
'storage_fee' => (float) $row->storage_fee,
|
|
'calculated_at' => optional($row->updated_at)->format('Y-m-d H:i'),
|
|
])
|
|
->all();
|
|
$settlementDetail = $this->settlementDetailData();
|
|
|
|
return view('business.pages.courier-payables.partials.third-party-warehouse-quotes', [
|
|
'warehouseQuotes' => $data['rows'],
|
|
'warehouseQuoteSummary' => $data['summary'],
|
|
'warehouseQuotesReady' => $data['ready'],
|
|
'settlementHistory' => $history,
|
|
'settlementDetailRows' => $settlementDetail['rows'],
|
|
'settlementDetailSummary' => $settlementDetail['summary'],
|
|
]);
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
private function summaryFromSettlement(ThirdPartyWarehouseStorageSettlement $s): array
|
|
{
|
|
return [
|
|
'period' => $s->period,
|
|
'warehouse_id' => (int) $s->warehouse_id,
|
|
'warehouse_name' => $s->warehouse_name,
|
|
'quote_id' => $s->quote_id,
|
|
'days_in_month' => (int) $s->days_in_month,
|
|
'inbound_rate_per_ton' => (float) $s->inbound_rate_per_ton,
|
|
'outbound_rate_per_order' => (float) $s->outbound_rate_per_order,
|
|
'pieces_per_pallet' => (float) $s->pieces_per_pallet,
|
|
'pallet_area_sqm' => (float) $s->pallet_area_sqm,
|
|
'storage_rate_per_sqm_month' => (float) $s->storage_rate_per_sqm_month,
|
|
'inbound_tons' => (float) $s->inbound_tons,
|
|
'inbound_fee' => (float) $s->inbound_fee,
|
|
'outbound_qty' => (float) $s->outbound_qty,
|
|
'outbound_fee' => (float) $s->outbound_fee,
|
|
'storage_fee' => (float) $s->storage_fee,
|
|
'memo_fee' => (float) $s->memo_fee,
|
|
'loss_fee' => (float) $s->loss_fee,
|
|
'compensation_fee' => (float) $s->compensation_fee,
|
|
'total_fee' => (float) $s->total_fee,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 明细行只参与当前服务端渲染,不进入 Livewire 公共快照,避免日期切换时回传整张表。
|
|
*
|
|
* @return array{rows:list<array<string,mixed>>,summary:array<string,float|int>|null}
|
|
*/
|
|
private function settlementDetailData(): array
|
|
{
|
|
$empty = ['rows' => [], 'summary' => null];
|
|
|
|
if ($this->settlementDetailPeriod === null
|
|
|| ! in_array($this->settlementDetailDate, $this->settlementDetailDates, true)) {
|
|
return $empty;
|
|
}
|
|
|
|
$settlement = $this->settlementForPeriod($this->settlementDetailPeriod);
|
|
if ($settlement === null) {
|
|
return $empty;
|
|
}
|
|
|
|
$detail = $settlement->detail_json ?: [];
|
|
$rows = array_values(array_filter(
|
|
$detail['daily_sku'] ?? [],
|
|
fn (array $row): bool => ($row['date'] ?? null) === $this->settlementDetailDate,
|
|
));
|
|
usort($rows, static fn (array $left, array $right): int => strcmp(
|
|
(string) ($left['sku_code'] ?? ''),
|
|
(string) ($right['sku_code'] ?? ''),
|
|
));
|
|
|
|
return [
|
|
'rows' => $rows,
|
|
'summary' => [
|
|
'sku_count' => count($rows),
|
|
'opening' => array_sum(array_column($rows, 'opening')),
|
|
'net_movement' => array_sum(array_column($rows, 'net_movement')),
|
|
'closing' => array_sum(array_column($rows, 'closing')),
|
|
'storage_fee' => array_sum(array_column($rows, 'storage_fee')),
|
|
],
|
|
];
|
|
}
|
|
|
|
private function settlementForPeriod(string $period): ?ThirdPartyWarehouseStorageSettlement
|
|
{
|
|
return ThirdPartyWarehouseStorageSettlement::query()
|
|
->where('company_id', (int) company_id())
|
|
->where('warehouse_id', self::WAREHOUSE_ID)
|
|
->where('period', $period)
|
|
->first();
|
|
}
|
|
|
|
private function showMissingSettlementDetails(string $period): void
|
|
{
|
|
$this->settlementPeriod = $period;
|
|
$this->settlementDetailPeriod = $period;
|
|
$this->settlementDetailDate = '';
|
|
$this->settlementDetailDates = [];
|
|
$this->settlementDetailMessage = $period . ' 的原存档没有每日商品明细,请重新核算该月份后查看和导出。';
|
|
$this->dispatch('warehouse-settlement-details-opened');
|
|
}
|
|
|
|
/** @return array<string,array<int,string>> */
|
|
protected function rules(): array
|
|
{
|
|
return [
|
|
'warehouseName' => ['required', 'string', 'max:191'],
|
|
'effectiveAt' => ['required', 'date_format:Y-m-d'],
|
|
'inboundRatePerTon' => ['required', 'numeric', 'min:0', 'max:999999.9999'],
|
|
'outboundRatePerOrder' => ['required', 'numeric', 'min:0', 'max:999999.9999'],
|
|
'piecesPerPallet' => ['required', 'numeric', 'gt:0', 'max:999999.9999'],
|
|
'palletAreaSqm' => ['required', 'numeric', 'gt:0', 'max:999999.9999'],
|
|
'storageRatePerSqmMonth' => ['required', 'numeric', 'min:0', 'max:999999.9999'],
|
|
'remark' => ['nullable', 'string', 'max:2000'],
|
|
];
|
|
}
|
|
|
|
/** @return array<string,string> */
|
|
protected function messages(): array
|
|
{
|
|
return [
|
|
'piecesPerPallet.gt' => '每托折算件数必须大于 0。',
|
|
'palletAreaSqm.gt' => '每托占地面积必须大于 0。',
|
|
];
|
|
}
|
|
|
|
private function repository(): ThirdPartyWarehouseQuoteRepository
|
|
{
|
|
return app(ThirdPartyWarehouseQuoteRepository::class);
|
|
}
|
|
|
|
private function ensureAuthenticated(): void
|
|
{
|
|
abort_unless(auth()->check(), 401);
|
|
}
|
|
}
|