CaiWuXiTong/app/Support/CourierQuoteChangeExport.php

177 lines
9.6 KiB
PHP

<?php
namespace App\Support;
use App\Models\Business\CourierQuoteChange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/** 单张变价单导出;以保存的明细为准,不重新读取可变的报价底稿。 */
final class CourierQuoteChangeExport
{
public function build(CourierQuoteChange $change): Spreadsheet
{
abort_unless((int) $change->company_id === (int) company_id(), 403);
abort_if($change->isStockQuote(), 422, '存量报价请在详情页查看原报价资料。');
$book = new Spreadsheet();
$book->removeSheetByIndex(0);
$cells = [];
if (! $change->hasQuotePayload()) {
foreach ($change->items()->reorder('source_row')->orderBy('source_column')->cursor() as $item) {
$cells[] = ['row' => $item->source_row, 'column' => $item->source_column, 'region' => $item->region,
'label' => $item->weight_band ?: $item->charge_item ?: '价格', 'old' => $item->original_amount,
'new' => $item->new_amount, 'cell' => $item->source_cell, 'unit' => $item->unit, 'note' => $item->note];
}
} else {
$requotes = app(CourierRequoteService::class);
$onboarding = $requotes->onboardingForCourier($change->carrier_name);
abort_if($onboarding === null, 422, '未找到原入驻报价,无法导出新旧对比。');
$old = $requotes->basePayload($onboarding, $change);
$diff = $requotes->diff($onboarding, $old, $change->quote_payload);
foreach ($diff['cells'] as $cell) {
$cells[] = ['row' => $cell['region'], 'column' => $cell['label'], 'region' => $cell['region'],
'label' => $cell['label'], 'old' => $cell['old'], 'new' => $cell['new'], 'cell' => '', 'unit' => '元', 'note' => ''];
}
}
$sheet = $this->sheet($book, $change, '横向变价对比');
$this->text($sheet, 'A5', '仅列本单已保存的变价项;—表示本单未变更或无对应价格,不代表零价。备注见逐项明细。');
if (isset($diff)) {
$this->text($sheet, 'A5', $diff['same_structure']
? '价格变动见下表;新增或移除地区、规则变更见结构与规则变更及原、新报价全表。'
: '计费结构已变化,请对照结构与规则变更、原报价全表与新报价全表,不计算不同区间之间的差额。');
}
$this->text($sheet, 'A7', '地区 / 规则');
$sheet->mergeCells('A7:A8');
$columns = collect($cells)->unique('column')->values();
foreach ($columns as $index => $column) {
$start = 2 + $index * 3;
$this->text($sheet, Coordinate::stringFromColumnIndex($start) . '7', $column['label']);
$sheet->mergeCells(Coordinate::stringFromColumnIndex($start) . '7:' . Coordinate::stringFromColumnIndex($start + 2) . '7');
foreach (['原价', '新价', '差额'] as $offset => $label) {
$this->text($sheet, Coordinate::stringFromColumnIndex($start + $offset) . '8', $label);
}
}
$row = 9;
foreach (collect($cells)->groupBy('row') as $items) {
$this->text($sheet, 'A' . $row, $items->first()['region']);
$byColumn = $items->keyBy('column');
foreach ($columns as $index => $column) {
$cell = $byColumn->get($column['column']);
foreach ([$cell['old'] ?? null, $cell['new'] ?? null, $cell === null ? null : $this->delta($cell)] as $offset => $value) {
$address = Coordinate::stringFromColumnIndex(2 + $index * 3 + $offset) . $row;
$this->amount($sheet, $address, $value);
}
}
$row++;
}
$this->style($sheet, max(1, 1 + $columns->count() * 3), max(8, $row - 1), 8);
$sheet->freezePane('B9');
$details = $this->sheet($book, $change, '逐项明细');
foreach (['地区 / 规则', '重量区间 / 计费项', '原价', '新价', '差额', '单位', '备注', '原单元格'] as $index => $label) {
$this->text($details, Coordinate::stringFromColumnIndex($index + 1) . '7', $label);
}
foreach ($cells as $index => $cell) {
$line = $index + 8;
foreach (['A' => $cell['region'], 'B' => $cell['label'], 'F' => $cell['unit'], 'G' => $cell['note'], 'H' => $cell['cell']] as $column => $value) {
$this->text($details, $column . $line, $value);
}
foreach (['C' => $cell['old'], 'D' => $cell['new'], 'E' => $this->delta($cell)] as $column => $value) {
$this->amount($details, $column . $line, $value);
}
}
$this->style($details, 8, max(7, count($cells) + 7), 7);
$details->getColumnDimension('B')->setWidth(30);
$details->getColumnDimension('G')->setWidth(48);
$details->getStyle('G8:G' . max(8, count($cells) + 7))->getAlignment()->setWrapText(true);
if (isset($diff)) {
$rules = $this->sheet($book, $change, '结构与规则变更');
$rows = [['原计费方式', $diff['old_mode']], ['新计费方式', $diff['new_mode']],
['新增地区', implode('、', $diff['added']) ?: '无'], ['移除地区', implode('、', $diff['removed']) ?: '无']];
foreach ($diff['rules'] as $rule) {
$rows[] = ['原规则', $rule['old']];
$rows[] = ['新规则', $rule['new']];
}
if (! $diff['same_structure']) {
$rows[] = ['核对说明', '计费结构已变化,请对照原报价全表与新报价全表,不按相同区间计算差额。'];
}
foreach ($rows as $index => $values) {
$this->text($rules, 'A' . ($index + 7), $values[0]);
$this->text($rules, 'B' . ($index + 7), $values[1]);
}
$this->style($rules, 2, count($rows) + 6, 7);
$rules->getColumnDimension('B')->setWidth(100);
foreach (['原报价全表' => $old, '新报价全表' => $change->quote_payload] as $title => $payload) {
$model = $requotes->draftModel($onboarding, $payload);
$full = $this->sheet($book, $change, $title);
$headers = ['地区 / 规则', ...array_column($model->quoteColumns(), 'label')];
foreach ($headers as $index => $label) {
$this->text($full, Coordinate::stringFromColumnIndex($index + 1) . '7', $label);
}
foreach ($model->quoteRowList() as $index => $item) {
$this->text($full, 'A' . ($index + 8), $item['region']);
foreach (CourierQuoteStructure::rowValues($item, $model->quoteColumns()) as $column => $value) {
$this->amount($full, Coordinate::stringFromColumnIndex($column + 2) . ($index + 8), $value);
}
}
$this->style($full, count($headers), max(7, count($model->quoteRowList()) + 7), 7);
}
}
$book->setActiveSheetIndex(0);
return $book;
}
private function sheet(Spreadsheet $book, CourierQuoteChange $change, string $title): Worksheet
{
$sheet = $book->createSheet()->setTitle($title);
foreach (['报价变价核对 · ' . $change->number, $change->carrier_name . ' · 原报价 ' . $change->source_quote_id,
'状态:' . $change->statusLabel() . ';执行期间:' . $change->periodLabel(), '变更说明:' . $change->summary] as $index => $value) {
$this->text($sheet, 'A' . ($index + 1), $value);
}
return $sheet;
}
private function delta(array $cell): ?float
{
return $cell['old'] === null || $cell['new'] === null ? null : round((float) $cell['new'] - (float) $cell['old'], 4);
}
private function text(Worksheet $sheet, string $address, mixed $value): void
{
$sheet->setCellValueExplicit($address, (string) ($value ?? ''), DataType::TYPE_STRING);
}
private function amount(Worksheet $sheet, string $address, mixed $value): void
{
if ($value === null) {
$this->text($sheet, $address, '—');
return;
}
$sheet->setCellValueExplicit($address, round((float) $value, 4), DataType::TYPE_NUMERIC);
$sheet->getStyle($address)->getNumberFormat()->setFormatCode('0.0000');
}
private function style(Worksheet $sheet, int $columns, int $lastRow, int $headerRow): void
{
$last = Coordinate::stringFromColumnIndex($columns);
$sheet->getStyle('A1:' . $last . $lastRow)->getFont()->setName('Microsoft YaHei')->setSize(10);
$sheet->getStyle('A7:' . $last . $headerRow)->applyFromArray([
'font' => ['bold' => true, 'color' => ['rgb' => '513E21']],
'fill' => ['fillType' => 'solid', 'startColor' => ['rgb' => 'F7F0DE']],
'alignment' => ['wrapText' => true, 'vertical' => 'center'],
]);
foreach (range(1, $columns) as $column) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($column))->setWidth($column === 1 ? 22 : 16);
}
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
$sheet->freezePane('B' . ($headerRow + 1));
$sheet->setAutoFilter('A' . $headerRow . ':' . $last . $lastRow);
$sheet->getPageSetup()->setOrientation('landscape')->setFitToWidth(1)->setFitToHeight(0);
}
}