feat: 供应商管理新增是否包邮厂家 #415
@ -2,26 +2,41 @@
|
||||
|
||||
namespace App\Http\Controllers\Business;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository;
|
||||
use Modules\EcommerceFinance\Services\SupplierFreeShippingStore;
|
||||
use Modules\EcommerceFinance\Services\SupplierFreeShippingWorkbook;
|
||||
use Modules\EcommerceFinance\View\SupplierManagementPageData;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class SupplierManagement extends Controller
|
||||
{
|
||||
public function export(Request $request, SupplierManagementMirrorRepository $repository, SupplierManagementPageData $page)
|
||||
{
|
||||
public function export(
|
||||
Request $request,
|
||||
SupplierManagementMirrorRepository $repository,
|
||||
SupplierManagementPageData $page,
|
||||
SupplierFreeShippingStore $freeShipping,
|
||||
): StreamedResponse {
|
||||
abort_unless($repository->ready(), 503, '供应商资料尚未完成首次同步,暂时不能导出。');
|
||||
$filters = $page->filters($request->query());
|
||||
$columns = SupplierManagementPageData::columns();
|
||||
$companyId = (int) (company_id() ?: 1);
|
||||
|
||||
return response()->streamDownload(static function () use ($repository, $filters): void {
|
||||
return response()->streamDownload(static function () use ($repository, $filters, $columns, $freeShipping, $companyId): void {
|
||||
$csv = fopen('php://output', 'w');
|
||||
fputs($csv, "\xEF\xBB\xBF");
|
||||
fputcsv($csv, array_values(SupplierManagementMirrorRepository::COLUMNS));
|
||||
fputcsv($csv, array_values($columns));
|
||||
foreach ($repository->exportRows($filters) as $row) {
|
||||
$row = $freeShipping->decorateRow($row, $companyId);
|
||||
$values = [];
|
||||
foreach (array_keys(SupplierManagementMirrorRepository::COLUMNS) as $column) {
|
||||
$value = (string) ($row->{$column} ?? '');
|
||||
foreach (array_keys($columns) as $column) {
|
||||
$value = $column === SupplierManagementPageData::FREE_SHIPPING_COLUMN
|
||||
? ((bool) ($row->{$column} ?? false) ? '包邮' : '')
|
||||
: (string) ($row->{$column} ?? '');
|
||||
if ($column === 'coopersgn') $value = ['1' => '合作', '2' => '不合作'][$value] ?? '未维护';
|
||||
// Excel 不能执行供应商提供的公式文本,且长编码保留为文本。
|
||||
if (preg_match('/^[=+\-@\t\r\n]/', ltrim($value)) || preg_match('/^\d{12,}$/', $value)) $value = "'" . $value;
|
||||
@ -33,4 +48,114 @@ class SupplierManagement extends Controller
|
||||
}, 'supplier_management_' . now()->format('Ymd_His') . '.csv',
|
||||
['Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'private, no-store']);
|
||||
}
|
||||
|
||||
public function template(SupplierFreeShippingWorkbook $workbook): StreamedResponse
|
||||
{
|
||||
return $workbook->template();
|
||||
}
|
||||
|
||||
public function import(
|
||||
Request $request,
|
||||
SupplierManagementMirrorRepository $repository,
|
||||
SupplierFreeShippingStore $freeShipping,
|
||||
SupplierFreeShippingWorkbook $workbook,
|
||||
): RedirectResponse {
|
||||
$request->validate(['import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:10240']]);
|
||||
abort_unless($repository->ready(), 503, '供应商资料尚未完成首次同步,暂时不能导入。');
|
||||
|
||||
$valuesByCode = $workbook->read($request->file('import'));
|
||||
$suppliers = $repository->suppliersByCodes(array_keys($valuesByCode));
|
||||
$matched = [];
|
||||
foreach ($suppliers as $supplier) {
|
||||
$code = mb_strtolower(trim((string) ($supplier->scmcode ?? '')));
|
||||
if ($code !== '') {
|
||||
$matched[$code][] = $supplier;
|
||||
}
|
||||
}
|
||||
|
||||
$missing = [];
|
||||
$ambiguous = [];
|
||||
$valuesBySupplierId = [];
|
||||
foreach ($valuesByCode as $code => $isFreeShipping) {
|
||||
$key = mb_strtolower(trim((string) $code));
|
||||
if (! isset($matched[$key])) {
|
||||
$missing[] = (string) $code;
|
||||
continue;
|
||||
}
|
||||
if (count($matched[$key]) !== 1) {
|
||||
$ambiguous[] = (string) $code;
|
||||
continue;
|
||||
}
|
||||
$valuesBySupplierId[(int) $matched[$key][0]->Id] = $isFreeShipping;
|
||||
}
|
||||
if ($ambiguous !== []) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => '以下供应商代码在供应商主档中存在多条记录,未导入:' . implode('、', array_slice($ambiguous, 0, 8)),
|
||||
]);
|
||||
}
|
||||
if ($missing !== []) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => '以下供应商代码未找到,未导入:' . implode('、', array_slice($missing, 0, 8))
|
||||
. (count($missing) > 8 ? ' 等 ' . count($missing) . ' 条' : ''),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $freeShipping->saveMany(
|
||||
$valuesBySupplierId,
|
||||
(int) (company_id() ?: 1),
|
||||
user_id() ? (int) user_id() : null,
|
||||
'import',
|
||||
);
|
||||
|
||||
return $this->redirectToPage($request)->with(
|
||||
'supplier_management_message',
|
||||
"导入完成:新增 {$result['created']} 家供应商维护记录,更新 {$result['updated']} 家供应商维护记录。",
|
||||
);
|
||||
}
|
||||
|
||||
public function updateFreeShipping(
|
||||
Request $request,
|
||||
int $supplier,
|
||||
SupplierManagementMirrorRepository $repository,
|
||||
SupplierFreeShippingStore $freeShipping,
|
||||
): JsonResponse|RedirectResponse {
|
||||
$request->validate(['value' => ['nullable', 'string', 'in:,包邮']]);
|
||||
abort_unless($repository->ready(), 503, '供应商资料尚未完成首次同步,暂时不能编辑。');
|
||||
abort_if($repository->supplierById($supplier) === null, 404, '供应商不存在。');
|
||||
|
||||
$isFreeShipping = $request->input('value') === '包邮';
|
||||
$freeShipping->save(
|
||||
$supplier,
|
||||
$isFreeShipping,
|
||||
(int) (company_id() ?: 1),
|
||||
user_id() ? (int) user_id() : null,
|
||||
'manual',
|
||||
);
|
||||
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'value' => $isFreeShipping ? '包邮' : '',
|
||||
'display' => $isFreeShipping ? '包邮' : '-',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->redirectToPage($request)->with('supplier_management_message', '是否包邮厂家已保存。');
|
||||
}
|
||||
|
||||
private function redirectToPage(Request $request): RedirectResponse
|
||||
{
|
||||
$query = array_filter([
|
||||
...app(SupplierManagementPageData::class)->filters($request->query()),
|
||||
'page' => max((int) $request->query('page', 1), 1),
|
||||
], static fn (mixed $value): bool => $value !== '');
|
||||
|
||||
$url = route('business.pages.show', [
|
||||
'company_id' => company_id() ?: 1,
|
||||
'section' => 'supplier-payables',
|
||||
'page' => 'supplier-management',
|
||||
]);
|
||||
|
||||
return redirect()->to($url . ($query === [] ? '' : '?' . http_build_query($query)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,6 +88,40 @@ class SupplierManagementMirrorRepository
|
||||
];
|
||||
}
|
||||
|
||||
public function supplierById(int $id): ?object
|
||||
{
|
||||
if (! $this->ready() || $id <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->mirror->connection()->table($this->datasets->get(self::DATASET)->targetTable())
|
||||
->where('Id', $id)
|
||||
->first(['Id', 'scmcode', 'scmname']);
|
||||
}
|
||||
|
||||
/** @param list<string> $codes
|
||||
* @return list<object>
|
||||
*/
|
||||
public function suppliersByCodes(array $codes): array
|
||||
{
|
||||
$codes = array_values(array_unique(array_filter(
|
||||
array_map('trim', $codes),
|
||||
static fn (string $code): bool => $code !== '',
|
||||
)));
|
||||
if ($codes === []) {
|
||||
return [];
|
||||
}
|
||||
if (! $this->ready()) {
|
||||
throw new RuntimeException('supplier_management_mirror_not_ready');
|
||||
}
|
||||
|
||||
return $this->mirror->connection()->table($this->datasets->get(self::DATASET)->targetTable())
|
||||
->whereIn('scmcode', $codes)
|
||||
->orderBy('Id')
|
||||
->get(['Id', 'scmcode', 'scmname'])
|
||||
->all();
|
||||
}
|
||||
|
||||
/** 全部筛选结果,按上游稳定键小批读取,保留编码/账号文本。 */
|
||||
public function exportRows(array $filters): \Generator
|
||||
{
|
||||
|
||||
101
modules/EcommerceFinance/Services/SupplierFreeShippingStore.php
Normal file
101
modules/EcommerceFinance/Services/SupplierFreeShippingStore.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Services;
|
||||
|
||||
/**
|
||||
* 供应商是否包邮厂家的本地补充资料。
|
||||
*
|
||||
* 供应商主档来自只读共享镜像,包邮标记保存在现有供应商维护表中,
|
||||
* 以供应商镜像的稳定 ID 关联,不写回上游。
|
||||
*/
|
||||
class SupplierFreeShippingStore
|
||||
{
|
||||
public const DATASET = 'supplier-management.free-shipping-manufacturer';
|
||||
|
||||
public const FIELD = 'free_shipping_manufacturer';
|
||||
|
||||
/** @var array<int,array<string,array<string,mixed>>> */
|
||||
private array $cache = [];
|
||||
|
||||
public function __construct(private readonly SupplierPayableOverrideStore $overrides)
|
||||
{
|
||||
}
|
||||
|
||||
/** @param list<object> $rows
|
||||
* @return list<object>
|
||||
*/
|
||||
public function decorateRows(array $rows, int $companyId): array
|
||||
{
|
||||
foreach ($rows as $row) {
|
||||
$this->decorateRow($row, $companyId);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function decorateRow(object $row, int $companyId): object
|
||||
{
|
||||
$id = (int) ($row->Id ?? 0);
|
||||
$values = $this->values($companyId);
|
||||
$row->{self::FIELD} = $id > 0 && (bool) ($values[$this->key($id)][self::FIELD] ?? false);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/** @return array{created:int,updated:int} */
|
||||
public function save(int $supplierId, bool $isFreeShipping, int $companyId, ?int $userId, string $source = 'manual'): array
|
||||
{
|
||||
$result = $this->overrides->save(
|
||||
self::DATASET,
|
||||
$this->key($supplierId),
|
||||
[self::FIELD => $isFreeShipping],
|
||||
$companyId,
|
||||
$userId,
|
||||
$source,
|
||||
);
|
||||
unset($this->cache[$companyId]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,bool> $values 供应商 ID => 是否包邮厂家
|
||||
* @return array{created:int,updated:int}
|
||||
*/
|
||||
public function saveMany(array $values, int $companyId, ?int $userId, string $source = 'import'): array
|
||||
{
|
||||
$records = [];
|
||||
foreach ($values as $supplierId => $isFreeShipping) {
|
||||
if ((int) $supplierId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$records[$this->key((int) $supplierId)] = [self::FIELD => (bool) $isFreeShipping];
|
||||
}
|
||||
|
||||
$result = $this->overrides->saveBatch(self::DATASET, $records, $companyId, $userId, $source);
|
||||
unset($this->cache[$companyId]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @return array<string,array<string,mixed>> */
|
||||
private function values(int $companyId): array
|
||||
{
|
||||
if (! isset($this->cache[$companyId])) {
|
||||
$this->cache[$companyId] = [];
|
||||
foreach ($this->overrides->listRows(self::DATASET, $companyId) as $row) {
|
||||
$key = (string) ($row['_action_key'] ?? '');
|
||||
if ($key !== '') {
|
||||
$this->cache[$companyId][$key] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cache[$companyId];
|
||||
}
|
||||
|
||||
private function key(int $supplierId): string
|
||||
{
|
||||
return 'supplier:' . $supplierId;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Services;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Throwable;
|
||||
|
||||
/** 读取和生成「是否包邮厂家」导入文件。 */
|
||||
class SupplierFreeShippingWorkbook
|
||||
{
|
||||
public const SUPPLIER_CODE_HEADER = '供应商代码';
|
||||
|
||||
public const FIELD_HEADER = '是否包邮厂家';
|
||||
|
||||
/**
|
||||
* 返回供应商代码 => 是否包邮厂家。空白或“-”表示否。
|
||||
*
|
||||
* @return array<string,bool>
|
||||
*/
|
||||
public function read(UploadedFile $file): array
|
||||
{
|
||||
try {
|
||||
$reader = IOFactory::createReader(match (mb_strtolower($file->getClientOriginalExtension())) {
|
||||
'xlsx' => 'Xlsx',
|
||||
'xls' => 'Xls',
|
||||
'csv' => 'Csv',
|
||||
default => throw new \RuntimeException('Unsupported workbook format.'),
|
||||
});
|
||||
$book = $reader->load($file->getRealPath());
|
||||
} catch (Throwable) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => '无法读取导入文件,请确认文件未损坏且格式为 xlsx、xls 或 csv。',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
[$sheet, $headerRow, $codeColumn, $fieldColumn] = $this->locateColumns($book);
|
||||
$highestRow = $sheet->getHighestDataRow();
|
||||
if ($highestRow > 5001) {
|
||||
throw ValidationException::withMessages(['import' => '单次最多导入 5,000 条供应商记录,请拆分文件后重试。']);
|
||||
}
|
||||
|
||||
$records = [];
|
||||
$lineByCode = [];
|
||||
for ($row = $headerRow + 1; $row <= $highestRow; $row++) {
|
||||
$code = $this->text($sheet, $codeColumn, $row);
|
||||
if (str_starts_with($code, "'")) {
|
||||
$code = substr($code, 1);
|
||||
}
|
||||
$field = $this->text($sheet, $fieldColumn, $row);
|
||||
if ($code === '' && $field === '') {
|
||||
continue;
|
||||
}
|
||||
if ($code === '') {
|
||||
throw ValidationException::withMessages(['import' => "第 {$row} 行供应商代码为空。"]);
|
||||
}
|
||||
if (mb_strlen($code) > 191) {
|
||||
throw ValidationException::withMessages(['import' => "第 {$row} 行供应商代码超过 191 个字符。"]);
|
||||
}
|
||||
$codeKey = mb_strtolower($code);
|
||||
if (isset($lineByCode[$codeKey])) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => "第 {$row} 行供应商代码“{$code}”与第 {$lineByCode[$codeKey]} 行重复。",
|
||||
]);
|
||||
}
|
||||
|
||||
$records[$code] = $this->parseFlag($field, $row);
|
||||
$lineByCode[$codeKey] = $row;
|
||||
}
|
||||
|
||||
if ($records === []) {
|
||||
throw ValidationException::withMessages(['import' => '表格中没有可导入的供应商记录。']);
|
||||
}
|
||||
|
||||
return $records;
|
||||
} finally {
|
||||
$book->disconnectWorksheets();
|
||||
}
|
||||
}
|
||||
|
||||
public function template(): StreamedResponse
|
||||
{
|
||||
return response()->streamDownload(function (): void {
|
||||
$book = new Spreadsheet();
|
||||
$sheet = $book->getActiveSheet();
|
||||
$sheet->setTitle('是否包邮厂家');
|
||||
$sheet->fromArray([self::SUPPLIER_CODE_HEADER, '供应商名称', self::FIELD_HEADER], null, 'A1');
|
||||
$sheet->getStyle('A1:C1')->applyFromArray([
|
||||
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '856632']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'D8D2C7']]],
|
||||
]);
|
||||
$sheet->getRowDimension(1)->setRowHeight(24);
|
||||
$sheet->freezePane('A2');
|
||||
$sheet->setAutoFilter('A1:C1');
|
||||
foreach (['A' => 22, 'B' => 34, 'C' => 20] as $column => $width) {
|
||||
$sheet->getColumnDimension($column)->setWidth($width);
|
||||
}
|
||||
$sheet->getStyle('A2:A5001')->getNumberFormat()->setFormatCode('@');
|
||||
|
||||
$instructions = $book->createSheet();
|
||||
$instructions->setTitle('填写说明');
|
||||
$instructions->fromArray([
|
||||
['项目', '说明'],
|
||||
[self::SUPPLIER_CODE_HEADER, '必填。请填写供应商管理页面中的供应商代码,按代码精确关联。'],
|
||||
[self::FIELD_HEADER, '填写“包邮”表示是包邮厂家;空白或“-”表示不是包邮厂家。'],
|
||||
['兼容导出表', '也可以使用本页面导出的文件,在此列补充后直接导入。'],
|
||||
['覆盖规则', '同一供应商再次导入会更新该字段;其他列不会修改。'],
|
||||
], null, 'A1');
|
||||
$instructions->getStyle('A1:B1')->applyFromArray([
|
||||
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '856632']],
|
||||
]);
|
||||
$instructions->getColumnDimension('A')->setWidth(22);
|
||||
$instructions->getColumnDimension('B')->setWidth(76);
|
||||
$instructions->getStyle('A1:B5')->getAlignment()->setVertical(Alignment::VERTICAL_CENTER)->setWrapText(true);
|
||||
foreach (range(1, 5) as $row) {
|
||||
$instructions->getRowDimension($row)->setRowHeight($row === 1 ? 24 : 34);
|
||||
}
|
||||
|
||||
$book->setActiveSheetIndex(0);
|
||||
(new Xlsx($book))->save('php://output');
|
||||
$book->disconnectWorksheets();
|
||||
}, '供应商是否包邮厂家_导入模板_' . now()->format('Ymd') . '.xlsx', [
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'Cache-Control' => 'private, no-store',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array{0:Worksheet,1:int,2:int,3:int} */
|
||||
private function locateColumns(Spreadsheet $book): array
|
||||
{
|
||||
$best = null;
|
||||
foreach ($book->getWorksheetIterator() as $sheet) {
|
||||
$rows = $sheet->toArray(null, false, false, false);
|
||||
foreach (array_slice($rows, 0, 20, true) as $rowIndex => $row) {
|
||||
$headers = [];
|
||||
foreach ($row as $columnIndex => $value) {
|
||||
$header = $this->header($value);
|
||||
if ($header !== '') {
|
||||
$headers[$header] = $columnIndex + 1;
|
||||
}
|
||||
}
|
||||
if (isset($headers[self::SUPPLIER_CODE_HEADER], $headers[self::FIELD_HEADER])) {
|
||||
$candidate = [$sheet, $rowIndex + 1, $headers[self::SUPPLIER_CODE_HEADER], $headers[self::FIELD_HEADER]];
|
||||
$best = $candidate;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($best === null) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => '缺少表头:需要“供应商代码”和“是否包邮厂家”两列。',
|
||||
]);
|
||||
}
|
||||
|
||||
return $best;
|
||||
}
|
||||
|
||||
private function parseFlag(string $value, int $row): bool
|
||||
{
|
||||
$normalized = mb_strtolower(preg_replace('/[\s\x{FEFF}]+/u', '', $value));
|
||||
if (in_array($normalized, ['', '-', '否', '不是', '0', 'false', 'no'], true)) {
|
||||
return false;
|
||||
}
|
||||
if (in_array($normalized, ['包邮', '是', '是包邮厂家', '1', 'true', 'yes'], true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'import' => "第 {$row} 行“是否包邮厂家”只能填写“包邮”,或留空表示否。",
|
||||
]);
|
||||
}
|
||||
|
||||
private function text(Worksheet $sheet, int $column, int $row): string
|
||||
{
|
||||
$cell = $sheet->getCell([$column, $row]);
|
||||
if ($cell->getDataType() === 'f') {
|
||||
throw ValidationException::withMessages(['import' => "第 {$row} 行含公式,请填写实际值。"]);
|
||||
}
|
||||
|
||||
return trim((string) $cell->getFormattedValue());
|
||||
}
|
||||
|
||||
private function header(mixed $value): string
|
||||
{
|
||||
return str_replace([' ', ' ', "\n", "\r", "\t", "\x{FEFF}"], '', trim((string) $value));
|
||||
}
|
||||
}
|
||||
@ -4,14 +4,35 @@ namespace Modules\EcommerceFinance\View;
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository;
|
||||
use Modules\EcommerceFinance\Services\SupplierFreeShippingStore;
|
||||
use Throwable;
|
||||
|
||||
class SupplierManagementPageData
|
||||
{
|
||||
public const TABS = ['contracts' => '合同信息', 'contacts' => '联系信息', 'payments' => '打款信息', 'logs' => '操作日志'];
|
||||
|
||||
public function __construct(private readonly SupplierManagementMirrorRepository $repository)
|
||||
public const FREE_SHIPPING_COLUMN = SupplierFreeShippingStore::FIELD;
|
||||
|
||||
public const FREE_SHIPPING_LABEL = '是否包邮厂家';
|
||||
|
||||
public function __construct(
|
||||
private readonly SupplierManagementMirrorRepository $repository,
|
||||
private readonly SupplierFreeShippingStore $freeShipping,
|
||||
) {
|
||||
}
|
||||
|
||||
/** 是否包邮厂家紧跟在供应商名称后展示。 */
|
||||
public static function columns(): array
|
||||
{
|
||||
$columns = [];
|
||||
foreach (SupplierManagementMirrorRepository::COLUMNS as $key => $label) {
|
||||
$columns[$key] = $label;
|
||||
if ($key === 'scmname') {
|
||||
$columns[self::FREE_SHIPPING_COLUMN] = self::FREE_SHIPPING_LABEL;
|
||||
}
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function filters(array $input): array
|
||||
@ -39,6 +60,10 @@ class SupplierManagementPageData
|
||||
$ready = $this->repository->ready();
|
||||
if ($ready) {
|
||||
$result = $this->repository->paginate($filters, $page, $limit);
|
||||
$result['rows'] = $this->freeShipping->decorateRows(
|
||||
$result['rows'],
|
||||
(int) (company_id() ?: 1),
|
||||
);
|
||||
if ((int) request('supplier_id', 0) > 0) {
|
||||
// 选择必须仍在当前列表中;筛选和翻页后清除旧选择。
|
||||
$selected = collect($result['rows'])->firstWhere('Id', (int) request('supplier_id'));
|
||||
@ -61,7 +86,7 @@ class SupplierManagementPageData
|
||||
return [
|
||||
'view' => 'business.pages.supplier-payables.supplier-management',
|
||||
'supplierFilters' => $filters, 'supplierState' => $state, 'supplierReady' => $ready,
|
||||
'supplierError' => $error, 'supplierColumns' => SupplierManagementMirrorRepository::COLUMNS,
|
||||
'supplierError' => $error, 'supplierColumns' => self::columns(),
|
||||
'suppliers' => new LengthAwarePaginator($result['rows'], $result['total'], $limit, $page,
|
||||
['path' => request()->url(), 'query' => request()->except('page', 'supplier_id', 'detail_page')]),
|
||||
'selectedSupplier' => $selected, 'supplierDetailTab' => $tab,
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
$listUrl = route('business.pages.show', ['section' => 'supplier-payables', 'page' => 'supplier-management']);
|
||||
$localUrl = static fn (array $changes): string => $listUrl . '?' . http_build_query(array_merge(request()->except('detail_page'), $changes));
|
||||
$cell = static fn ($value): string => $value === null || $value === '' ? '—' : (string) $value;
|
||||
$freeShippingColumn = \Modules\EcommerceFinance\View\SupplierManagementPageData::FREE_SHIPPING_COLUMN;
|
||||
$detailColumns = match ($supplierDetailTab) {
|
||||
'contacts' => ['sourcefrom' => '数据来源', 'linker' => '联系人', 'linkjob' => '职务', 'linkmobile' => '联系电话', 'linkweixin' => '微信', 'memo' => '备注'],
|
||||
'payments' => ['enable' => '是否启用', 'sourcefrom' => '数据来源', 'accounttype' => '账户类型', 'account' => '打款账号', 'accountname' => '账户名称', 'accountbank' => '开户银行', 'accountnature' => '账户性质', 'invoice' => '开票信息', 'memo' => '备注'],
|
||||
@ -9,7 +10,7 @@
|
||||
default => ['ordernum' => '合同编号', 'originname' => '数据来源', 'title' => '合同名称', 'tradebody' => '合同主体', 'company' => '签订公司', 'eSignDocTemplateId' => '合同类型', 'type' => '合同类型名称', 'tradebtime' => '合同开始时间', 'tradeetime' => '合同截止时间', 'sendTime' => '合同发送时间', 'completeTime' => '完成时间', 'state' => '签署状态'],
|
||||
};
|
||||
@endphp
|
||||
<section class="supplier-management" data-supplier-management aria-label="供应商管理">
|
||||
<section class="supplier-management" data-supplier-management data-csrf="{{ csrf_token() }}" aria-label="供应商管理">
|
||||
<style>
|
||||
.supplier-management { color:var(--vip-content-ink,#171717); font-size:.8125rem; }
|
||||
.supplier-management [hidden] { display:none !important; }
|
||||
@ -35,6 +36,10 @@
|
||||
.supplier-management-empty { height:120px; text-align:center !important; color:var(--vip-content-muted,#595959); white-space:normal !important; }
|
||||
.supplier-management-badge { padding:3px 8px; border-radius:4px; background:var(--vip-content-bg,#F8F6F1); color:var(--vip-content-muted,#595959); }
|
||||
.supplier-management-badge.is-cooperating { color:var(--vip-success,#53793E); }
|
||||
.supplier-management-free-shipping { width:76px; height:28px; padding:0 6px; border:1px solid transparent; border-radius:6px; background:transparent; color:var(--vip-content-ink,#171717); font:inherit; cursor:pointer; }
|
||||
.supplier-management-free-shipping:hover,.supplier-management-free-shipping:focus { border-color:var(--vip-border,#D8D2C7); background:#fff; }
|
||||
.supplier-management-free-shipping.is-saving { cursor:wait; opacity:.55; }
|
||||
.supplier-management-free-shipping[data-value="包邮"] { color:var(--vip-content-accent-dark,#856632); font-weight:600; }
|
||||
.supplier-management-pagination { padding:12px; display:flex; align-items:center; flex-wrap:wrap; justify-content:space-between; gap:12px; }
|
||||
.supplier-management-pagination nav { display:flex; align-items:center; gap:12px; }
|
||||
.supplier-management-pagination a { color:var(--vip-content-accent-dark,#856632); }
|
||||
@ -45,6 +50,17 @@
|
||||
.supplier-management-detail-tabs a[aria-selected="true"] { color:var(--vip-content-accent-dark,#856632); border-color:var(--vip-content-accent,#B9954F); }
|
||||
.supplier-management-alert { margin-bottom:12px; padding:12px; border:1px solid var(--vip-border,#D8D2C7); border-radius:8px; background:var(--vip-content-accent-soft,#F7F0DE); }
|
||||
.supplier-management-alert.is-error { color:var(--vip-danger,#B80000); border-color:var(--vip-danger,#B80000); }
|
||||
.supplier-management-import { position:fixed; inset:0; z-index:1060; display:grid; place-items:center; padding:16px; }
|
||||
.supplier-management-import[hidden] { display:none; }
|
||||
.supplier-management-import__backdrop { position:absolute; inset:0; background:rgba(5,5,6,.58); }
|
||||
.supplier-management-import__dialog { position:relative; width:min(520px,94vw); overflow:hidden; border:1px solid var(--vip-border,#D8D2C7); border-radius:8px; background:var(--vip-surface,#fff); box-shadow:0 4px 8px rgba(23,23,23,.10); }
|
||||
.supplier-management-import__dialog header { display:flex; align-items:center; justify-content:space-between; padding:16px; border-bottom:1px solid var(--vip-border,#D8D2C7); }
|
||||
.supplier-management-import__dialog h2 { margin:0; font-size:.875rem; }
|
||||
.supplier-management-import__dialog header button { border:0; background:transparent; color:var(--vip-content-muted,#595959); cursor:pointer; }
|
||||
.supplier-management-import__body { display:grid; gap:12px; padding:16px; }
|
||||
.supplier-management-import__body p { margin:0; color:var(--vip-content-muted,#595959); font-size:.75rem; }
|
||||
.supplier-management-import__body input[type="file"] { width:100%; padding:12px; border:1px dashed var(--vip-border,#D8D2C7); border-radius:8px; background:var(--vip-content-bg,#F8F6F1); }
|
||||
.supplier-management-import__dialog footer { display:flex; justify-content:flex-end; gap:8px; padding:16px; border-top:1px solid var(--vip-border,#D8D2C7); }
|
||||
@media (max-width:640px) { .supplier-management-field { width:100%; } .supplier-management-detail-tabs { gap:12px; overflow-x:auto; white-space:nowrap; } }
|
||||
</style>
|
||||
<form method="GET" action="{{ $listUrl }}" class="supplier-management-toolbar" data-supplier-query aria-label="供应商管理筛选">
|
||||
@ -68,11 +84,19 @@
|
||||
<button type="submit" class="supplier-management-button is-primary">查询</button>
|
||||
<a href="{{ $listUrl }}" class="supplier-management-button" data-supplier-local>重置</a>
|
||||
@if ($supplierReady && !$supplierError)
|
||||
<a class="supplier-management-button" href="{{ route('business.supplier-management.free-shipping.template') }}">导入模板</a>
|
||||
<button type="button" class="supplier-management-button" data-supplier-free-shipping-import-open>导入</button>
|
||||
<a class="supplier-management-button" href="{{ route('business.supplier-management.export', $supplierFilters) }}">导出</a>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
<p role="status" aria-live="polite" data-supplier-load-status></p>
|
||||
@if (session('supplier_management_message'))
|
||||
<div class="supplier-management-alert" role="status">{{ session('supplier_management_message') }}</div>
|
||||
@endif
|
||||
@if ($errors->has('import'))
|
||||
<div class="supplier-management-alert is-error" role="alert">{{ $errors->first('import') }}</div>
|
||||
@endif
|
||||
<div class="supplier-management-alert is-error" role="alert" data-supplier-load-error hidden>加载失败,当前内容已保留,请重新查询。</div>
|
||||
@if ($supplierError)
|
||||
<div class="supplier-management-alert is-error" role="alert">{{ $supplierError }}</div>
|
||||
@ -87,8 +111,11 @@
|
||||
</div>
|
||||
<div class="supplier-management-panel" data-finance-table-surface>
|
||||
<div class="supplier-management-scroll">
|
||||
<table class="supplier-management-table" data-finance-table aria-label="供应商列表">
|
||||
<thead><tr><th scope="col">序号</th>@foreach ($supplierColumns as $label)<th scope="col">{{ $label }}</th>@endforeach</tr></thead>
|
||||
{{-- 列结构变更后使用新版本键,避免旧列设置把新列及其后字段隐藏。 --}}
|
||||
<table class="supplier-management-table" data-finance-table
|
||||
data-finance-table-key="{{ auth()->id() }}:{{ company_id() }}:supplier-payables:supplier-management:v2"
|
||||
aria-label="供应商列表">
|
||||
<thead><tr><th scope="col">序号</th>@foreach ($supplierColumns as $key => $label)<th scope="col" @if ($key === $freeShippingColumn) data-finance-column-key="{{ $freeShippingColumn }}" @endif>{{ $label }}</th>@endforeach</tr></thead>
|
||||
<tbody>
|
||||
@forelse ($suppliers as $supplier)
|
||||
@php $selectUrl = $localUrl(['supplier_id' => $supplier->Id]); @endphp
|
||||
@ -100,6 +127,14 @@
|
||||
<span @class(['supplier-management-badge', 'is-cooperating' => (int) $supplier->coopersgn === 1])>{{ ['1' => '合作', '2' => '不合作'][(string) $supplier->coopersgn] ?? '未维护' }}</span>
|
||||
@elseif ($key === 'scmname')
|
||||
<a href="{{ $selectUrl }}" data-supplier-local @if ($selectedSupplier && $selectedSupplier->Id == $supplier->Id) aria-current="true" @endif>{{ $cell($supplier->{$key} ?? null) }}</a>
|
||||
@elseif ($key === $freeShippingColumn)
|
||||
<select class="supplier-management-free-shipping" data-supplier-free-shipping
|
||||
data-url="{{ route('business.supplier-management.free-shipping.update', ['supplier' => $supplier->Id]) }}"
|
||||
data-value="{{ !empty($supplier->{$freeShippingColumn}) ? '包邮' : '' }}"
|
||||
aria-label="{{ $cell($supplier->scmname ?? null) }}是否包邮厂家">
|
||||
<option value="" @selected(empty($supplier->{$freeShippingColumn}))>-</option>
|
||||
<option value="包邮" @selected(!empty($supplier->{$freeShippingColumn}))>包邮</option>
|
||||
</select>
|
||||
@else
|
||||
{{ $cell($supplier->{$key} ?? null) }}
|
||||
@endif
|
||||
@ -151,6 +186,29 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@if ($supplierReady && !$supplierError)
|
||||
<div class="supplier-management-import" data-supplier-free-shipping-import hidden>
|
||||
<div class="supplier-management-import__backdrop" data-supplier-free-shipping-import-close></div>
|
||||
<section class="supplier-management-import__dialog" role="dialog" aria-modal="true" aria-labelledby="supplier-free-shipping-import-title">
|
||||
<header>
|
||||
<h2 id="supplier-free-shipping-import-title">导入是否包邮厂家</h2>
|
||||
<button type="button" data-supplier-free-shipping-import-close aria-label="关闭"><span class="material-icons-outlined">close</span></button>
|
||||
</header>
|
||||
<form method="POST" enctype="multipart/form-data"
|
||||
action="{{ route('business.supplier-management.free-shipping.import', request()->only(['supplier_name', 'buyer', 'cooperation_status', 'page'])) }}">
|
||||
@csrf
|
||||
<div class="supplier-management-import__body">
|
||||
<p>按供应商代码匹配。“是否包邮厂家”填写“包邮”表示是;空白或“-”表示不是。可下载模板,也可使用本页导出的文件填写后导入。</p>
|
||||
<input type="file" name="import" accept=".xlsx,.xls,.csv" required>
|
||||
</div>
|
||||
<footer>
|
||||
<button type="button" class="supplier-management-button" data-supplier-free-shipping-import-close>取消</button>
|
||||
<button type="submit" class="supplier-management-button is-primary">开始导入</button>
|
||||
</footer>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
@push('body_end')
|
||||
<script data-caiwu-replay>
|
||||
@ -158,6 +216,7 @@
|
||||
if (window.caiwuSupplierManagementBound) return;
|
||||
window.caiwuSupplierManagementBound = true;
|
||||
let pending = false;
|
||||
let importOpener = null;
|
||||
async function load(href) {
|
||||
const current = document.querySelector('[data-supplier-management]');
|
||||
if (!current || pending) return;
|
||||
@ -188,13 +247,69 @@
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', event => {
|
||||
const importOpen = event.target.closest('[data-supplier-free-shipping-import-open]');
|
||||
if (importOpen) {
|
||||
event.preventDefault();
|
||||
const modal = document.querySelector('[data-supplier-free-shipping-import]');
|
||||
if (!modal) return;
|
||||
modal.hidden = false;
|
||||
importOpener = importOpen;
|
||||
modal.querySelector('input[type="file"]')?.focus();
|
||||
return;
|
||||
}
|
||||
const importClose = event.target.closest('[data-supplier-free-shipping-import-close]');
|
||||
if (importClose) {
|
||||
const modal = importClose.closest('[data-supplier-free-shipping-import]');
|
||||
if (modal) {
|
||||
modal.hidden = true;
|
||||
importOpener?.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
|
||||
const link = event.target.closest('[data-supplier-management] a[data-supplier-local]');
|
||||
const row = event.target.closest('[data-supplier-select]');
|
||||
if (!link && !row) return;
|
||||
if (!link && (!row || event.target.closest('select,button,input'))) return;
|
||||
event.preventDefault();
|
||||
load(link ? link.href : row.dataset.supplierSelect);
|
||||
});
|
||||
document.addEventListener('change', async event => {
|
||||
const select = event.target.closest('[data-supplier-free-shipping]');
|
||||
if (!select) return;
|
||||
const root = select.closest('[data-supplier-management]');
|
||||
const status = root?.querySelector('[data-supplier-load-status]');
|
||||
const previous = select.dataset.value || '';
|
||||
select.disabled = true;
|
||||
select.classList.add('is-saving');
|
||||
if (status) status.textContent = '正在保存是否包邮厂家…';
|
||||
try {
|
||||
const data = new FormData();
|
||||
data.append('_token', root?.dataset.csrf || '');
|
||||
data.append('_method', 'PATCH');
|
||||
data.append('value', select.value);
|
||||
const response = await fetch(select.dataset.url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { Accept: 'application/json' },
|
||||
body: data,
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok || !payload?.ok) {
|
||||
const first = payload?.errors ? Object.values(payload.errors).flat()[0] : null;
|
||||
throw new Error(first || payload?.message || `保存失败(HTTP ${response.status})`);
|
||||
}
|
||||
select.dataset.value = payload.value || '';
|
||||
select.value = payload.value || '';
|
||||
select.title = payload.display || '-';
|
||||
if (status) status.textContent = '是否包邮厂家已保存。';
|
||||
} catch (error) {
|
||||
select.value = previous;
|
||||
if (status) status.textContent = String(error?.message || error);
|
||||
} finally {
|
||||
select.disabled = false;
|
||||
select.classList.remove('is-saving');
|
||||
}
|
||||
});
|
||||
document.addEventListener('submit', event => {
|
||||
const form = event.target.closest('[data-supplier-query]');
|
||||
if (!form) return;
|
||||
@ -202,6 +317,14 @@
|
||||
load(form.action + '?' + new URLSearchParams(new FormData(form)).toString());
|
||||
});
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape') {
|
||||
const modal = document.querySelector('[data-supplier-free-shipping-import]:not([hidden])');
|
||||
if (modal) {
|
||||
modal.hidden = true;
|
||||
importOpener?.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const tab = event.target.closest('[data-supplier-management] [role="tab"]');
|
||||
if (!tab || !['ArrowLeft','ArrowRight','Home','End'].includes(event.key)) return;
|
||||
const tabs = [...tab.parentElement.querySelectorAll('[role="tab"]')];
|
||||
|
||||
@ -636,6 +636,19 @@ Route::get('workspace/supplier-payables/supplier-management/export', [\App\Http\
|
||||
->middleware('business.page:supplier-payables,supplier-management')
|
||||
->name('business.supplier-management.export');
|
||||
|
||||
Route::get('workspace/supplier-payables/supplier-management/free-shipping-manufacturer-template', [\App\Http\Controllers\Business\SupplierManagement::class, 'template'])
|
||||
->middleware('business.page:supplier-payables,supplier-management')
|
||||
->name('business.supplier-management.free-shipping.template');
|
||||
|
||||
Route::post('workspace/supplier-payables/supplier-management/free-shipping-manufacturer-import', [\App\Http\Controllers\Business\SupplierManagement::class, 'import'])
|
||||
->middleware(['business.page:supplier-payables,supplier-management', 'import'])
|
||||
->name('business.supplier-management.free-shipping.import');
|
||||
|
||||
Route::patch('workspace/supplier-payables/supplier-management/free-shipping-manufacturer/{supplier}', [\App\Http\Controllers\Business\SupplierManagement::class, 'updateFreeShipping'])
|
||||
->whereNumber('supplier')
|
||||
->middleware('business.page:supplier-payables,supplier-management')
|
||||
->name('business.supplier-management.free-shipping.update');
|
||||
|
||||
Route::post('workspace/financial-ledger/mapping/{id}', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateMapping'])
|
||||
->whereNumber('id')
|
||||
->middleware('business.page:financial-ledger,ledger')
|
||||
|
||||
30
tests/Feature/Business/SupplierFreeShippingStoreTest.php
Normal file
30
tests/Feature/Business/SupplierFreeShippingStoreTest.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use Modules\EcommerceFinance\Services\SupplierFreeShippingStore;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class SupplierFreeShippingStoreTest extends FeatureTestCase
|
||||
{
|
||||
public function test_marks_are_scoped_by_company_and_default_to_false(): void
|
||||
{
|
||||
$store = app(SupplierFreeShippingStore::class);
|
||||
$companyId = (int) $this->company->id;
|
||||
$store->save(9, true, $companyId, (int) $this->user->id, 'manual');
|
||||
$store->save(11, false, $companyId, (int) $this->user->id, 'manual');
|
||||
|
||||
$rows = $store->decorateRows([
|
||||
(object) ['Id' => 9],
|
||||
(object) ['Id' => 11],
|
||||
(object) ['Id' => 12],
|
||||
], $companyId);
|
||||
|
||||
$this->assertTrue($rows[0]->free_shipping_manufacturer);
|
||||
$this->assertFalse($rows[1]->free_shipping_manufacturer);
|
||||
$this->assertFalse($rows[2]->free_shipping_manufacturer);
|
||||
|
||||
$otherCompanyRows = $store->decorateRows([(object) ['Id' => 9]], $companyId + 1);
|
||||
$this->assertFalse($otherCompanyRows[0]->free_shipping_manufacturer);
|
||||
}
|
||||
}
|
||||
55
tests/Feature/Business/SupplierFreeShippingWorkbookTest.php
Normal file
55
tests/Feature/Business/SupplierFreeShippingWorkbookTest.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Modules\EcommerceFinance\Services\SupplierFreeShippingWorkbook;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SupplierFreeShippingWorkbookTest extends TestCase
|
||||
{
|
||||
public function test_csv_reads_package_shipping_value_and_blank_as_false(): void
|
||||
{
|
||||
$file = UploadedFile::fake()->createWithContent(
|
||||
'supplier-free-shipping.csv',
|
||||
"\xEF\xBB\xBF供应商代码,供应商名称,是否包邮厂家\nN901,一泰,包邮\nY411,七彩,\nS30,万都装饰,-\n",
|
||||
);
|
||||
|
||||
$this->assertSame([
|
||||
'N901' => true,
|
||||
'Y411' => false,
|
||||
'S30' => false,
|
||||
], app(SupplierFreeShippingWorkbook::class)->read($file));
|
||||
}
|
||||
|
||||
public function test_invalid_package_shipping_value_is_rejected_with_row_number(): void
|
||||
{
|
||||
$file = UploadedFile::fake()->createWithContent(
|
||||
'supplier-free-shipping.csv',
|
||||
"供应商代码,是否包邮厂家\nN901,待确认\n",
|
||||
);
|
||||
|
||||
$this->expectException(ValidationException::class);
|
||||
try {
|
||||
app(SupplierFreeShippingWorkbook::class)->read($file);
|
||||
} catch (ValidationException $exception) {
|
||||
$this->assertSame(
|
||||
['import' => ['第 2 行“是否包邮厂家”只能填写“包邮”,或留空表示否。']],
|
||||
$exception->errors(),
|
||||
);
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
public function test_case_insensitive_duplicate_codes_are_rejected(): void
|
||||
{
|
||||
$file = UploadedFile::fake()->createWithContent(
|
||||
'supplier-free-shipping.csv',
|
||||
"供应商代码,是否包邮厂家\nN901,包邮\nn901,\n",
|
||||
);
|
||||
|
||||
$this->expectException(ValidationException::class);
|
||||
app(SupplierFreeShippingWorkbook::class)->read($file);
|
||||
}
|
||||
}
|
||||
@ -3,12 +3,17 @@
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository;
|
||||
use Modules\EcommerceFinance\Services\SupplierFreeShippingStore;
|
||||
use Modules\EcommerceFinance\Services\SupplierFreeShippingWorkbook;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class SupplierManagementPageTest extends FeatureTestCase
|
||||
{
|
||||
private \Mockery\MockInterface $freeShipping;
|
||||
|
||||
private function repository(bool $ready): \Mockery\MockInterface
|
||||
{
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
@ -16,6 +21,15 @@ class SupplierManagementPageTest extends FeatureTestCase
|
||||
$repository->shouldReceive('ready')->andReturn($ready);
|
||||
$repository->shouldReceive('state')->andReturn(null);
|
||||
$this->app->instance(SupplierManagementMirrorRepository::class, $repository);
|
||||
$this->freeShipping = Mockery::mock(SupplierFreeShippingStore::class);
|
||||
$this->freeShipping->shouldReceive('decorateRows')->andReturnUsing(
|
||||
static fn (array $rows): array => $rows,
|
||||
);
|
||||
$this->freeShipping->shouldReceive('decorateRow')->andReturnUsing(
|
||||
static fn (object $row): object => $row,
|
||||
);
|
||||
$this->app->instance(SupplierFreeShippingStore::class, $this->freeShipping);
|
||||
|
||||
return $repository;
|
||||
}
|
||||
|
||||
@ -36,7 +50,8 @@ class SupplierManagementPageTest extends FeatureTestCase
|
||||
{
|
||||
$repository = $this->repository(true);
|
||||
$row = (object) ['Id' => 3, 'scmcode' => '001', 'scmname' => '测试供应商', 'coopersgn' => 1,
|
||||
'region' => '苏北', 'sindou' => '双日', 'capacity' => '120'];
|
||||
'region' => '苏北', 'sindou' => '双日', 'capacity' => '120',
|
||||
SupplierFreeShippingStore::FIELD => true];
|
||||
$repository->shouldReceive('paginate')->once()->with(['supplier_name' => '测试', 'buyer' => '', 'cooperation_status' => '1'], 1, 20)
|
||||
->andReturn(['rows' => [$row], 'total' => 1]);
|
||||
$repository->shouldReceive('details')->once()->with($row, 'contacts', 1, 20)
|
||||
@ -44,7 +59,11 @@ class SupplierManagementPageTest extends FeatureTestCase
|
||||
$this->loginAs()->get($this->url(['supplier_name' => ' 测试 ', 'cooperation_status' => '1', 'supplier_id' => 3, 'detail_tab' => 'contacts']))
|
||||
->assertOk()->assertSeeText('测试供应商')->assertSeeText('测试联系人')->assertSee('data-caiwu-replay', false)->assertSeeText('打款信息')
|
||||
->assertSeeText('工厂产能')->assertSeeText('叫货周期')->assertSeeText('赔付终止日期')
|
||||
->assertSeeText('苏北')->assertSeeText('双日')->assertSeeText('120');
|
||||
->assertSeeText('苏北')->assertSeeText('双日')->assertSeeText('120')
|
||||
->assertSeeText('是否包邮厂家')->assertSeeText('包邮')
|
||||
->assertSee('data-supplier-free-shipping', false)
|
||||
->assertSee('>-</option>', false)
|
||||
->assertSee(route('business.supplier-management.free-shipping.template'), false);
|
||||
}
|
||||
|
||||
public function test_detail_failure_keeps_the_supplier_list_visible(): void
|
||||
@ -66,10 +85,62 @@ class SupplierManagementPageTest extends FeatureTestCase
|
||||
$response = $this->loginAs()->get(route('business.supplier-management.export', ['supplier_name' => '测试', 'page' => 2]))->assertOk();
|
||||
$content = $response->streamedContent();
|
||||
$this->assertStringStartsWith("\xEF\xBB\xBF", $content);
|
||||
$this->assertStringStartsWith(
|
||||
"数据来源,是否合作,供应商代码,供应商名称,是否包邮厂家,供应商类型",
|
||||
ltrim($content, "\xEF\xBB\xBF"),
|
||||
);
|
||||
$this->assertSame(23, substr_count($content, "'=SUM(1,2)"));
|
||||
$this->assertStringContainsString("'123456789012345678", $content);
|
||||
}
|
||||
|
||||
public function test_free_shipping_flag_can_be_edited_inline(): void
|
||||
{
|
||||
$repository = $this->repository(true);
|
||||
$supplier = (object) ['Id' => 9, 'scmcode' => 'N901', 'scmname' => '一泰'];
|
||||
$repository->shouldReceive('supplierById')->once()->with(9)->andReturn($supplier);
|
||||
$this->freeShipping->shouldReceive('save')->once()
|
||||
->with(9, true, Mockery::type('int'), Mockery::type('int'), 'manual')
|
||||
->andReturn(['created' => 1, 'updated' => 0]);
|
||||
|
||||
$this->loginAs()->patchJson(
|
||||
route('business.supplier-management.free-shipping.update', ['supplier' => 9]),
|
||||
['value' => '包邮'],
|
||||
)->assertOk()->assertJson([
|
||||
'ok' => true,
|
||||
'value' => '包邮',
|
||||
'display' => '包邮',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_free_shipping_import_matches_supplier_codes_and_saves_blank_as_false(): void
|
||||
{
|
||||
$repository = $this->repository(true);
|
||||
$suppliers = [
|
||||
(object) ['Id' => 9, 'scmcode' => 'N901', 'scmname' => '一泰'],
|
||||
(object) ['Id' => 11, 'scmcode' => 'Y411', 'scmname' => '七彩'],
|
||||
];
|
||||
$repository->shouldReceive('suppliersByCodes')->once()->with(['N901', 'Y411'])->andReturn($suppliers);
|
||||
$workbook = Mockery::mock(SupplierFreeShippingWorkbook::class);
|
||||
$workbook->shouldReceive('read')->once()->andReturn(['N901' => true, 'Y411' => false]);
|
||||
$this->app->instance(SupplierFreeShippingWorkbook::class, $workbook);
|
||||
$this->freeShipping->shouldReceive('saveMany')->once()
|
||||
->with([9 => true, 11 => false], Mockery::type('int'), Mockery::type('int'), 'import')
|
||||
->andReturn(['created' => 2, 'updated' => 0]);
|
||||
|
||||
$this->loginAs()->post(route('business.supplier-management.free-shipping.import'), [
|
||||
'import' => UploadedFile::fake()->create('free-shipping.xlsx', 10, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
|
||||
])->assertRedirect()
|
||||
->assertSessionHas('supplier_management_message', '导入完成:新增 2 家供应商维护记录,更新 0 家供应商维护记录。');
|
||||
}
|
||||
|
||||
public function test_free_shipping_template_is_downloadable(): void
|
||||
{
|
||||
$this->repository(true);
|
||||
|
||||
$response = $this->loginAs()->get(route('business.supplier-management.free-shipping.template'))->assertOk();
|
||||
$this->assertStringStartsWith('PK', $response->streamedContent());
|
||||
}
|
||||
|
||||
public function test_unauthenticated_export_requires_login(): void
|
||||
{
|
||||
$this->withExceptionHandling()->get(route('business.supplier-management.export', ['company_id' => $this->company->id]))->assertRedirect();
|
||||
|
||||
@ -139,6 +139,20 @@ class SupplierManagementMirrorRepositoryTest extends TestCase
|
||||
$this->assertNull($this->repository->signingSupplier(999));
|
||||
}
|
||||
|
||||
public function test_supplier_lookup_uses_supplier_codes_for_local_supplements(): void
|
||||
{
|
||||
$this->ready(SupplierManagementMirrorRepository::DATASET);
|
||||
$this->table('js_supplier_management')->insert([
|
||||
['Id' => 9, 'scmcode' => 'N901', 'scmname' => '一泰'],
|
||||
['Id' => 11, 'scmcode' => 'Y411', 'scmname' => '七彩'],
|
||||
]);
|
||||
|
||||
$rows = $this->repository->suppliersByCodes(['N901', 'Y411', 'UNKNOWN']);
|
||||
$this->assertSame([9, 11], array_map(static fn (object $row): int => (int) $row->Id, $rows));
|
||||
$this->assertSame('一泰', $this->repository->supplierById(9)->scmname);
|
||||
$this->assertNull($this->repository->supplierById(999));
|
||||
}
|
||||
|
||||
private function ready(string $key): void
|
||||
{
|
||||
$this->table('business_mirror_table_states')->insert(['dataset_key' => $key, 'initial_completed_at' => '2026-10-11 00:00:00']);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user