CaiWuXiTong/modules/EcommerceFinance/Services/SupplierBookkeepingConfigImporter.php
limeng 09b4ffcb8a 供应商应付-厂家记账:新增厂家记账配置并按配置判定厂家扣款
- 新增配置表 business_supplier_bookkeeping_configs(+ 操作日志表),迁移时导入财务提供的 209 家存量配置
- 记账方式:正常记账 / 仅代发仓发错货记账 / 不记账,支持生效期;配置页支持新增、编辑、删除恢复、Excel 导入导出
- 扣款判定:厂家记账按供应商配置,仅正常记账扣款;仓库记账仅订单发货仓库为代发仓且二级原因含“仓库发错货”时作为厂家扣款项,其余不扣
- 未配置/生效期外显示待配置,多供应商判定不一致显示待确认,均不计入扣款金额
- 明细页新增厂家扣款判定筛选、扣款工单数与扣款金额指标、导出列

涉及:数据库迁移(新建配置表并写入存量配置)、金额汇总展示;不涉及上游写入和权限模型。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JouFR6TJ7vGscRJanYScEo
2026-10-19 10:38:36 +08:00

122 lines
6.0 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Modules\EcommerceFinance\Services;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Modules\EcommerceFinance\Services\SupplierBookkeepingDeductionRule as Rule;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
/**
* 读取厂家记账配置 Excel:
* - 财务原表:供应商、厂家是否记账(原文如“代发仓发货错需记账,其他原因均不记账”“不记账(2025.3月-2027.12.30)”);
* - 本系统导出表:供应商、记账方式、生效开始日期、生效结束日期、原始说明。
*/
class SupplierBookkeepingConfigImporter
{
/** @return list<array{supplier_name:string,mode:string,effective_from:?string,effective_to:?string,source_text:string}> */
public function read(string $path): array
{
$reader = IOFactory::createReader('Xlsx');
$info = $reader->listWorksheetInfo($path);
if ($info === [] || $info[0]['totalRows'] > 5001 || $info[0]['totalColumns'] > 20) {
throw ValidationException::withMessages(['file' => '请使用第一个工作表存放配置,最多5000条。']);
}
$reader->setLoadSheetsOnly($info[0]['worksheetName']);
$book = $reader->load($path);
try {
$sheet = $book->getSheet(0);
$headers = [];
for ($column = 1; $column <= $info[0]['totalColumns']; $column++) {
$headers[$this->label($sheet->getCell([$column, 1])->getValue())] = $column;
}
$supplierColumn = $headers['供应商'] ?? $headers['供应商名称'] ?? null;
$ruleColumn = $headers['厂家是否记账'] ?? null;
$modeColumn = $headers['记账方式'] ?? null;
if ($supplierColumn === null || ($ruleColumn === null && $modeColumn === null)) {
throw ValidationException::withMessages(['file' => '缺少列:需要“供应商”和“厂家是否记账”(或“记账方式”)。']);
}
$rows = [];
$seen = [];
for ($number = 2; $number <= $sheet->getHighestDataRow(); $number++) {
$supplier = $this->text($sheet, $supplierColumn, $number);
$ruleText = $ruleColumn !== null ? $this->text($sheet, $ruleColumn, $number) : '';
$modeText = $modeColumn !== null ? $this->text($sheet, $modeColumn, $number) : '';
// 原表中间重复出现的表头行、空行跳过。
if (($supplier === '' && $ruleText === '' && $modeText === '') || $this->label($supplier) === '供应商') {
continue;
}
if ($supplier === '' || mb_strlen($supplier) > 191) {
throw ValidationException::withMessages(['file' => "第{$number}行供应商为空或超长。"]);
}
$key = mb_strtolower($supplier);
if (isset($seen[$key])) {
throw ValidationException::withMessages(['file' => "第{$number}行供应商“{$supplier}”与第{$seen[$key]}行重复。"]);
}
$seen[$key] = $number;
try {
$parsed = Rule::parseRuleText($ruleText !== '' ? $ruleText : $modeText);
} catch (InvalidArgumentException $exception) {
throw ValidationException::withMessages(['file' => "第{$number}行:" . $exception->getMessage()]);
}
foreach (['effective_from' => '生效开始日期', 'effective_to' => '生效结束日期'] as $field => $label) {
if (isset($headers[$label])) {
$date = $this->date($sheet, $headers[$label], $number);
if ($date !== null) {
$parsed[$field] = $date;
}
}
}
if ($parsed['effective_from'] !== null && $parsed['effective_to'] !== null && $parsed['effective_from'] > $parsed['effective_to']) {
throw ValidationException::withMessages(['file' => "第{$number}行生效结束日期早于开始日期。"]);
}
$source = isset($headers['原始说明']) ? $this->text($sheet, $headers['原始说明'], $number) : '';
$rows[] = ['supplier_name' => $supplier] + $parsed + ['source_text' => $ruleText !== '' ? $ruleText : ($source !== '' ? $source : $modeText)];
}
if ($rows === []) {
throw ValidationException::withMessages(['file' => '配置表没有有效记录。']);
}
return $rows;
} finally {
$book->disconnectWorksheets();
}
}
private function label(mixed $value): string
{
return str_replace([' ', ' ', "\n"], '', trim((string) $value));
}
private function text($sheet, int $column, int $row): string
{
$cell = $sheet->getCell([$column, $row]);
if ($cell->getDataType() === 'f') {
throw ValidationException::withMessages(['file' => "第{$row}行含公式,请使用实际配置值。"]);
}
return trim((string) $cell->getFormattedValue());
}
private function date($sheet, int $column, int $row): ?string
{
$cell = $sheet->getCell([$column, $row]);
$value = $cell->getValue();
if ($value === null || trim((string) $value) === '') {
return null;
}
if (is_numeric($value) && ExcelDate::isDateTime($cell)) {
return ExcelDate::excelToDateTimeObject($value)->format('Y-m-d');
}
$text = str_replace(['/', '.'], '-', trim((string) $cell->getFormattedValue()));
if (! preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $text, $m) || ! checkdate((int) $m[2], (int) $m[3], (int) $m[1])) {
throw ValidationException::withMessages(['file' => "第{$row}行日期无效,请使用 2026-01-31 格式。"]);
}
return sprintf('%04d-%02d-%02d', $m[1], $m[2], $m[3]);
}
}