Add warehouse short names to inventory report
Restore warehouse name mapping ledger and inventory short-name column.
This commit is contained in:
parent
e73183f52e
commit
26556a9169
@ -145,7 +145,8 @@ docker compose -f docker-compose.local.yml start
|
|||||||
|
|
||||||
## 修改后的生效方式
|
## 修改后的生效方式
|
||||||
|
|
||||||
- PHP、配置、模块、页面模板和路由目录已经挂载到容器,本地 PHP OPcache 会在每次请求检查源码变更,刷新浏览器即可生效;
|
- PHP、配置、模块、页面模板和路由目录已经挂载到容器;开发环境关闭 PHP OPcache 和 Blade 编译缓存,刷新浏览器即可加载最新源码,无需重启 PHP 服务;
|
||||||
|
- 开发环境启动时只执行 `optimize:clear`,不会生成 Laravel 的配置、路由或视图缓存;生产环境仍保留原有缓存构建机制;
|
||||||
- 本地 Compose 会同时启动 `frontend` watcher。修改 `resources/assets` 下的 Vue、JavaScript 或 SCSS 后,Laravel Mix 会通过轮询自动重新编译,Apache 使用共享静态资源卷提供最新文件;浏览器刷新即可看到结果。
|
- 本地 Compose 会同时启动 `frontend` watcher。修改 `resources/assets` 下的 Vue、JavaScript 或 SCSS 后,Laravel Mix 会通过轮询自动重新编译,Apache 使用共享静态资源卷提供最新文件;浏览器刷新即可看到结果。
|
||||||
- 如果 watcher 构建失败,查看 `docker compose -f docker-compose.local.yml logs -f frontend`;修复源码后它会继续监听并重新编译。
|
- 如果 watcher 构建失败,查看 `docker compose -f docker-compose.local.yml logs -f frontend`;修复源码后它会继续监听并重新编译。
|
||||||
- Composer 依赖、前端依赖、Dockerfile 或构建资源发生变化时,重新执行 `bash deploy/local-init.sh`,脚本会自动重建;
|
- Composer 依赖、前端依赖、Dockerfile 或构建资源发生变化时,重新执行 `bash deploy/local-init.sh`,脚本会自动重建;
|
||||||
|
|||||||
213
app/Business/InventoryWarehouseNameMappingWorkbookImporter.php
Normal file
213
app/Business/InventoryWarehouseNameMappingWorkbookImporter.php
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Business;
|
||||||
|
|
||||||
|
use App\Models\Business\InventoryWarehouseNameMapping;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
|
||||||
|
class InventoryWarehouseNameMappingWorkbookImporter
|
||||||
|
{
|
||||||
|
private const HEADERS = ['仓储方', '仓库', '仓库简称'];
|
||||||
|
|
||||||
|
/** @return array{rows:list<array>,sheet:string,headers:list<string>,summary:array} */
|
||||||
|
public function preview(UploadedFile $file): array
|
||||||
|
{
|
||||||
|
$spreadsheet = IOFactory::load($file->getRealPath());
|
||||||
|
$worksheet = $this->findWorksheet($spreadsheet);
|
||||||
|
|
||||||
|
if ($worksheet === null) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'import' => '未找到包含“仓储方、仓库、仓库简称”三列的工作表。Sheet2 仅是辅助仓库清单,不能直接导入。',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $worksheet->toArray(null, true, false, false);
|
||||||
|
$parsed = [];
|
||||||
|
$seen = [];
|
||||||
|
$created = 0;
|
||||||
|
$same = 0;
|
||||||
|
$conflicts = 0;
|
||||||
|
$invalid = 0;
|
||||||
|
|
||||||
|
foreach (array_slice($rows, 1) as $offset => $row) {
|
||||||
|
$rowNumber = $offset + 2;
|
||||||
|
$provider = trim((string) ($row[0] ?? ''));
|
||||||
|
$warehouse = trim((string) ($row[1] ?? ''));
|
||||||
|
$shortName = trim((string) ($row[2] ?? ''));
|
||||||
|
if ($provider === '' && $warehouse === '' && $shortName === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $this->key($provider, $warehouse);
|
||||||
|
$item = [
|
||||||
|
'row_number' => $rowNumber,
|
||||||
|
'warehouse_provider' => $provider,
|
||||||
|
'warehouse' => $warehouse,
|
||||||
|
'warehouse_short_name' => $shortName,
|
||||||
|
'status' => 'new',
|
||||||
|
'message' => '待新增',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($provider === '' || $warehouse === '' || $shortName === '') {
|
||||||
|
$item['status'] = 'invalid';
|
||||||
|
$item['message'] = '仓储方、仓库、仓库简称均为必填字段';
|
||||||
|
$invalid++;
|
||||||
|
} elseif (isset($seen[$key])) {
|
||||||
|
$item['status'] = $seen[$key]['warehouse_short_name'] === $shortName ? 'same' : 'conflict';
|
||||||
|
$item['message'] = $item['status'] === 'same'
|
||||||
|
? '与文件内前一行重复,将跳过'
|
||||||
|
: '与文件内前一行的仓库简称冲突';
|
||||||
|
$item['status'] === 'same' ? $same++ : $conflicts++;
|
||||||
|
} else {
|
||||||
|
$existing = InventoryWarehouseNameMapping::query()
|
||||||
|
->where('warehouse_provider', $provider)
|
||||||
|
->where('warehouse', $warehouse)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$item['status'] = $existing->warehouse_short_name === $shortName ? 'same' : 'conflict';
|
||||||
|
$item['message'] = $item['status'] === 'same'
|
||||||
|
? '数据库已有相同映射,将跳过'
|
||||||
|
: '数据库已有不同简称,禁止静默覆盖';
|
||||||
|
$item['status'] === 'same' ? $same++ : $conflicts++;
|
||||||
|
} else {
|
||||||
|
$created++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsed[] = $item;
|
||||||
|
if ($provider !== '' && $warehouse !== '' && $shortName !== '' && ! isset($seen[$key])) {
|
||||||
|
$seen[$key] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'rows' => $parsed,
|
||||||
|
'sheet' => $worksheet->getTitle(),
|
||||||
|
'headers' => self::HEADERS,
|
||||||
|
'summary' => compact('created', 'same', 'conflicts', 'invalid'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array{rows:list<array>} $preview */
|
||||||
|
public function commit(array $preview): array
|
||||||
|
{
|
||||||
|
$rows = $preview['rows'] ?? [];
|
||||||
|
$invalid = collect($rows)->first(fn (array $row): bool => in_array($row['status'] ?? '', ['invalid', 'conflict'], true));
|
||||||
|
if ($invalid !== null) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'import' => '导入预览存在必填字段错误或简称冲突,请先处理标记为失败的行。',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$created = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
DB::transaction(function () use ($rows, &$created, &$skipped): void {
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (($row['status'] ?? '') === 'same') {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = InventoryWarehouseNameMapping::query()
|
||||||
|
->where('warehouse_provider', $row['warehouse_provider'])
|
||||||
|
->where('warehouse', $row['warehouse']);
|
||||||
|
if ($query->exists()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'import' => "第 {$row['row_number']} 行在确认时已被其他操作占用,请重新预览。",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
InventoryWarehouseNameMapping::create([
|
||||||
|
'company_id' => company_id() ?: 1,
|
||||||
|
'warehouse_provider' => $row['warehouse_provider'],
|
||||||
|
'warehouse' => $row['warehouse'],
|
||||||
|
'warehouse_short_name' => $row['warehouse_short_name'],
|
||||||
|
'created_by' => auth()->id(),
|
||||||
|
'updated_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
$created++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return compact('created', 'skipped');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function suggestion(string $provider, string $warehouse): string
|
||||||
|
{
|
||||||
|
$provider = trim($provider);
|
||||||
|
$warehouse = trim($warehouse);
|
||||||
|
$haystack = $provider . ' ' . $warehouse;
|
||||||
|
|
||||||
|
if (str_starts_with($provider, '奇门-')) {
|
||||||
|
return '不看';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'配件单仓' => '配件单仓-不看',
|
||||||
|
'POP仓' => '京东入仓-不看',
|
||||||
|
'自动发货仓' => '虚拟仓-不看',
|
||||||
|
'京东云仓' => '京东云仓-不看',
|
||||||
|
'京东京造' => '京东京造-不看',
|
||||||
|
'苏州福利品仓' => '苏州福利品仓-不看',
|
||||||
|
'信誉订单仓' => '信誉订单仓-不看',
|
||||||
|
'代发仓' => '代发仓',
|
||||||
|
'销退仓' => '销退仓-不看',
|
||||||
|
'客退' => '客退仓-不看',
|
||||||
|
'次品仓' => '退货报废-不看',
|
||||||
|
'数量校验仓' => '数量校验仓-不看',
|
||||||
|
] as $needle => $result) {
|
||||||
|
if (str_contains($haystack, $needle)) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'运营C崔桥工厂仓' => '崔桥工厂仓',
|
||||||
|
'运营C横林二仓' => '横林二仓',
|
||||||
|
'运营C睢宁' => '睢宁仓',
|
||||||
|
'运营C新苏北' => '新苏北仓',
|
||||||
|
'江西仓送货' => '南昌樵舍仓',
|
||||||
|
] as $needle => $result) {
|
||||||
|
if (str_contains($haystack, $needle)) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$short = preg_replace('/(主仓|进货仓)$/u', '', $warehouse) ?: $warehouse;
|
||||||
|
$short = preg_replace('/^(简尚-|京仓-|三方-|奇门-)/u', '', $short) ?: $short;
|
||||||
|
$providerBase = preg_replace('/^(简尚-|京仓-|三方-|奇门-)/u', '', $provider) ?: $provider;
|
||||||
|
if ($short === $providerBase || str_starts_with($short, $providerBase)) {
|
||||||
|
$short = $providerBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $short !== '' ? $short : $warehouse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findWorksheet($spreadsheet): ?object
|
||||||
|
{
|
||||||
|
foreach ($spreadsheet->getWorksheetIterator() as $worksheet) {
|
||||||
|
$row = $worksheet->toArray(null, true, false, false)[0] ?? [];
|
||||||
|
$headers = array_map(fn ($value): string => $this->normalize((string) $value), $row);
|
||||||
|
$expected = array_map(fn ($value): string => $this->normalize($value), self::HEADERS);
|
||||||
|
if (count(array_intersect($expected, $headers)) === count($expected)) {
|
||||||
|
return $worksheet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalize(string $value): string
|
||||||
|
{
|
||||||
|
return mb_strtolower(preg_replace('/[\s\x{FEFF}_\-()()]+/u', '', trim($value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function key(string $provider, string $warehouse): string
|
||||||
|
{
|
||||||
|
return $provider . "\0" . $warehouse;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Business/InventoryWarehouseShortNames.php
Normal file
35
app/Business/InventoryWarehouseShortNames.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Business;
|
||||||
|
|
||||||
|
use App\Models\Business\InventoryWarehouseNameMapping;
|
||||||
|
|
||||||
|
class InventoryWarehouseShortNames
|
||||||
|
{
|
||||||
|
/** @return array<string,string> */
|
||||||
|
public function forCompany(int $companyId): array
|
||||||
|
{
|
||||||
|
return InventoryWarehouseNameMapping::allCompanies()
|
||||||
|
->companyId($companyId)
|
||||||
|
->get(['warehouse_provider', 'warehouse', 'warehouse_short_name'])
|
||||||
|
->mapWithKeys(static fn (InventoryWarehouseNameMapping $mapping): array => [
|
||||||
|
self::key($mapping->warehouse_provider, $mapping->warehouse) => $mapping->warehouse_short_name,
|
||||||
|
])->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,string> $names */
|
||||||
|
public function fill(object $row, array $names): object
|
||||||
|
{
|
||||||
|
$row->warehouse_short_name = $names[self::key(
|
||||||
|
(string) ($row->warehouse_provider ?? ''),
|
||||||
|
(string) ($row->warehouse ?? ''),
|
||||||
|
)] ?? '';
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function key(string $provider, string $warehouse): string
|
||||||
|
{
|
||||||
|
return $provider . "\0" . $warehouse;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@
|
|||||||
namespace App\Http\Controllers\Business;
|
namespace App\Http\Controllers\Business;
|
||||||
|
|
||||||
use App\Business\BusinessTableImporter;
|
use App\Business\BusinessTableImporter;
|
||||||
|
use App\Business\InventoryWarehouseNameMappingWorkbookImporter;
|
||||||
|
use App\Business\InventoryWarehouseShortNames;
|
||||||
use App\Business\CourierPayableSummaryFilter;
|
use App\Business\CourierPayableSummaryFilter;
|
||||||
use App\Business\CourierPayableSummaryStore;
|
use App\Business\CourierPayableSummaryStore;
|
||||||
use App\Business\FundForecastTemplate;
|
use App\Business\FundForecastTemplate;
|
||||||
@ -18,6 +20,7 @@ use App\Models\Business\CourierQuoteSignature;
|
|||||||
use App\Models\Business\CourierQuoteVersion;
|
use App\Models\Business\CourierQuoteVersion;
|
||||||
use App\Models\Business\CourierSigningSubject;
|
use App\Models\Business\CourierSigningSubject;
|
||||||
use App\Models\Business\InstallationDetail;
|
use App\Models\Business\InstallationDetail;
|
||||||
|
use App\Models\Business\InventoryWarehouseNameMapping;
|
||||||
use App\Business\CourierSurchargeAuditLogRepository;
|
use App\Business\CourierSurchargeAuditLogRepository;
|
||||||
use App\Business\CourierSurchargeQuoteRepository;
|
use App\Business\CourierSurchargeQuoteRepository;
|
||||||
use App\Business\CourierSurchargeTypeRepository;
|
use App\Business\CourierSurchargeTypeRepository;
|
||||||
@ -32,6 +35,11 @@ use Illuminate\Support\Carbon;
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
|
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||||
use Modules\EcommerceFinance\Repositories\AlibabaAccountBalanceRepository;
|
use Modules\EcommerceFinance\Repositories\AlibabaAccountBalanceRepository;
|
||||||
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
|
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
|
||||||
use Modules\EcommerceFinance\View\BillOffsetPageData;
|
use Modules\EcommerceFinance\View\BillOffsetPageData;
|
||||||
@ -380,11 +388,20 @@ class Pages extends Controller
|
|||||||
$tabValue = null;
|
$tabValue = null;
|
||||||
$tabColumn = null;
|
$tabColumn = null;
|
||||||
$upstreamError = null;
|
$upstreamError = null;
|
||||||
|
$inventoryWarehouseMappings = collect();
|
||||||
$agreementCourierNames = $this->agreementCourierNames($page['slug'] ?? null);
|
$agreementCourierNames = $this->agreementCourierNames($page['slug'] ?? null);
|
||||||
$agreementCourierAccounts = $this->agreementCourierAccounts($page['slug'] ?? null, $agreementCourierNames);
|
$agreementCourierAccounts = $this->agreementCourierAccounts($page['slug'] ?? null, $agreementCourierNames);
|
||||||
|
|
||||||
if (($section['slug'] ?? null) === 'product-inventory' && ($page['slug'] ?? null) === 'inventory-report') {
|
if (($section['slug'] ?? null) === 'product-inventory' && ($page['slug'] ?? null) === 'inventory-report') {
|
||||||
[$rows, $upstreamError] = $this->inventoryReportRows();
|
if (request()->boolean('mapping')) {
|
||||||
|
$rows = new LengthAwarePaginator([], 0, 1, 1, [
|
||||||
|
'path' => request()->url(),
|
||||||
|
'query' => request()->query(),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
[$rows, $upstreamError] = $this->inventoryReportRows();
|
||||||
|
}
|
||||||
|
$inventoryWarehouseMappings = $this->inventoryWarehouseMappingRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($page && $content === null) {
|
if ($page && $content === null) {
|
||||||
@ -432,6 +449,7 @@ class Pages extends Controller
|
|||||||
'agreementCourierNames' => $agreementCourierNames,
|
'agreementCourierNames' => $agreementCourierNames,
|
||||||
'agreementCourierAccounts' => $agreementCourierAccounts,
|
'agreementCourierAccounts' => $agreementCourierAccounts,
|
||||||
'agreementQuoteVersions' => $this->agreementQuoteVersions($page['slug']),
|
'agreementQuoteVersions' => $this->agreementQuoteVersions($page['slug']),
|
||||||
|
'inventoryWarehouseMappings' => $inventoryWarehouseMappings,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,6 +508,7 @@ class Pages extends Controller
|
|||||||
'agreementCourierNames' => $agreementCourierNames,
|
'agreementCourierNames' => $agreementCourierNames,
|
||||||
'agreementCourierAccounts' => $agreementCourierAccounts,
|
'agreementCourierAccounts' => $agreementCourierAccounts,
|
||||||
'agreementQuoteVersions' => $this->agreementQuoteVersions($page['slug'] ?? null),
|
'agreementQuoteVersions' => $this->agreementQuoteVersions($page['slug'] ?? null),
|
||||||
|
'inventoryWarehouseMappings' => $inventoryWarehouseMappings,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,8 +532,11 @@ class Pages extends Controller
|
|||||||
$perPage,
|
$perPage,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$shortNames = app(InventoryWarehouseShortNames::class);
|
||||||
|
$names = $shortNames->forCompany((int) (company_id() ?: 1));
|
||||||
|
|
||||||
return [new LengthAwarePaginator(
|
return [new LengthAwarePaginator(
|
||||||
$result['data'],
|
array_map(static fn (object $row): object => $shortNames->fill($row, $names), $result['data']),
|
||||||
$result['total'],
|
$result['total'],
|
||||||
$perPage,
|
$perPage,
|
||||||
$page,
|
$page,
|
||||||
@ -535,6 +557,163 @@ class Pages extends Controller
|
|||||||
]), $error];
|
]), $error];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function inventoryWarehouseMappingRows(): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
$query = InventoryWarehouseNameMapping::query()->orderBy('warehouse_provider')->orderBy('warehouse');
|
||||||
|
$provider = mb_substr(trim((string) request('mapping_warehouse_provider', '')), 0, 191);
|
||||||
|
$warehouse = mb_substr(trim((string) request('mapping_warehouse', '')), 0, 191);
|
||||||
|
|
||||||
|
if ($provider !== '') {
|
||||||
|
$query->where('warehouse_provider', 'like', '%' . addcslashes($provider, '%_\\') . '%');
|
||||||
|
}
|
||||||
|
if ($warehouse !== '') {
|
||||||
|
$query->where('warehouse', 'like', '%' . addcslashes($warehouse, '%_\\') . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->paginate(50)->withQueryString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeInventoryWarehouseMapping(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'warehouse_provider' => ['required', 'string', 'max:191'],
|
||||||
|
'warehouse' => ['required', 'string', 'max:191'],
|
||||||
|
'warehouse_short_name' => ['nullable', 'string', 'max:191'],
|
||||||
|
]);
|
||||||
|
$data['warehouse_provider'] = trim($data['warehouse_provider']);
|
||||||
|
$data['warehouse'] = trim($data['warehouse']);
|
||||||
|
$data['warehouse_short_name'] = trim((string) ($data['warehouse_short_name'] ?? ''))
|
||||||
|
?: InventoryWarehouseNameMappingWorkbookImporter::suggestion($data['warehouse_provider'], $data['warehouse']);
|
||||||
|
|
||||||
|
if (InventoryWarehouseNameMapping::query()
|
||||||
|
->where('warehouse_provider', $data['warehouse_provider'])
|
||||||
|
->where('warehouse', $data['warehouse'])
|
||||||
|
->exists()) {
|
||||||
|
return back()->withInput()->withErrors(['warehouse' => '相同“仓储方 + 仓库”已存在,请使用修改功能。']);
|
||||||
|
}
|
||||||
|
|
||||||
|
InventoryWarehouseNameMapping::create($data + [
|
||||||
|
'company_id' => company_id() ?: 1,
|
||||||
|
'created_by' => auth()->id(),
|
||||||
|
'updated_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->inventoryWarehouseMappingRedirect()->with('success', '仓库名称映射已新增。');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateInventoryWarehouseMapping(Request $request, int $id)
|
||||||
|
{
|
||||||
|
$mapping = InventoryWarehouseNameMapping::query()->findOrFail($id);
|
||||||
|
$data = $request->validate([
|
||||||
|
'warehouse_provider' => ['required', 'string', 'max:191'],
|
||||||
|
'warehouse' => ['required', 'string', 'max:191'],
|
||||||
|
'warehouse_short_name' => ['required', 'string', 'max:191'],
|
||||||
|
]);
|
||||||
|
$data = array_map(static fn (string $value): string => trim($value), $data);
|
||||||
|
|
||||||
|
if (InventoryWarehouseNameMapping::query()
|
||||||
|
->where('id', '!=', $mapping->id)
|
||||||
|
->where('warehouse_provider', $data['warehouse_provider'])
|
||||||
|
->where('warehouse', $data['warehouse'])
|
||||||
|
->exists()) {
|
||||||
|
return back()->withInput()->withErrors(['warehouse' => '相同“仓储方 + 仓库”已存在,不能产生重复台账。']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapping->update($data + ['updated_by' => auth()->id()]);
|
||||||
|
|
||||||
|
return $this->inventoryWarehouseMappingRedirect()->with('success', '仓库名称映射已修改。');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function previewInventoryWarehouseMappingImport(
|
||||||
|
Request $request,
|
||||||
|
InventoryWarehouseNameMappingWorkbookImporter $importer,
|
||||||
|
) {
|
||||||
|
$request->validate([
|
||||||
|
'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:10240'],
|
||||||
|
]);
|
||||||
|
$preview = $importer->preview($request->file('import'));
|
||||||
|
$token = Str::random(40);
|
||||||
|
session()->put('inventory_warehouse_mapping_previews.' . $token, $preview);
|
||||||
|
|
||||||
|
return $this->inventoryWarehouseMappingRedirect()
|
||||||
|
->with('warehouse_mapping_preview', $preview + ['token' => $token]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function commitInventoryWarehouseMappingImport(
|
||||||
|
Request $request,
|
||||||
|
InventoryWarehouseNameMappingWorkbookImporter $importer,
|
||||||
|
) {
|
||||||
|
$token = (string) $request->input('preview_token');
|
||||||
|
$preview = session()->pull('inventory_warehouse_mapping_previews.' . $token);
|
||||||
|
abort_if($token === '' || ! is_array($preview), 422, '导入预览已过期,请重新上传文件。');
|
||||||
|
$result = $importer->commit($preview);
|
||||||
|
|
||||||
|
return $this->inventoryWarehouseMappingRedirect()->with(
|
||||||
|
'success',
|
||||||
|
"导入完成:新增 {$result['created']} 条,跳过 {$result['skipped']} 条重复映射。",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportInventoryWarehouseMappings(Request $request): StreamedResponse
|
||||||
|
{
|
||||||
|
$query = InventoryWarehouseNameMapping::query()->orderBy('warehouse_provider')->orderBy('warehouse');
|
||||||
|
$provider = mb_substr(trim((string) $request->query('mapping_warehouse_provider', '')), 0, 191);
|
||||||
|
$warehouse = mb_substr(trim((string) $request->query('mapping_warehouse', '')), 0, 191);
|
||||||
|
if ($provider !== '') {
|
||||||
|
$query->where('warehouse_provider', 'like', '%' . addcslashes($provider, '%_\\') . '%');
|
||||||
|
}
|
||||||
|
if ($warehouse !== '') {
|
||||||
|
$query->where('warehouse', 'like', '%' . addcslashes($warehouse, '%_\\') . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->streamDownload(function () use ($query): void {
|
||||||
|
$spreadsheet = new Spreadsheet();
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
$sheet->setTitle('仓库名称映射表');
|
||||||
|
$headers = ['仓储方', '仓库', '仓库简称'];
|
||||||
|
$sheet->fromArray($headers, null, 'A1');
|
||||||
|
$lastColumn = Coordinate::stringFromColumnIndex(count($headers));
|
||||||
|
$headerRange = "A1:{$lastColumn}1";
|
||||||
|
$sheet->getStyle($headerRange)->applyFromArray([
|
||||||
|
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||||||
|
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '856632']],
|
||||||
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||||
|
]);
|
||||||
|
$sheet->getRowDimension(1)->setRowHeight(24);
|
||||||
|
$sheet->freezePane('A2');
|
||||||
|
$sheet->setAutoFilter($headerRange);
|
||||||
|
foreach ([20, 32, 20] as $index => $width) {
|
||||||
|
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
|
||||||
|
}
|
||||||
|
$rowNumber = 2;
|
||||||
|
foreach ($query->cursor() as $mapping) {
|
||||||
|
$sheet->fromArray([
|
||||||
|
$mapping->warehouse_provider,
|
||||||
|
$mapping->warehouse,
|
||||||
|
$mapping->warehouse_short_name,
|
||||||
|
], null, "A{$rowNumber}");
|
||||||
|
$rowNumber++;
|
||||||
|
}
|
||||||
|
(new Xlsx($spreadsheet))->save('php://output');
|
||||||
|
$spreadsheet->disconnectWorksheets();
|
||||||
|
}, '仓库名称映射表_' . now()->format('Ymd_His') . '.xlsx', [
|
||||||
|
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'Cache-Control' => 'private, no-store, no-cache, must-revalidate',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function inventoryWarehouseMappingRedirect()
|
||||||
|
{
|
||||||
|
return redirect()->route('business.pages.show', [
|
||||||
|
'company_id' => company_id() ?: 1,
|
||||||
|
'section' => 'product-inventory',
|
||||||
|
'page' => 'inventory-report',
|
||||||
|
'mapping' => 1,
|
||||||
|
'mapping_warehouse_provider' => request('mapping_warehouse_provider', ''),
|
||||||
|
'mapping_warehouse' => request('mapping_warehouse', ''),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return array<string,string> */
|
/** @return array<string,string> */
|
||||||
private function inventoryReportFilters(Request $request): array
|
private function inventoryReportFilters(Request $request): array
|
||||||
{
|
{
|
||||||
@ -1345,7 +1524,7 @@ class Pages extends Controller
|
|||||||
|
|
||||||
if ($scope === 'all' && should_queue()) {
|
if ($scope === 'all' && should_queue()) {
|
||||||
$fileName = Str::filename('进销存报表') . '-' . now()->format('Ymd_His') . '-' . Str::random(8) . '.csv';
|
$fileName = Str::filename('进销存报表') . '-' . now()->format('Ymd_His') . '-' . Str::random(8) . '.csv';
|
||||||
dispatch(new InventoryReportExport(user(), $filters, $columns, $fileName))->onQueue('exports');
|
dispatch(new InventoryReportExport(user(), $filters, $columns, $fileName, (int) (company_id() ?: 1)))->onQueue('exports');
|
||||||
flash('进销存报表已提交后台生成,完成后可从通知中心下载。')->success();
|
flash('进销存报表已提交后台生成,完成后可从通知中心下载。')->success();
|
||||||
|
|
||||||
return back();
|
return back();
|
||||||
@ -1358,7 +1537,7 @@ class Pages extends Controller
|
|||||||
// download connection. The job stores the completed file and
|
// download connection. The job stores the completed file and
|
||||||
// raises the existing notification when it is ready.
|
// raises the existing notification when it is ready.
|
||||||
$fileName = Str::filename('进销存报表') . '-' . now()->format('Ymd_His') . '-' . Str::random(8) . '.csv';
|
$fileName = Str::filename('进销存报表') . '-' . now()->format('Ymd_His') . '-' . Str::random(8) . '.csv';
|
||||||
dispatch(new InventoryReportExport(user(), $filters, $columns, $fileName))->afterResponse();
|
dispatch(new InventoryReportExport(user(), $filters, $columns, $fileName, (int) (company_id() ?: 1)))->afterResponse();
|
||||||
flash('进销存报表已提交后台生成,完成后可从通知中心下载。')->success();
|
flash('进销存报表已提交后台生成,完成后可从通知中心下载。')->success();
|
||||||
|
|
||||||
return back();
|
return back();
|
||||||
@ -1386,8 +1565,11 @@ class Pages extends Controller
|
|||||||
$rows = $scope === 'page'
|
$rows = $scope === 'page'
|
||||||
? $this->thirdPartyWarehouseStorage->paginateMonthlyInventoryReport($filters, $page, $perPage, false)['data']
|
? $this->thirdPartyWarehouseStorage->paginateMonthlyInventoryReport($filters, $page, $perPage, false)['data']
|
||||||
: $this->thirdPartyWarehouseStorage->exportMonthlyInventoryReportRows($filters);
|
: $this->thirdPartyWarehouseStorage->exportMonthlyInventoryReportRows($filters);
|
||||||
|
$shortNames = app(InventoryWarehouseShortNames::class);
|
||||||
|
$names = $shortNames->forCompany((int) (company_id() ?: 1));
|
||||||
$written = 0;
|
$written = 0;
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
|
$row = $shortNames->fill($row, $names);
|
||||||
fputcsv($csv, array_map(function (array $column) use ($row): mixed {
|
fputcsv($csv, array_map(function (array $column) use ($row): mixed {
|
||||||
$value = data_get($row, $column['name']);
|
$value = data_get($row, $column['name']);
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Jobs\Common;
|
namespace App\Jobs\Common;
|
||||||
|
|
||||||
use App\Abstracts\JobShouldQueue;
|
use App\Abstracts\JobShouldQueue;
|
||||||
|
use App\Business\InventoryWarehouseShortNames;
|
||||||
use App\Notifications\Common\ExportFailed;
|
use App\Notifications\Common\ExportFailed;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Modules\EcommerceFinance\Repositories\ThirdPartyWarehouseStorageMirrorRepository;
|
use Modules\EcommerceFinance\Repositories\ThirdPartyWarehouseStorageMirrorRepository;
|
||||||
@ -15,16 +16,20 @@ class InventoryReportExport extends JobShouldQueue
|
|||||||
|
|
||||||
public int $tries = 1;
|
public int $tries = 1;
|
||||||
|
|
||||||
|
protected int $company_id = 1;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected $user,
|
protected $user,
|
||||||
protected array $filters,
|
protected array $filters,
|
||||||
protected array $columns,
|
protected array $columns,
|
||||||
protected string $file_name,
|
protected string $file_name,
|
||||||
|
?int $companyId = null,
|
||||||
) {
|
) {
|
||||||
|
$this->company_id = $companyId ?? 1;
|
||||||
$this->onQueue('exports');
|
$this->onQueue('exports');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(ThirdPartyWarehouseStorageMirrorRepository $repository): void
|
public function handle(ThirdPartyWarehouseStorageMirrorRepository $repository, InventoryWarehouseShortNames $shortNames): void
|
||||||
{
|
{
|
||||||
$path = storage_path('app/temp/' . $this->file_name);
|
$path = storage_path('app/temp/' . $this->file_name);
|
||||||
File::ensureDirectoryExists(dirname($path));
|
File::ensureDirectoryExists(dirname($path));
|
||||||
@ -37,7 +42,9 @@ class InventoryReportExport extends JobShouldQueue
|
|||||||
|
|
||||||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||||
fputcsv($csv, array_column($this->columns, 'label'));
|
fputcsv($csv, array_column($this->columns, 'label'));
|
||||||
|
$names = $shortNames->forCompany($this->company_id);
|
||||||
foreach ($repository->exportMonthlyInventoryReportRows($this->filters) as $row) {
|
foreach ($repository->exportMonthlyInventoryReportRows($this->filters) as $row) {
|
||||||
|
$row = $shortNames->fill($row, $names);
|
||||||
fputcsv($csv, array_map(function (array $column) use ($row): mixed {
|
fputcsv($csv, array_map(function (array $column) use ($row): mixed {
|
||||||
$value = data_get($row, $column['name']);
|
$value = data_get($row, $column['name']);
|
||||||
|
|
||||||
|
|||||||
34
app/Models/Business/InventoryWarehouseNameMapping.php
Normal file
34
app/Models/Business/InventoryWarehouseNameMapping.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Business;
|
||||||
|
|
||||||
|
use App\Abstracts\Model;
|
||||||
|
|
||||||
|
class InventoryWarehouseNameMapping extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'business_inventory_warehouse_name_mappings';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'company_id',
|
||||||
|
'warehouse_provider',
|
||||||
|
'warehouse',
|
||||||
|
'warehouse_short_name',
|
||||||
|
'created_by',
|
||||||
|
'updated_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'company_id' => 'integer',
|
||||||
|
'created_by' => 'integer',
|
||||||
|
'updated_by' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function columnLabels(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'warehouse_provider' => '仓储方',
|
||||||
|
'warehouse' => '仓库',
|
||||||
|
'warehouse_short_name' => '仓库简称',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -335,6 +335,24 @@ $navigation = [
|
|||||||
'pages' => [
|
'pages' => [
|
||||||
['slug' => 'normal-products', 'title' => '普通商品资料'],
|
['slug' => 'normal-products', 'title' => '普通商品资料'],
|
||||||
['slug' => 'online-daifa-price', 'title' => '线上代发价格'],
|
['slug' => 'online-daifa-price', 'title' => '线上代发价格'],
|
||||||
|
[
|
||||||
|
'slug' => 'monthly-weighted-average-price',
|
||||||
|
'title' => '商品月加权平均单价',
|
||||||
|
'directory' => [
|
||||||
|
'icon' => 'calculate',
|
||||||
|
'status' => '取数规则待确认',
|
||||||
|
'description' => '商品月加权平均单价页面入口已建立;接入数据前需确认采购入库范围、金额字段和退货处理口径。',
|
||||||
|
'empty' => '商品月加权平均单价尚未同步。确认取数规则后再接入经营分析副本库数据。',
|
||||||
|
'fields' => [
|
||||||
|
['name' => 'month', 'label' => '月份', 'kind' => 'date'],
|
||||||
|
['name' => 'product_code', 'label' => '商品编码'],
|
||||||
|
['name' => 'product_name', 'label' => '商品名称'],
|
||||||
|
['name' => 'purchase_quantity', 'label' => '采购数量', 'kind' => 'number'],
|
||||||
|
['name' => 'purchase_amount', 'label' => '采购金额', 'kind' => 'number'],
|
||||||
|
['name' => 'weighted_average_unit_price', 'label' => '月加权平均单价', 'kind' => 'number'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'slug' => 'inventory-report',
|
'slug' => 'inventory-report',
|
||||||
'title' => '进销存报表',
|
'title' => '进销存报表',
|
||||||
@ -348,6 +366,7 @@ $navigation = [
|
|||||||
['name' => 'month', 'label' => '月份', 'kind' => 'date'],
|
['name' => 'month', 'label' => '月份', 'kind' => 'date'],
|
||||||
['name' => 'warehouse_provider', 'label' => '仓储方'],
|
['name' => 'warehouse_provider', 'label' => '仓储方'],
|
||||||
['name' => 'warehouse', 'label' => '仓库'],
|
['name' => 'warehouse', 'label' => '仓库'],
|
||||||
|
['name' => 'warehouse_short_name', 'label' => '仓库简称'],
|
||||||
['name' => 'product_code', 'label' => '商品编码'],
|
['name' => 'product_code', 'label' => '商品编码'],
|
||||||
['name' => 'category', 'label' => '分类'],
|
['name' => 'category', 'label' => '分类'],
|
||||||
['name' => 'opening_quantity', 'label' => '期初数量', 'kind' => 'integer'],
|
['name' => 'opening_quantity', 'label' => '期初数量', 'kind' => 'integer'],
|
||||||
|
|||||||
@ -33,4 +33,20 @@ return [
|
|||||||
realpath(storage_path('framework/views'))
|
realpath(storage_path('framework/views'))
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Compiled View Cache
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Blade still needs a writable path to generate PHP for each request, but
|
||||||
|
| local development must not trust an existing compiled template. Setting
|
||||||
|
| the compiler cache flag to false makes Laravel recompile every view and
|
||||||
|
| leaves production's normal compiled-view behavior unchanged.
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cache' => env('APP_ENV', 'production') === 'local'
|
||||||
|
? false
|
||||||
|
: env('VIEW_CACHE', true),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('business_inventory_warehouse_name_mappings')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::create('business_inventory_warehouse_name_mappings', function (Blueprint $table): void {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->unsignedInteger('company_id')->default(1);
|
||||||
|
$table->string('warehouse_provider', 191)->comment('仓储方');
|
||||||
|
$table->string('warehouse', 191)->comment('仓库');
|
||||||
|
$table->string('warehouse_short_name', 191)->comment('仓库简称');
|
||||||
|
$table->unsignedInteger('created_by')->nullable();
|
||||||
|
$table->unsignedInteger('updated_by')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
$table->unique(
|
||||||
|
['company_id', 'warehouse_provider', 'warehouse'],
|
||||||
|
'inventory_warehouse_mapping_identity',
|
||||||
|
);
|
||||||
|
$table->index(
|
||||||
|
['company_id', 'warehouse_short_name'],
|
||||||
|
'inventory_warehouse_mapping_short_name_index',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('business_inventory_warehouse_name_mappings');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -37,6 +37,11 @@ run_artisan_as_web() {
|
|||||||
runuser -u www-data -- php artisan "$@"
|
runuser -u www-data -- php artisan "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app_environment=""
|
||||||
|
if [ -f .env ]; then
|
||||||
|
app_environment="$(sed -n 's/^APP_ENV=//p' .env | tail -n 1 | tr -d '\r' | sed 's/^"//; s/"$//; s/^'\''//; s/'\''$//')"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "${1:-}" = "apache2-foreground" ]; then
|
if [ "${1:-}" = "apache2-foreground" ]; then
|
||||||
sed -ri 's/^Listen 80$/Listen 18088/' /etc/apache2/ports.conf
|
sed -ri 's/^Listen 80$/Listen 18088/' /etc/apache2/ports.conf
|
||||||
assert_web_writable
|
assert_web_writable
|
||||||
@ -53,8 +58,17 @@ if [ "${1:-}" = "apache2-foreground" ]; then
|
|||||||
run_artisan_as_web migrate --force
|
run_artisan_as_web migrate --force
|
||||||
fi
|
fi
|
||||||
|
|
||||||
run_artisan_as_web config:cache
|
if [ "$app_environment" = "local" ]; then
|
||||||
run_artisan_as_web view:cache
|
# Keep development source live: optimize:clear above removes any
|
||||||
|
# stale bootstrap/view artifacts, while view.cache=false makes
|
||||||
|
# Blade recompile templates on every request without a restart.
|
||||||
|
run_artisan_as_web optimize:clear
|
||||||
|
else
|
||||||
|
# Production keeps the existing immutable config and compiled
|
||||||
|
# Blade caches for startup and request performance.
|
||||||
|
run_artisan_as_web config:cache
|
||||||
|
run_artisan_as_web view:cache
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@ -81,6 +81,7 @@ calculate_local_image_fingerprint() {
|
|||||||
deploy/Dockerfile.local \
|
deploy/Dockerfile.local \
|
||||||
deploy/apache-vhost.conf \
|
deploy/apache-vhost.conf \
|
||||||
deploy/php.ini \
|
deploy/php.ini \
|
||||||
|
deploy/php-local.ini \
|
||||||
deploy/entrypoint.sh \
|
deploy/entrypoint.sh \
|
||||||
deploy/local-healthcheck.sh
|
deploy/local-healthcheck.sh
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
; Local bind-mounted PHP source must be revalidated on every request.
|
; Local development serves bind-mounted PHP/Blade source directly. Disable
|
||||||
|
; opcode caching so a browser refresh always executes the current source.
|
||||||
|
opcache.enable=0
|
||||||
|
opcache.enable_cli=0
|
||||||
opcache.validate_timestamps=1
|
opcache.validate_timestamps=1
|
||||||
opcache.revalidate_freq=0
|
opcache.revalidate_freq=0
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
@php
|
@php
|
||||||
$directoryRows = $rows ?? collect();
|
$directoryRows = $rows ?? collect();
|
||||||
$directoryFields = $directory['fields'] ?? [];
|
$directoryFields = $directory['fields'] ?? [];
|
||||||
|
$isInventoryReportPage = ($page['slug'] ?? '') === 'inventory-report';
|
||||||
|
$inventoryWarehouseMappings = $isInventoryReportPage
|
||||||
|
? ($inventoryWarehouseMappings ?? collect())
|
||||||
|
: collect();
|
||||||
|
$warehouseMappingPreview = $isInventoryReportPage ? session('warehouse_mapping_preview') : null;
|
||||||
// “最近三个月销量”必须是三个独立的月份列;月份按当前月向前取三个完整月份。
|
// “最近三个月销量”必须是三个独立的月份列;月份按当前月向前取三个完整月份。
|
||||||
// 保留原字段名作为兼容契约,渲染时展开为 *_1、*_2、*_3 三个值。
|
// 保留原字段名作为兼容契约,渲染时展开为 *_1、*_2、*_3 三个值。
|
||||||
$directoryFields = collect($directoryFields)->flatMap(function (array $field): array {
|
$directoryFields = collect($directoryFields)->flatMap(function (array $field): array {
|
||||||
@ -485,6 +490,8 @@
|
|||||||
.directory-sort-link:hover { color: var(--vip-content-accent-dark, #856632); }
|
.directory-sort-link:hover { color: var(--vip-content-accent-dark, #856632); }
|
||||||
.directory-sort-icon { width: 1rem; height: 1rem; color: var(--vip-content-muted, #595959); font-size: 1rem; line-height: 1; }
|
.directory-sort-icon { width: 1rem; height: 1rem; color: var(--vip-content-muted, #595959); font-size: 1rem; line-height: 1; }
|
||||||
.directory-sort-link[aria-current="true"] .directory-sort-icon { color: var(--vip-content-accent-dark, #856632); }
|
.directory-sort-link[aria-current="true"] .directory-sort-icon { color: var(--vip-content-accent-dark, #856632); }
|
||||||
|
.directory-inventory-sort { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; }
|
||||||
|
.directory-inventory-sort:hover, .directory-inventory-sort:focus-visible { color: var(--vip-content-accent-dark, #856632); }
|
||||||
.directory-header-menu { position: relative; display: inline-flex; align-items: center; gap: 0.15rem; }
|
.directory-header-menu { position: relative; display: inline-flex; align-items: center; gap: 0.15rem; }
|
||||||
.directory-header-menu summary { display: inline-flex; align-items: center; cursor: pointer; list-style: none; }
|
.directory-header-menu summary { display: inline-flex; align-items: center; cursor: pointer; list-style: none; }
|
||||||
.directory-header-menu summary::-webkit-details-marker { display: none; }
|
.directory-header-menu summary::-webkit-details-marker { display: none; }
|
||||||
@ -565,6 +572,256 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inventory-report-tabs {
|
||||||
|
min-height: 5rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 2rem;
|
||||||
|
gap: 2rem;
|
||||||
|
border-color: var(--vip-border, #d8d2c7);
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-report-tabs [data-inventory-report-tab] {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 5rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vip-content-muted, #595959);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-report-tabs [data-inventory-report-tab]:hover,
|
||||||
|
.inventory-report-tabs [data-inventory-report-tab][aria-selected="true"] {
|
||||||
|
color: var(--vip-content-accent-dark, #856632);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-report-tabs [data-inventory-report-tab][aria-selected="true"]::after {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 999px 999px 0 0;
|
||||||
|
background: var(--vip-content-accent, #b9954f);
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-report-tabs [data-inventory-report-tab]:focus-visible {
|
||||||
|
outline: 2px solid var(--vip-content-accent-dark, #856632);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-report-panel[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__table {
|
||||||
|
min-width: 52rem;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__table thead th {
|
||||||
|
position: sticky !important;
|
||||||
|
top: 0 !important;
|
||||||
|
z-index: 20 !important;
|
||||||
|
background: var(--vip-content-accent-soft, #f7f0de);
|
||||||
|
box-shadow: 0 1px 0 var(--vip-border, #d8d2c7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__table thead,
|
||||||
|
.inventory-warehouse-mapping__table thead tr {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__table thead th[data-directory-fixed="start"],
|
||||||
|
.inventory-warehouse-mapping__table thead th[data-directory-fixed="end"] {
|
||||||
|
z-index: 21 !important;
|
||||||
|
background: var(--vip-content-accent-soft, #f7f0de);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__table tbody td {
|
||||||
|
position: relative !important;
|
||||||
|
z-index: 0 !important;
|
||||||
|
background: var(--vip-surface, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__table tbody tr:hover td {
|
||||||
|
background: #fbf8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__empty {
|
||||||
|
padding: 3rem 1.25rem;
|
||||||
|
color: var(--vip-content-muted, #595959);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__filters {
|
||||||
|
grid-template-columns: repeat(2, minmax(12rem, 1fr)) auto;
|
||||||
|
align-items: end;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__filters .directory-query-actions {
|
||||||
|
grid-column: auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__dialog input {
|
||||||
|
min-height: 2.25rem;
|
||||||
|
padding: 0.45rem 0.65rem;
|
||||||
|
border: 1px solid var(--vip-border, #d8d2c7);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--vip-content-ink, #171717);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__filters .biz-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
height: 2.1rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__import {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__export,
|
||||||
|
.inventory-warehouse-mapping__file-label,
|
||||||
|
.inventory-warehouse-mapping__import button {
|
||||||
|
display: inline-flex;
|
||||||
|
height: 2.25rem;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0 0.875rem;
|
||||||
|
border: 1px solid var(--vip-content-accent, #b9954f);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--vip-content-accent-dark, #856632);
|
||||||
|
font: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__file-label input[type="file"] {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__actions button {
|
||||||
|
padding: 0.25rem 0.45rem;
|
||||||
|
border: 1px solid var(--vip-border, #d8d2c7);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--vip-content-accent-dark, #856632);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__preview {
|
||||||
|
margin: 1rem 1.25rem;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid var(--vip-border, #d8d2c7);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__preview summary {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--vip-content-accent-dark, #856632);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__preview table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__preview th,
|
||||||
|
.inventory-warehouse-mapping__preview td {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-top: 1px solid var(--vip-border, #d8d2c7);
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__preview .is-invalid,
|
||||||
|
.inventory-warehouse-mapping__preview .is-conflict {
|
||||||
|
color: #a33a32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__preview .is-same {
|
||||||
|
color: var(--vip-content-muted, #595959);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__dialog {
|
||||||
|
width: min(32rem, calc(100vw - 2rem));
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--vip-border, #d8d2c7);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__dialog::backdrop {
|
||||||
|
background: rgba(21, 18, 12, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__dialog form {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__dialog h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--vip-content-ink, #171717);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__dialog label {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
color: var(--vip-content-muted, #595959);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-warehouse-mapping__dialog__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.directory-table-head {
|
.directory-table-head {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -610,18 +867,40 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<header class="directory-table-head">
|
@if ($isInventoryReportPage)
|
||||||
<div class="directory-table-title">
|
<nav class="business-tab-bar inventory-report-tabs" data-inventory-report-tabs role="tablist" aria-label="进销存报表数据表">
|
||||||
<span class="material-icons-outlined" aria-hidden="true">{{ $directory['icon'] ?? 'table_view' }}</span>
|
<button type="button" role="tab" aria-selected="true" aria-controls="inventory-report-total-panel" id="inventory-report-total-tab" data-inventory-report-tab="total">
|
||||||
<div>
|
进销存总表
|
||||||
<strong>{{ $page['title'] ?? $section['title'] }} · 字段结构</strong>
|
</button>
|
||||||
@if (!empty($directory['description']))
|
<button type="button" role="tab" aria-selected="false" aria-controls="inventory-report-mapping-panel" id="inventory-report-mapping-tab" data-inventory-report-tab="mapping" tabindex="-1">
|
||||||
<p class="directory-table-description">{{ $directory['description'] }}</p>
|
仓库名称映射表
|
||||||
@endif
|
</button>
|
||||||
|
</nav>
|
||||||
|
<section
|
||||||
|
id="inventory-report-total-panel"
|
||||||
|
class="inventory-report-panel"
|
||||||
|
data-inventory-report-panel="total"
|
||||||
|
data-directory-table-scope
|
||||||
|
data-directory-table-key="total"
|
||||||
|
data-directory-last-page="{{ $directoryLastPage }}"
|
||||||
|
data-directory-source-url="{{ request()->fullUrlWithQuery(['page' => 1]) }}"
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby="inventory-report-total-tab"
|
||||||
|
>
|
||||||
|
@else
|
||||||
|
<header class="directory-table-head">
|
||||||
|
<div class="directory-table-title">
|
||||||
|
<span class="material-icons-outlined" aria-hidden="true">{{ $directory['icon'] ?? 'table_view' }}</span>
|
||||||
|
<div>
|
||||||
|
<strong>{{ $page['title'] ?? $section['title'] }} · 字段结构</strong>
|
||||||
|
@if (!empty($directory['description']))
|
||||||
|
<p class="directory-table-description">{{ $directory['description'] }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span class="directory-table-status" data-directory-status>{{ $directory['status'] ?? '尚未同步' }}</span>
|
||||||
<span class="directory-table-status" data-directory-status>{{ $directory['status'] ?? '尚未同步' }}</span>
|
</header>
|
||||||
</header>
|
@endif
|
||||||
|
|
||||||
@if (!empty($upstreamError))
|
@if (!empty($upstreamError))
|
||||||
<div class="directory-table-notice" role="alert">
|
<div class="directory-table-notice" role="alert">
|
||||||
@ -1008,12 +1287,17 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div class="directory-table-scroll">
|
<div class="directory-table-scroll">
|
||||||
<table class="directory-table" @if($directoryUsesFieldQuery) data-columns-ready="true" @endif>
|
<table
|
||||||
|
id="{{ $isInventoryReportPage ? 'inventory-report-total-table' : 'directory-table-' . ($page['slug'] ?? 'page') }}"
|
||||||
|
class="directory-table"
|
||||||
|
@if($isInventoryReportPage) data-directory-table-key="total" @endif
|
||||||
|
@if($directoryUsesFieldQuery) data-columns-ready="true" @endif
|
||||||
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">#</th>
|
<th scope="col" data-directory-fixed="start">#</th>
|
||||||
@foreach ($directoryFields as $field)
|
@foreach ($directoryFields as $field)
|
||||||
<th scope="col" data-column-key="{{ $field['name'] }}" class="{{ in_array($field['kind'] ?? null, ['number', 'integer'], true) ? 'is-number' : '' }} {{ !in_array($field['name'], $visibleDirectoryColumns, true) ? 'is-column-hidden' : '' }}">
|
<th scope="col" data-column-key="{{ $field['name'] }}" @if($isInventoryReportPage && $field['name'] === 'warehouse_short_name') aria-sort="none" @endif class="{{ in_array($field['kind'] ?? null, ['number', 'integer'], true) ? 'is-number' : '' }} {{ !in_array($field['name'], $visibleDirectoryColumns, true) ? 'is-column-hidden' : '' }}">
|
||||||
@if ($directoryUsesFieldQuery)
|
@if ($directoryUsesFieldQuery)
|
||||||
@php
|
@php
|
||||||
$isSorted = request('sort') === $field['name'];
|
$isSorted = request('sort') === $field['name'];
|
||||||
@ -1031,6 +1315,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
@elseif ($isInventoryReportPage && $field['name'] === 'warehouse_short_name')
|
||||||
|
<button type="button" class="directory-inventory-sort" data-inventory-short-name-sort aria-label="仓库简称按当前页升序排序" title="当前页排序">{{ $field['label'] }}<span class="material-icons-outlined directory-sort-icon" aria-hidden="true">unfold_more</span></button>
|
||||||
@else
|
@else
|
||||||
{{ $field['label'] }}
|
{{ $field['label'] }}
|
||||||
@endif
|
@endif
|
||||||
@ -1041,7 +1327,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@forelse ($directoryRows as $rowIndex => $row)
|
@forelse ($directoryRows as $rowIndex => $row)
|
||||||
<tr @if (($page['slug'] ?? '') === 'subject-operating-report') data-subject-row data-subject-details='@json($row->company_details ?? [])' tabindex="0" aria-label="查看主体详细资料" @endif>
|
<tr @if (($page['slug'] ?? '') === 'subject-operating-report') data-subject-row data-subject-details='@json($row->company_details ?? [])' tabindex="0" aria-label="查看主体详细资料" @endif>
|
||||||
<td>{{ $rowIndex + 1 + (($directoryCurrentPage - 1) * $directoryPerPage) }}</td>
|
<td data-directory-fixed="start">{{ $rowIndex + 1 + (($directoryCurrentPage - 1) * $directoryPerPage) }}</td>
|
||||||
@foreach ($directoryFields as $field)
|
@foreach ($directoryFields as $field)
|
||||||
@php
|
@php
|
||||||
$source = array_key_exists('source', $field) ? $field['source'] : $field['name'];
|
$source = array_key_exists('source', $field) ? $field['source'] : $field['name'];
|
||||||
@ -1080,7 +1366,7 @@
|
|||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
{{ filled($display) ? $display : '—' }}
|
{{ filled($display) ? $display : ($isInventoryReportPage && $field['name'] === 'warehouse_short_name' ? '' : '—') }}
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
@endforeach
|
@endforeach
|
||||||
@ -1118,6 +1404,164 @@
|
|||||||
</footer>
|
</footer>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@if ($isInventoryReportPage)
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
id="inventory-report-mapping-panel"
|
||||||
|
class="inventory-report-panel"
|
||||||
|
data-inventory-report-panel="mapping"
|
||||||
|
data-directory-table-scope
|
||||||
|
data-directory-table-key="warehouse-mapping"
|
||||||
|
data-directory-last-page="{{ $inventoryWarehouseMappings->lastPage() }}"
|
||||||
|
data-directory-source-url="{{ request()->fullUrlWithQuery(['mapping' => 1, 'page' => 1]) }}"
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby="inventory-report-mapping-tab"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<div class="inventory-warehouse-mapping">
|
||||||
|
<form method="GET" action="{{ request()->url() }}" class="directory-query-bar inventory-warehouse-mapping__filters" aria-label="仓库名称映射查询">
|
||||||
|
<input type="hidden" name="mapping" value="1">
|
||||||
|
<label class="directory-query-field">
|
||||||
|
<span>仓储方</span>
|
||||||
|
<input type="search" name="mapping_warehouse_provider" value="{{ request('mapping_warehouse_provider') }}" placeholder="按仓储方包含查询">
|
||||||
|
</label>
|
||||||
|
<label class="directory-query-field">
|
||||||
|
<span>仓库名</span>
|
||||||
|
<input type="search" name="mapping_warehouse" value="{{ request('mapping_warehouse') }}" placeholder="按仓库名包含查询">
|
||||||
|
</label>
|
||||||
|
<div class="directory-query-actions">
|
||||||
|
<button type="submit" class="is-primary">查询</button>
|
||||||
|
<a href="{{ request()->url() }}?mapping=1" class="biz-btn biz-btn-ghost">清除</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="directory-table-tools" data-directory-tools>
|
||||||
|
<label class="sr-only" for="directory-search-inventory-warehouse-mapping">搜索当前表格</label>
|
||||||
|
<input id="directory-search-inventory-warehouse-mapping" type="search" placeholder="搜索当前页数据..." data-directory-search>
|
||||||
|
<div class="directory-field-picker" data-directory-field-picker>
|
||||||
|
<button type="button" aria-expanded="false" aria-controls="directory-fields-inventory-warehouse-mapping" data-directory-field-toggle>列设置</button>
|
||||||
|
<div class="directory-field-picker__panel" id="directory-fields-inventory-warehouse-mapping" role="group" aria-label="选择展示字段" hidden>
|
||||||
|
<span class="directory-field-picker__hint">可隐藏业务字段并拖动调整顺序;序号和操作列固定。</span>
|
||||||
|
@foreach ([
|
||||||
|
['name' => 'warehouse_provider', 'label' => '仓储方'],
|
||||||
|
['name' => 'warehouse', 'label' => '仓库'],
|
||||||
|
['name' => 'warehouse_short_name', 'label' => '仓库简称'],
|
||||||
|
] as $mappingField)
|
||||||
|
<label class="directory-field-picker__option" draggable="true" data-directory-field-option>
|
||||||
|
<input type="checkbox" value="{{ $mappingField['name'] }}" data-directory-field-check checked>
|
||||||
|
<span>{{ $mappingField['label'] }}</span>
|
||||||
|
</label>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="directory-export-group" aria-label="导出范围">
|
||||||
|
<button type="button" data-directory-export="visible">当前页·显示列</button>
|
||||||
|
<button type="button" data-directory-export="all-fields">当前页·全部字段</button>
|
||||||
|
<button type="button" data-directory-export-all="visible">全部筛选结果·显示列</button>
|
||||||
|
<button type="button" data-directory-export-all="all-fields">全部筛选结果·全部字段</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" data-directory-reset>恢复默认</button>
|
||||||
|
<button type="button" data-inventory-mapping-template>下载导入模板</button>
|
||||||
|
<button type="button" class="is-primary" data-inventory-mapping-create>新增仓库</button>
|
||||||
|
<a class="inventory-warehouse-mapping__export" href="{{ route('business.product-inventory.inventory-report.warehouse-mappings.export', request()->only(['mapping_warehouse_provider', 'mapping_warehouse'])) }}">导出完整表格(Excel)</a>
|
||||||
|
<form method="POST" enctype="multipart/form-data" action="{{ route('business.product-inventory.inventory-report.warehouse-mappings.import.preview') }}" class="inventory-warehouse-mapping__import">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="mapping_warehouse_provider" value="{{ request('mapping_warehouse_provider') }}">
|
||||||
|
<input type="hidden" name="mapping_warehouse" value="{{ request('mapping_warehouse') }}">
|
||||||
|
<label class="inventory-warehouse-mapping__file-label" for="inventory-warehouse-mapping-file">
|
||||||
|
<span>选择文件</span>
|
||||||
|
<input id="inventory-warehouse-mapping-file" type="file" name="import" accept=".xlsx,.xls,.csv" required data-inventory-mapping-import-file>
|
||||||
|
</label>
|
||||||
|
<button type="submit" class="inventory-warehouse-mapping__import-button">导入并预览</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@if (is_array($warehouseMappingPreview))
|
||||||
|
@php($previewSummary = $warehouseMappingPreview['summary'] ?? [])
|
||||||
|
<details class="inventory-warehouse-mapping__preview" open>
|
||||||
|
<summary>导入预览:新增 {{ $previewSummary['created'] ?? 0 }} 条,重复 {{ $previewSummary['same'] ?? 0 }} 条,冲突 {{ $previewSummary['conflicts'] ?? 0 }} 条,失败 {{ $previewSummary['invalid'] ?? 0 }} 条</summary>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>行号</th><th>仓储方</th><th>仓库</th><th>仓库简称</th><th>结果</th><th>原因</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (($warehouseMappingPreview['rows'] ?? []) as $previewRow)
|
||||||
|
<tr class="is-{{ $previewRow['status'] }}">
|
||||||
|
<td>{{ $previewRow['row_number'] }}</td>
|
||||||
|
<td>{{ $previewRow['warehouse_provider'] ?: '—' }}</td>
|
||||||
|
<td>{{ $previewRow['warehouse'] ?: '—' }}</td>
|
||||||
|
<td>{{ $previewRow['warehouse_short_name'] ?: '—' }}</td>
|
||||||
|
<td>{{ match ($previewRow['status']) { 'new' => '待新增', 'same' => '跳过', 'conflict' => '冲突', default => '失败' } }}</td>
|
||||||
|
<td>{{ $previewRow['message'] }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
@if (($previewSummary['conflicts'] ?? 0) === 0 && ($previewSummary['invalid'] ?? 0) === 0)
|
||||||
|
<form method="POST" action="{{ route('business.product-inventory.inventory-report.warehouse-mappings.import.commit') }}" style="padding:0.75rem 1rem;">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="preview_token" value="{{ $warehouseMappingPreview['token'] ?? '' }}">
|
||||||
|
<button type="submit" class="is-primary">确认导入</button>
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
<p style="padding:0 1rem 0.75rem;margin:0;color:#a33a32;">存在失败或冲突行,系统不会写入任何记录。请修正 Excel 后重新预览。</p>
|
||||||
|
@endif
|
||||||
|
</details>
|
||||||
|
@endif
|
||||||
|
@if ($inventoryWarehouseMappings->isEmpty())
|
||||||
|
<div class="inventory-warehouse-mapping__empty">当前查询暂无仓库名称映射。可点击“新增仓库”或上传 Sheet1 导入。</div>
|
||||||
|
@else
|
||||||
|
<div class="directory-table-scroll">
|
||||||
|
<table id="inventory-warehouse-mapping-table" class="directory-table inventory-warehouse-mapping__table" data-directory-table-key="warehouse-mapping">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" data-directory-fixed="start">#</th>
|
||||||
|
<th scope="col" data-column-key="warehouse_provider">仓储方</th>
|
||||||
|
<th scope="col" data-column-key="warehouse">仓库</th>
|
||||||
|
<th scope="col" data-column-key="warehouse_short_name">仓库简称</th>
|
||||||
|
<th scope="col" data-directory-fixed="end">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($inventoryWarehouseMappings as $mapping)
|
||||||
|
<tr>
|
||||||
|
<td data-directory-fixed="start">{{ ($inventoryWarehouseMappings->currentPage() - 1) * $inventoryWarehouseMappings->perPage() + $loop->iteration }}</td>
|
||||||
|
<td data-column-key="warehouse_provider">{{ $mapping->warehouse_provider }}</td>
|
||||||
|
<td data-column-key="warehouse">{{ $mapping->warehouse }}</td>
|
||||||
|
<td data-column-key="warehouse_short_name">{{ $mapping->warehouse_short_name }}</td>
|
||||||
|
<td data-directory-fixed="end">
|
||||||
|
<span class="inventory-warehouse-mapping__actions">
|
||||||
|
<button type="button" data-inventory-mapping-edit data-id="{{ $mapping->id }}" data-provider="{{ $mapping->warehouse_provider }}" data-warehouse="{{ $mapping->warehouse }}" data-short-name="{{ $mapping->warehouse_short_name }}">修改</button>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<footer class="directory-table-pagination">
|
||||||
|
<span>共 {{ $inventoryWarehouseMappings->total() }} 条 · 第 {{ $inventoryWarehouseMappings->currentPage() }} / {{ $inventoryWarehouseMappings->lastPage() }} 页</span>
|
||||||
|
<span>
|
||||||
|
@if ($inventoryWarehouseMappings->onFirstPage()) <span>上一页</span> @else <a href="{{ $inventoryWarehouseMappings->previousPageUrl() }}&mapping=1">上一页</a> @endif
|
||||||
|
·
|
||||||
|
@if ($inventoryWarehouseMappings->hasMorePages()) <a href="{{ $inventoryWarehouseMappings->nextPageUrl() }}&mapping=1">下一页</a> @else <span>下一页</span> @endif
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<dialog class="inventory-warehouse-mapping__dialog" data-inventory-mapping-dialog>
|
||||||
|
<form method="POST" action="{{ route('business.product-inventory.inventory-report.warehouse-mappings.store') }}" data-inventory-mapping-form>
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="_method" value="POST" data-inventory-mapping-method>
|
||||||
|
<h3 data-inventory-mapping-dialog-title>新增仓库名称映射</h3>
|
||||||
|
<label>仓储方 <input name="warehouse_provider" required maxlength="191" data-inventory-mapping-provider></label>
|
||||||
|
<label>仓库 <input name="warehouse" required maxlength="191" data-inventory-mapping-warehouse></label>
|
||||||
|
<label>仓库简称 <input name="warehouse_short_name" required maxlength="191" data-inventory-mapping-short-name><small>输入仓储方和仓库后会自动预填,可按业务需要修改。</small></label>
|
||||||
|
<div class="inventory-warehouse-mapping__dialog__actions">
|
||||||
|
<button type="button" data-inventory-mapping-cancel>取消</button>
|
||||||
|
<button type="submit" class="is-primary">保存</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</section>
|
||||||
|
@endif
|
||||||
|
|
||||||
@if (($page['slug'] ?? '') === 'subject-operating-report')
|
@if (($page['slug'] ?? '') === 'subject-operating-report')
|
||||||
<section class="directory-subject-details" data-subject-details-panel hidden aria-live="polite">
|
<section class="directory-subject-details" data-subject-details-panel hidden aria-live="polite">
|
||||||
<header class="directory-subject-details__head">
|
<header class="directory-subject-details__head">
|
||||||
@ -1345,24 +1789,58 @@
|
|||||||
});
|
});
|
||||||
document.querySelectorAll('[data-directory-tools]').forEach((tools) => {
|
document.querySelectorAll('[data-directory-tools]').forEach((tools) => {
|
||||||
const section = tools.closest('[data-directory-page]');
|
const section = tools.closest('[data-directory-page]');
|
||||||
const table = section?.querySelector('.directory-table');
|
const tableScope = tools.closest('[data-directory-table-scope]');
|
||||||
|
const table = tableScope?.querySelector('.directory-table') || section?.querySelector('.directory-table');
|
||||||
const search = tools.querySelector('[data-directory-search]');
|
const search = tools.querySelector('[data-directory-search]');
|
||||||
if (!table || !search) return;
|
if (!table || !search) return;
|
||||||
|
table.addEventListener('click', (event) => {
|
||||||
|
const shortNameSort = event.target instanceof Element
|
||||||
|
? event.target.closest('[data-inventory-short-name-sort]')
|
||||||
|
: null;
|
||||||
|
if (!shortNameSort) return;
|
||||||
|
const header = shortNameSort.closest('th');
|
||||||
|
const ascending = header.getAttribute('aria-sort') !== 'ascending';
|
||||||
|
const rows = [...table.tBodies[0].querySelectorAll('tr:not(.is-empty)')];
|
||||||
|
rows.sort((left, right) => {
|
||||||
|
const a = left.querySelector('[data-column-key="warehouse_short_name"]')?.textContent.trim() || '';
|
||||||
|
const b = right.querySelector('[data-column-key="warehouse_short_name"]')?.textContent.trim() || '';
|
||||||
|
if (!a) return b ? 1 : 0;
|
||||||
|
if (!b) return -1;
|
||||||
|
return a.localeCompare(b, 'zh-Hans-CN') * (ascending ? 1 : -1);
|
||||||
|
}).forEach((row) => table.tBodies[0].appendChild(row));
|
||||||
|
header.setAttribute('aria-sort', ascending ? 'ascending' : 'descending');
|
||||||
|
shortNameSort.setAttribute('aria-label', `仓库简称按当前页${ascending ? '降序' : '升序'}排序`);
|
||||||
|
shortNameSort.querySelector('.directory-sort-icon').textContent = ascending ? 'arrow_upward' : 'arrow_downward';
|
||||||
|
});
|
||||||
const picker = tools.querySelector('[data-directory-field-picker]');
|
const picker = tools.querySelector('[data-directory-field-picker]');
|
||||||
const toggle = picker?.querySelector('[data-directory-field-toggle]');
|
const toggle = picker?.querySelector('[data-directory-field-toggle]');
|
||||||
const panel = picker?.querySelector('.directory-field-picker__panel');
|
const panel = picker?.querySelector('.directory-field-picker__panel');
|
||||||
const checks = [...(picker?.querySelectorAll('[data-directory-field-check]') || [])];
|
const checks = [...(picker?.querySelectorAll('[data-directory-field-check]') || [])];
|
||||||
|
const tableKey = tableScope?.dataset.directoryTableKey || section?.dataset.directoryTab || 'table';
|
||||||
|
const sourceUrl = tableScope?.dataset.directorySourceUrl || window.location.href;
|
||||||
|
const lastPage = Number(tableScope?.dataset.directoryLastPage || section?.dataset.directoryLastPage || 1);
|
||||||
const storageKey = [
|
const storageKey = [
|
||||||
'directory-fields',
|
'directory-fields',
|
||||||
section.dataset.directoryUser || 'guest',
|
section.dataset.directoryUser || 'guest',
|
||||||
section.dataset.directorySection || 'section',
|
section.dataset.directorySection || 'section',
|
||||||
section.dataset.directoryPage || 'page',
|
section.dataset.directoryPage || 'page',
|
||||||
section.dataset.directoryTab || 'tab',
|
tableKey,
|
||||||
].join(':');
|
].join(':');
|
||||||
const defaultOrder = checks.map((check) => check.value);
|
const defaultOrder = checks.map((check) => check.value);
|
||||||
const orderedChecks = () => [...(picker?.querySelectorAll('[data-directory-field-option]') || [])]
|
const orderedChecks = () => [...(picker?.querySelectorAll('[data-directory-field-option]') || [])]
|
||||||
.map((option) => option.querySelector('[data-directory-field-check]'))
|
.map((option) => option.querySelector('[data-directory-field-check]'))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
const orderedExportCells = (row, selected) => {
|
||||||
|
const cells = [...row.children];
|
||||||
|
const fixedStart = cells.filter((cell) => cell.dataset.directoryFixed === 'start');
|
||||||
|
const rank = new Map(orderedChecks().map((check, index) => [check.value, index]));
|
||||||
|
const keyed = cells
|
||||||
|
.filter((cell) => cell.dataset.columnKey && (!selected || selected.has(cell.dataset.columnKey)))
|
||||||
|
.sort((a, b) => (rank.get(a.dataset.columnKey) ?? 0) - (rank.get(b.dataset.columnKey) ?? 0));
|
||||||
|
const other = cells.filter((cell) => !cell.dataset.columnKey && !cell.dataset.directoryFixed);
|
||||||
|
|
||||||
|
return [...fixedStart, ...keyed, ...other];
|
||||||
|
};
|
||||||
const applyColumns = (save = true) => {
|
const applyColumns = (save = true) => {
|
||||||
const ordered = orderedChecks();
|
const ordered = orderedChecks();
|
||||||
const selected = new Set(ordered.filter((check) => check.checked).map((check) => check.value));
|
const selected = new Set(ordered.filter((check) => check.checked).map((check) => check.value));
|
||||||
@ -1371,13 +1849,21 @@
|
|||||||
table.querySelectorAll('[data-column-key]').forEach((cell) => {
|
table.querySelectorAll('[data-column-key]').forEach((cell) => {
|
||||||
cell.classList.toggle('is-column-hidden', !selected.has(cell.dataset.columnKey));
|
cell.classList.toggle('is-column-hidden', !selected.has(cell.dataset.columnKey));
|
||||||
});
|
});
|
||||||
|
const reorderRow = (row) => {
|
||||||
|
const cells = [...row.children];
|
||||||
|
const fixedStart = cells.filter((cell) => cell.dataset.directoryFixed === 'start');
|
||||||
|
const fixedEnd = cells.filter((cell) => cell.dataset.directoryFixed === 'end');
|
||||||
|
const keyed = cells
|
||||||
|
.filter((cell) => cell.dataset.columnKey)
|
||||||
|
.sort((a, b) => (rank.get(a.dataset.columnKey) ?? 0) - (rank.get(b.dataset.columnKey) ?? 0));
|
||||||
|
const other = cells.filter((cell) => !cell.dataset.columnKey && !cell.dataset.directoryFixed);
|
||||||
|
[...fixedStart, ...keyed, ...fixedEnd, ...other].forEach((cell) => row.appendChild(cell));
|
||||||
|
};
|
||||||
[...table.tHead.rows].forEach((row) => {
|
[...table.tHead.rows].forEach((row) => {
|
||||||
[...row.children].sort((a, b) => (rank.get(a.dataset.columnKey) ?? -1) - (rank.get(b.dataset.columnKey) ?? -1))
|
reorderRow(row);
|
||||||
.forEach((cell) => row.appendChild(cell));
|
|
||||||
});
|
});
|
||||||
table.tBodies[0]?.querySelectorAll('tr').forEach((row) => {
|
table.tBodies[0]?.querySelectorAll('tr').forEach((row) => {
|
||||||
[...row.children].slice(1).sort((a, b) => (rank.get(a.dataset.columnKey) ?? -1) - (rank.get(b.dataset.columnKey) ?? -1))
|
reorderRow(row);
|
||||||
.forEach((cell) => row.appendChild(cell));
|
|
||||||
});
|
});
|
||||||
if (save) localStorage.setItem(storageKey, JSON.stringify({ order: ordered.map((check) => check.value), selected: [...selected] }));
|
if (save) localStorage.setItem(storageKey, JSON.stringify({ order: ordered.map((check) => check.value), selected: [...selected] }));
|
||||||
};
|
};
|
||||||
@ -1385,6 +1871,16 @@
|
|||||||
const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
|
const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
|
||||||
const savedOrder = Array.isArray(saved) ? saved : saved?.order;
|
const savedOrder = Array.isArray(saved) ? saved : saved?.order;
|
||||||
const savedSelected = Array.isArray(saved) ? saved : saved?.selected;
|
const savedSelected = Array.isArray(saved) ? saved : saved?.selected;
|
||||||
|
if (table.id === 'inventory-report-total-table' && !localStorage.getItem(`${storageKey}:warehouse-short-name-v1`)) {
|
||||||
|
if (Array.isArray(savedOrder) && !savedOrder.includes('warehouse_short_name')) {
|
||||||
|
savedOrder.splice(savedOrder.indexOf('warehouse') + 1, 0, 'warehouse_short_name');
|
||||||
|
}
|
||||||
|
if (Array.isArray(savedSelected) && !savedSelected.includes('warehouse_short_name')) {
|
||||||
|
savedSelected.push('warehouse_short_name');
|
||||||
|
}
|
||||||
|
if (saved) localStorage.setItem(storageKey, JSON.stringify({ order: savedOrder, selected: savedSelected }));
|
||||||
|
localStorage.setItem(`${storageKey}:warehouse-short-name-v1`, '1');
|
||||||
|
}
|
||||||
if (Array.isArray(savedSelected)) checks.forEach((check) => { check.checked = savedSelected.includes(check.value); });
|
if (Array.isArray(savedSelected)) checks.forEach((check) => { check.checked = savedSelected.includes(check.value); });
|
||||||
if (Array.isArray(savedOrder)) savedOrder.forEach((key) => {
|
if (Array.isArray(savedOrder)) savedOrder.forEach((key) => {
|
||||||
const option = picker.querySelector(`[data-directory-field-option] input[value="${CSS.escape(key)}"]`)?.closest('[data-directory-field-option]');
|
const option = picker.querySelector(`[data-directory-field-option] input[value="${CSS.escape(key)}"]`)?.closest('[data-directory-field-option]');
|
||||||
@ -1449,8 +1945,7 @@
|
|||||||
tools.querySelectorAll('[data-directory-export]').forEach((button) => button.addEventListener('click', () => {
|
tools.querySelectorAll('[data-directory-export]').forEach((button) => button.addEventListener('click', () => {
|
||||||
const selected = button.dataset.directoryExport === 'all-fields' ? null : new Set(orderedChecks().filter((check) => check.checked).map((check) => check.value));
|
const selected = button.dataset.directoryExport === 'all-fields' ? null : new Set(orderedChecks().filter((check) => check.checked).map((check) => check.value));
|
||||||
const rows = [...table.querySelectorAll('tr')].filter((row) => !row.hidden);
|
const rows = [...table.querySelectorAll('tr')].filter((row) => !row.hidden);
|
||||||
const cellsForExport = (row) => [...row.children].filter((cell) => !cell.dataset.columnKey || !selected || selected.has(cell.dataset.columnKey));
|
const csv = rows.map((row) => orderedExportCells(row, selected).map((cell) => csvEscape((cell.querySelector('[data-password-status]') || cell).textContent.trim())).join(',')).join('\n');
|
||||||
const csv = rows.map((row) => cellsForExport(row).map((cell) => csvEscape((cell.querySelector('[data-password-status]') || cell).textContent.trim())).join(',')).join('\n');
|
|
||||||
const blob = new Blob([`\ufeff${csv}`], { type: 'text/csv;charset=utf-8' });
|
const blob = new Blob([`\ufeff${csv}`], { type: 'text/csv;charset=utf-8' });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = URL.createObjectURL(blob);
|
link.href = URL.createObjectURL(blob);
|
||||||
@ -1460,25 +1955,25 @@
|
|||||||
}));
|
}));
|
||||||
tools.querySelectorAll('[data-directory-export-all]').forEach((button) => button.addEventListener('click', async (event) => {
|
tools.querySelectorAll('[data-directory-export-all]').forEach((button) => button.addEventListener('click', async (event) => {
|
||||||
const button = event.currentTarget;
|
const button = event.currentTarget;
|
||||||
const lastPage = Number(section.dataset.directoryLastPage || 1);
|
|
||||||
const selected = button.dataset.directoryExportAll === 'all-fields' ? null : new Set(orderedChecks().filter((check) => check.checked).map((check) => check.value));
|
const selected = button.dataset.directoryExportAll === 'all-fields' ? null : new Set(orderedChecks().filter((check) => check.checked).map((check) => check.value));
|
||||||
const cellsForExport = (row) => [...row.children]
|
const header = orderedExportCells(table.tHead.rows[0], selected).map((cell) => csvEscape(cell.textContent.trim())).join(',');
|
||||||
.filter((cell) => !cell.dataset.columnKey || !selected || selected.has(cell.dataset.columnKey));
|
|
||||||
const header = cellsForExport(table.tHead.rows[0]).map((cell) => csvEscape(cell.textContent.trim())).join(',');
|
|
||||||
const lines = [];
|
const lines = [];
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
const originalLabel = button.textContent;
|
const originalLabel = button.textContent;
|
||||||
try {
|
try {
|
||||||
for (let page = 1; page <= lastPage; page += 1) {
|
for (let page = 1; page <= lastPage; page += 1) {
|
||||||
button.textContent = `导出中 ${page}/${lastPage}`;
|
button.textContent = '导出中 ' + page + '/' + lastPage;
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(sourceUrl, window.location.origin);
|
||||||
url.searchParams.set('page', String(page));
|
url.searchParams.set('page', String(page));
|
||||||
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||||
if (!response.ok) throw new Error(`第 ${page} 页读取失败`);
|
if (!response.ok) throw new Error(`第 ${page} 页读取失败`);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
||||||
parsed.querySelectorAll('.directory-table tbody tr:not(.is-empty)').forEach((row) => {
|
const parsedTable = table.id
|
||||||
lines.push(cellsForExport(row).map((cell) => csvEscape(cell.textContent.trim())).join(','));
|
? parsed.querySelector(`#${CSS.escape(table.id)}`)
|
||||||
|
: parsed.querySelector('.directory-table');
|
||||||
|
parsedTable?.querySelectorAll('tbody tr:not(.is-empty)').forEach((row) => {
|
||||||
|
lines.push(orderedExportCells(row, selected).map((cell) => csvEscape(cell.textContent.trim())).join(','));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const csv = [header, ...lines].join('\n');
|
const csv = [header, ...lines].join('\n');
|
||||||
@ -1504,6 +1999,103 @@
|
|||||||
link.click();
|
link.click();
|
||||||
URL.revokeObjectURL(link.href);
|
URL.revokeObjectURL(link.href);
|
||||||
});
|
});
|
||||||
|
tools.querySelector('[data-inventory-mapping-template]')?.addEventListener('click', () => {
|
||||||
|
const header = ['仓储方', '仓库', '仓库简称'].map(csvEscape).join(',');
|
||||||
|
const blob = new Blob(['\ufeff' + header + '\n'], { type: 'text/csv;charset=utf-8' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = '仓库名称映射表-导入模板.csv';
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(link.href);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-inventory-report-tabs]').forEach((tabs) => {
|
||||||
|
const report = tabs.closest('[data-directory-page]');
|
||||||
|
const buttons = [...tabs.querySelectorAll('[data-inventory-report-tab]')];
|
||||||
|
const panels = [...(report?.querySelectorAll('[data-inventory-report-panel]') || [])];
|
||||||
|
if (!report || !buttons.length || !panels.length) return;
|
||||||
|
const select = (name, focus = false) => {
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
const selected = button.dataset.inventoryReportTab === name;
|
||||||
|
button.setAttribute('aria-selected', String(selected));
|
||||||
|
button.tabIndex = selected ? 0 : -1;
|
||||||
|
if (focus && selected) button.focus();
|
||||||
|
});
|
||||||
|
panels.forEach((panel) => {
|
||||||
|
panel.hidden = panel.dataset.inventoryReportPanel !== name;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
buttons.forEach((button, index) => {
|
||||||
|
button.addEventListener('click', () => select(button.dataset.inventoryReportTab, true));
|
||||||
|
button.addEventListener('keydown', (event) => {
|
||||||
|
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const nextIndex = event.key === 'Home' ? 0
|
||||||
|
: event.key === 'End' ? buttons.length - 1
|
||||||
|
: (index + (event.key === 'ArrowRight' ? 1 : -1) + buttons.length) % buttons.length;
|
||||||
|
select(buttons[nextIndex].dataset.inventoryReportTab, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
select(new URLSearchParams(window.location.search).get('mapping') === '1' ? 'mapping' : 'total');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-inventory-mapping-dialog]').forEach((dialog) => {
|
||||||
|
const form = dialog.querySelector('[data-inventory-mapping-form]');
|
||||||
|
const method = form?.querySelector('[data-inventory-mapping-method]');
|
||||||
|
const title = dialog.querySelector('[data-inventory-mapping-dialog-title]');
|
||||||
|
const provider = form?.querySelector('[data-inventory-mapping-provider]');
|
||||||
|
const warehouse = form?.querySelector('[data-inventory-mapping-warehouse]');
|
||||||
|
const shortName = form?.querySelector('[data-inventory-mapping-short-name]');
|
||||||
|
const storeUrl = @json(route('business.product-inventory.inventory-report.warehouse-mappings.store'));
|
||||||
|
const updateUrl = @json(route('business.product-inventory.inventory-report.warehouse-mappings.update', ['id' => '__ID__']));
|
||||||
|
if (!form || !method || !title || !provider || !warehouse || !shortName) return;
|
||||||
|
const suggest = () => {
|
||||||
|
if (shortName.dataset.edited === 'true') return;
|
||||||
|
const text = `${provider.value} ${warehouse.value}`;
|
||||||
|
let value = '';
|
||||||
|
const special = [
|
||||||
|
['代发仓', '代发仓'], ['京东云仓', '京东云仓-不看'], ['销退仓', '销退仓-不看'],
|
||||||
|
['客退', '客退仓-不看'], ['次品仓', '退货报废-不看'], ['数量校验仓', '数量校验仓-不看'],
|
||||||
|
['自动发货仓', '虚拟仓-不看'], ['京东京造', '京东京造-不看'], ['POP仓', '京东入仓-不看'],
|
||||||
|
['江西仓送货', '南昌樵舍仓'], ['运营C崔桥工厂仓', '崔桥工厂仓'], ['运营C横林二仓', '横林二仓'],
|
||||||
|
['运营C睢宁', '睢宁仓'], ['运营C新苏北', '新苏北仓'],
|
||||||
|
];
|
||||||
|
for (const [needle, result] of special) if (text.includes(needle)) { value = result; break; }
|
||||||
|
if (!value) {
|
||||||
|
value = warehouse.value.replace(/(主仓|进货仓)$/u, '').replace(/^(简尚-|京仓-|三方-|奇门-)/u, '');
|
||||||
|
const providerBase = provider.value.replace(/^(简尚-|京仓-|三方-|奇门-)/u, '');
|
||||||
|
if (value === providerBase || value.startsWith(providerBase)) value = providerBase;
|
||||||
|
}
|
||||||
|
shortName.value = value;
|
||||||
|
};
|
||||||
|
provider.addEventListener('input', suggest);
|
||||||
|
warehouse.addEventListener('input', suggest);
|
||||||
|
shortName.addEventListener('input', () => { shortName.dataset.edited = 'true'; });
|
||||||
|
dialog.querySelector('[data-inventory-mapping-cancel]')?.addEventListener('click', () => dialog.close());
|
||||||
|
document.querySelector('[data-inventory-mapping-create]')?.addEventListener('click', () => {
|
||||||
|
title.textContent = '新增仓库名称映射';
|
||||||
|
form.action = storeUrl;
|
||||||
|
method.value = 'POST';
|
||||||
|
provider.value = '';
|
||||||
|
warehouse.value = '';
|
||||||
|
shortName.value = '';
|
||||||
|
shortName.dataset.edited = 'false';
|
||||||
|
dialog.showModal();
|
||||||
|
provider.focus();
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-inventory-mapping-edit]').forEach((button) => button.addEventListener('click', () => {
|
||||||
|
title.textContent = '修改仓库名称映射';
|
||||||
|
form.action = updateUrl.replace('__ID__', button.dataset.id);
|
||||||
|
method.value = 'PATCH';
|
||||||
|
provider.value = button.dataset.provider || '';
|
||||||
|
warehouse.value = button.dataset.warehouse || '';
|
||||||
|
shortName.value = button.dataset.shortName || '';
|
||||||
|
shortName.dataset.edited = 'true';
|
||||||
|
dialog.showModal();
|
||||||
|
provider.focus();
|
||||||
|
}));
|
||||||
|
dialog.addEventListener('click', (event) => { if (event.target === dialog) dialog.close(); });
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll('[data-subject-row]').forEach((row) => {
|
document.querySelectorAll('[data-subject-row]').forEach((row) => {
|
||||||
|
|||||||
@ -321,11 +321,12 @@
|
|||||||
const keyFor = (table) => {
|
const keyFor = (table) => {
|
||||||
if (table.dataset.financeTableKey) return table.dataset.financeTableKey;
|
if (table.dataset.financeTableKey) return table.dataset.financeTableKey;
|
||||||
const currentContext = routeContext();
|
const currentContext = routeContext();
|
||||||
const parent = table.closest('[data-directory-page], [data-refund-page], [data-finance-table-surface], .business-content-section') || table.parentElement;
|
const tableScope = table.closest('[data-directory-table-scope]');
|
||||||
|
const parent = tableScope || table.closest('[data-directory-page], [data-refund-page], [data-finance-table-surface], .business-content-section') || table.parentElement;
|
||||||
const name = table.getAttribute('aria-label') || table.dataset.financeTableName || table.className || 'table';
|
const name = table.getAttribute('aria-label') || table.dataset.financeTableName || table.className || 'table';
|
||||||
const siblings = parent ? [...parent.querySelectorAll(selector)].filter((candidate) => candidate.matches(selector)) : [table];
|
const siblings = parent ? [...parent.querySelectorAll(selector)].filter((candidate) => candidate.matches(selector)) : [table];
|
||||||
const index = Math.max(0, siblings.indexOf(table));
|
const index = Math.max(0, siblings.indexOf(table));
|
||||||
const scope = parent?.dataset.directoryPage || parent?.dataset.refundPage || name;
|
const scope = tableScope?.dataset.directoryTableKey || parent?.dataset.directoryPage || parent?.dataset.refundPage || name;
|
||||||
table.dataset.financeTableKey = `${currentContext.user}:${currentContext.section}:${currentContext.page}:${currentContext.tab}:${scope}:${index}`;
|
table.dataset.financeTableKey = `${currentContext.user}:${currentContext.section}:${currentContext.page}:${currentContext.tab}:${scope}:${index}`;
|
||||||
return table.dataset.financeTableKey;
|
return table.dataset.financeTableKey;
|
||||||
};
|
};
|
||||||
@ -377,6 +378,8 @@
|
|||||||
cell.dataset.financeColumnKey = cell.dataset.columnKey || cell.dataset.field || cell.dataset.exportField || `${slug(cell.textContent)}_${index}`;
|
cell.dataset.financeColumnKey = cell.dataset.columnKey || cell.dataset.field || cell.dataset.exportField || `${slug(cell.textContent)}_${index}`;
|
||||||
}
|
}
|
||||||
cell.dataset.financeColumnIndex = String(index);
|
cell.dataset.financeColumnIndex = String(index);
|
||||||
|
if (!cell.dataset.financeFixed && cell.dataset.directoryFixed === 'start') cell.dataset.financeFixed = 'true';
|
||||||
|
if (!cell.dataset.financeFixed && cell.dataset.directoryFixed === 'end') cell.dataset.financeFixed = 'end';
|
||||||
if (!cell.dataset.financeFixed && index === 0 && (cell.querySelector('input[type="checkbox"]') || ['#', '序号'].includes(cell.textContent.trim()))) cell.dataset.financeFixed = 'true';
|
if (!cell.dataset.financeFixed && index === 0 && (cell.querySelector('input[type="checkbox"]') || ['#', '序号'].includes(cell.textContent.trim()))) cell.dataset.financeFixed = 'true';
|
||||||
});
|
});
|
||||||
const keys = headers.map((cell) => cell.dataset.financeColumnKey);
|
const keys = headers.map((cell) => cell.dataset.financeColumnKey);
|
||||||
@ -555,8 +558,19 @@
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const savedVisibility = JSON.parse(localStorage.getItem(storageKey(table, 'visibility')) || 'null');
|
const savedVisibility = JSON.parse(localStorage.getItem(storageKey(table, 'visibility')) || 'null');
|
||||||
if (Array.isArray(savedVisibility)) checks.forEach((check) => { if (!check.disabled) check.checked = savedVisibility.includes(check.value); });
|
|
||||||
const savedOrder = JSON.parse(localStorage.getItem(storageKey(table, 'order')) || 'null');
|
const savedOrder = JSON.parse(localStorage.getItem(storageKey(table, 'order')) || 'null');
|
||||||
|
if (table.id === 'inventory-report-total-table' && !localStorage.getItem(storageKey(table, 'warehouse-short-name-v1'))) {
|
||||||
|
if (Array.isArray(savedOrder) && !savedOrder.includes('warehouse_short_name')) {
|
||||||
|
savedOrder.splice(savedOrder.indexOf('warehouse') + 1, 0, 'warehouse_short_name');
|
||||||
|
localStorage.setItem(storageKey(table, 'order'), JSON.stringify(savedOrder));
|
||||||
|
}
|
||||||
|
if (Array.isArray(savedVisibility) && !savedVisibility.includes('warehouse_short_name')) {
|
||||||
|
savedVisibility.push('warehouse_short_name');
|
||||||
|
localStorage.setItem(storageKey(table, 'visibility'), JSON.stringify(savedVisibility));
|
||||||
|
}
|
||||||
|
localStorage.setItem(storageKey(table, 'warehouse-short-name-v1'), '1');
|
||||||
|
}
|
||||||
|
if (Array.isArray(savedVisibility)) checks.forEach((check) => { if (!check.disabled) check.checked = savedVisibility.includes(check.value); });
|
||||||
applyOrder(Array.isArray(savedOrder) ? savedOrder : defaults, false);
|
applyOrder(Array.isArray(savedOrder) ? savedOrder : defaults, false);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
setVisibility(table, checks, false);
|
setVisibility(table, checks, false);
|
||||||
@ -596,8 +610,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureControls(table, headers) {
|
function ensureControls(table, headers) {
|
||||||
const existingDirectoryTools = table.closest('[data-directory-page]')?.querySelector('[data-directory-tools]');
|
const tableScope = table.closest('[data-directory-table-scope]');
|
||||||
const root = table.closest('[data-finance-table-surface], .upstream-data-table-panel, .directory-table-page, .tb-panel, .ab-panel, .marketing-bills-table-wrapper, .bill-table-wrapper, .jd-wallet-table-wrap, .alibaba-bill-table-wrapper, .zhongcai-table-wrapper') || table.parentElement;
|
const existingDirectoryTools = tableScope?.querySelector('[data-directory-tools]') || table.closest('[data-directory-page]')?.querySelector('[data-directory-tools]');
|
||||||
|
const root = tableScope || table.closest('[data-finance-table-surface], .upstream-data-table-panel, .directory-table-page, .tb-panel, .ab-panel, .marketing-bills-table-wrapper, .bill-table-wrapper, .jd-wallet-table-wrap, .alibaba-bill-table-wrapper, .zhongcai-table-wrapper') || table.parentElement;
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
let controls = existingDirectoryTools || root.querySelector('[data-finance-controls]');
|
let controls = existingDirectoryTools || root.querySelector('[data-finance-controls]');
|
||||||
if (!controls) {
|
if (!controls) {
|
||||||
@ -665,31 +680,46 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function exportDirectory(table, button, includeHiddenRows, allColumns) {
|
async function exportDirectory(table, button, includeHiddenRows, allColumns) {
|
||||||
|
const tableScope = table.closest('[data-directory-table-scope]');
|
||||||
const page = table.closest('[data-directory-page]');
|
const page = table.closest('[data-directory-page]');
|
||||||
const search = page?.querySelector('[data-directory-search]');
|
const search = tableScope?.querySelector('[data-directory-search]') || page?.querySelector('[data-directory-search]');
|
||||||
const headers = leafHeaders(table);
|
const headers = leafHeaders(table);
|
||||||
const selected = new Set([...table.querySelectorAll('thead th[data-finance-column-key]')]
|
const exportableHeaders = headers.filter((header) => header.dataset.financeFixed !== 'end' && header.dataset.directoryFixed !== 'end');
|
||||||
|
const selected = new Set(exportableHeaders
|
||||||
.filter((cell) => !cell.classList.contains('is-column-hidden'))
|
.filter((cell) => !cell.classList.contains('is-column-hidden'))
|
||||||
.map((cell) => cell.dataset.financeColumnKey));
|
.map((cell) => cell.dataset.financeColumnKey));
|
||||||
const order = headers.map((header) => header.dataset.financeColumnKey);
|
const order = exportableHeaders.map((header) => header.dataset.financeColumnKey);
|
||||||
const exportOrder = allColumns ? order : order.filter((key) => selected.has(key));
|
const exportOrder = allColumns ? order : order.filter((key) => selected.has(key));
|
||||||
const header = exportOrder.map((key) => csvEscape(headers.find((cell) => cell.dataset.financeColumnKey === key)?.textContent.trim())).join(',');
|
const header = exportOrder.map((key) => csvEscape(headers.find((cell) => cell.dataset.financeColumnKey === key)?.textContent.trim())).join(',');
|
||||||
const rows = [];
|
const rows = [];
|
||||||
const lastPage = Number(page?.dataset.directoryLastPage || 1);
|
const lastPage = Number(tableScope?.dataset.directoryLastPage || page?.dataset.directoryLastPage || 1);
|
||||||
|
const sourceUrl = tableScope?.dataset.directorySourceUrl || window.location.href;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
const originalLabel = button.textContent;
|
const originalLabel = button.textContent;
|
||||||
try {
|
try {
|
||||||
if (includeHiddenRows && lastPage > 1) {
|
if (includeHiddenRows && lastPage > 1) {
|
||||||
for (let current = 1; current <= lastPage; current += 1) {
|
for (let current = 1; current <= lastPage; current += 1) {
|
||||||
button.textContent = `导出中 ${current}/${lastPage}`;
|
button.textContent = `导出中 ${current}/${lastPage}`;
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(sourceUrl, window.location.origin);
|
||||||
url.searchParams.set('page', String(current));
|
url.searchParams.set('page', String(current));
|
||||||
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||||
if (!response.ok) throw new Error(`第 ${current} 页读取失败`);
|
if (!response.ok) throw new Error(`第 ${current} 页读取失败`);
|
||||||
const parsed = new DOMParser().parseFromString(await response.text(), 'text/html');
|
const parsed = new DOMParser().parseFromString(await response.text(), 'text/html');
|
||||||
parsed.querySelectorAll('.directory-table tbody tr:not(.is-empty)').forEach((row) => {
|
const parsedTable = table.id
|
||||||
|
? parsed.querySelector(`#${CSS.escape(table.id)}`)
|
||||||
|
: parsed.querySelector('.directory-table');
|
||||||
|
parsedTable?.querySelectorAll('tbody tr:not(.is-empty)').forEach((row) => {
|
||||||
const cells = [...row.children];
|
const cells = [...row.children];
|
||||||
const map = new Map(cells.map((cell, index) => [cell.dataset.columnKey || headers[index]?.dataset.financeColumnKey, cell]));
|
const fixedStartKey = headers.find((header) => header.dataset.financeFixed === 'true')?.dataset.financeColumnKey;
|
||||||
|
const fixedEndKey = headers.find((header) => header.dataset.financeFixed === 'end')?.dataset.financeColumnKey;
|
||||||
|
const map = new Map(cells.map((cell, index) => {
|
||||||
|
const key = cell.dataset.financeColumnKey
|
||||||
|
|| cell.dataset.columnKey
|
||||||
|
|| (cell.dataset.directoryFixed === 'start' ? fixedStartKey : null)
|
||||||
|
|| (cell.dataset.directoryFixed === 'end' ? fixedEndKey : null)
|
||||||
|
|| headers[index]?.dataset.financeColumnKey;
|
||||||
|
return [key, cell];
|
||||||
|
}).filter(([key]) => key));
|
||||||
if (search?.value && !row.textContent.toLocaleLowerCase().includes(search.value.trim().toLocaleLowerCase())) return;
|
if (search?.value && !row.textContent.toLocaleLowerCase().includes(search.value.trim().toLocaleLowerCase())) return;
|
||||||
rows.push(exportOrder.map((key) => csvEscape(cellText(map.get(key)))).join(','));
|
rows.push(exportOrder.map((key) => csvEscape(cellText(map.get(key)))).join(','));
|
||||||
});
|
});
|
||||||
@ -766,7 +796,8 @@
|
|||||||
}
|
}
|
||||||
const button = event.target.closest('[data-directory-export], [data-directory-export-all], [data-finance-export-all-fields]');
|
const button = event.target.closest('[data-directory-export], [data-directory-export-all], [data-finance-export-all-fields]');
|
||||||
if (!button || !button.closest('[data-directory-page]')) return;
|
if (!button || !button.closest('[data-directory-page]')) return;
|
||||||
const table = button.closest('[data-directory-page]')?.querySelector('.directory-table');
|
const tableScope = button.closest('[data-directory-table-scope]');
|
||||||
|
const table = tableScope?.querySelector('.directory-table') || button.closest('[data-directory-page]')?.querySelector('.directory-table');
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
|
|||||||
@ -647,6 +647,7 @@
|
|||||||
'page' => $page,
|
'page' => $page,
|
||||||
'rows' => $rows,
|
'rows' => $rows,
|
||||||
'upstreamError' => $upstreamError,
|
'upstreamError' => $upstreamError,
|
||||||
|
'inventoryWarehouseMappings' => $inventoryWarehouseMappings ?? collect(),
|
||||||
])
|
])
|
||||||
@elseif ($pageView)
|
@elseif ($pageView)
|
||||||
@include($pageView)
|
@include($pageView)
|
||||||
|
|||||||
@ -158,6 +158,17 @@ Route::get('workspace/product-inventory/inventory-report/export', 'Business\Page
|
|||||||
->middleware('business.page:product-inventory,inventory-report')
|
->middleware('business.page:product-inventory,inventory-report')
|
||||||
->name('business.product-inventory.inventory-report.export');
|
->name('business.product-inventory.inventory-report.export');
|
||||||
|
|
||||||
|
Route::prefix('workspace/product-inventory/inventory-report/warehouse-mappings')
|
||||||
|
->middleware('business.page:product-inventory,inventory-report')
|
||||||
|
->name('business.product-inventory.inventory-report.warehouse-mappings.')
|
||||||
|
->group(function () {
|
||||||
|
Route::post('/', 'Business\Pages@storeInventoryWarehouseMapping')->name('store');
|
||||||
|
Route::patch('{id}', 'Business\Pages@updateInventoryWarehouseMapping')->whereNumber('id')->name('update');
|
||||||
|
Route::get('export', 'Business\Pages@exportInventoryWarehouseMappings')->name('export');
|
||||||
|
Route::post('import/preview', 'Business\Pages@previewInventoryWarehouseMappingImport')->middleware('import')->name('import.preview');
|
||||||
|
Route::post('import/commit', 'Business\Pages@commitInventoryWarehouseMappingImport')->middleware('import')->name('import.commit');
|
||||||
|
});
|
||||||
|
|
||||||
Route::prefix('workspace/supplier-payables/product-prices')
|
Route::prefix('workspace/supplier-payables/product-prices')
|
||||||
->name('business.supplier-payables.product-prices.')
|
->name('business.supplier-payables.product-prices.')
|
||||||
->middleware('business.page:supplier-payables,product-prices')
|
->middleware('business.page:supplier-payables,product-prices')
|
||||||
|
|||||||
@ -3,9 +3,12 @@
|
|||||||
namespace Tests\Feature\Business;
|
namespace Tests\Feature\Business;
|
||||||
|
|
||||||
use App\Models\Auth\Permission;
|
use App\Models\Auth\Permission;
|
||||||
|
use App\Models\Business\InventoryWarehouseNameMapping;
|
||||||
|
use App\Business\InventoryWarehouseShortNames;
|
||||||
use App\Jobs\Common\InventoryReportExport;
|
use App\Jobs\Common\InventoryReportExport;
|
||||||
use Illuminate\Support\Facades\Queue;
|
use Illuminate\Support\Facades\Queue;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
use Modules\EcommerceFinance\Repositories\CourierFreightReconciliationMirrorRepository;
|
use Modules\EcommerceFinance\Repositories\CourierFreightReconciliationMirrorRepository;
|
||||||
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
|
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
|
||||||
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
|
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
|
||||||
@ -18,6 +21,12 @@ class PagesTest extends FeatureTestCase
|
|||||||
{
|
{
|
||||||
public function testInventoryReportProvidesAFilteredExportButton(): void
|
public function testInventoryReportProvidesAFilteredExportButton(): void
|
||||||
{
|
{
|
||||||
|
InventoryWarehouseNameMapping::query()->create([
|
||||||
|
'company_id' => 1,
|
||||||
|
'warehouse_provider' => '测试映射仓储方',
|
||||||
|
'warehouse' => '测试映射仓库',
|
||||||
|
'warehouse_short_name' => '测试映射简称',
|
||||||
|
]);
|
||||||
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
||||||
$repository->shouldReceive('available')->once()->andReturnTrue();
|
$repository->shouldReceive('available')->once()->andReturnTrue();
|
||||||
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
|
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
|
||||||
@ -31,7 +40,11 @@ class PagesTest extends FeatureTestCase
|
|||||||
1,
|
1,
|
||||||
Mockery::type('int'),
|
Mockery::type('int'),
|
||||||
)->andReturn([
|
)->andReturn([
|
||||||
'data' => [(object) ['product_code' => '121630-49']],
|
'data' => [(object) [
|
||||||
|
'product_code' => '121630-49',
|
||||||
|
'warehouse_provider' => '测试仓储方',
|
||||||
|
'warehouse' => '测试主仓',
|
||||||
|
]],
|
||||||
'total' => 1,
|
'total' => 1,
|
||||||
'state' => ['status' => 'succeeded'],
|
'state' => ['status' => 'succeeded'],
|
||||||
'sync' => ['code' => 'healthy'],
|
'sync' => ['code' => 'healthy'],
|
||||||
@ -50,8 +63,77 @@ class PagesTest extends FeatureTestCase
|
|||||||
->assertSee('data-inventory-export-scope', false)
|
->assertSee('data-inventory-export-scope', false)
|
||||||
->assertSee('data-inventory-export-fields', false)
|
->assertSee('data-inventory-export-fields', false)
|
||||||
->assertSee('data-inventory-export', false)
|
->assertSee('data-inventory-export', false)
|
||||||
->assertDontSee('<button type="button" data-directory-export-all', false)
|
->assertSee('进销存总表')
|
||||||
->assertDontSee('<button type="button" data-directory-template', false);
|
->assertSee('仓库名称映射表')
|
||||||
|
->assertSee('测试仓储方')
|
||||||
|
->assertSee('测试主仓')
|
||||||
|
->assertSee('data-inventory-report-panel="mapping"', false)
|
||||||
|
->assertSee('id="inventory-report-total-table"', false)
|
||||||
|
->assertSee('id="inventory-warehouse-mapping-table"', false)
|
||||||
|
->assertSee('data-directory-source-url=', false)
|
||||||
|
->assertSee('mapping=1&page=1', false)
|
||||||
|
->assertSee('data-directory-table-key="warehouse-mapping"', false)
|
||||||
|
->assertSee('data-directory-field-picker', false)
|
||||||
|
->assertSee('data-directory-export-all="visible"', false)
|
||||||
|
->assertSee('data-directory-export-all="all-fields"', false)
|
||||||
|
->assertSee('data-inventory-mapping-template', false)
|
||||||
|
->assertSee('data-directory-fixed="start"', false)
|
||||||
|
->assertSee('data-directory-fixed="end"', false);
|
||||||
|
|
||||||
|
$html = $response->getContent();
|
||||||
|
$totalPanelStart = strpos($html, 'id="inventory-report-total-panel"');
|
||||||
|
$mappingPanelStart = strpos($html, 'id="inventory-report-mapping-panel"');
|
||||||
|
$noticePosition = strpos($html, '已从共享业务镜像读取');
|
||||||
|
$filterPosition = strpos($html, 'aria-label="进销存报表筛选查询栏"');
|
||||||
|
|
||||||
|
$this->assertIsInt($totalPanelStart);
|
||||||
|
$this->assertIsInt($mappingPanelStart);
|
||||||
|
$this->assertIsInt($noticePosition);
|
||||||
|
$this->assertIsInt($filterPosition);
|
||||||
|
$this->assertGreaterThan($totalPanelStart, $noticePosition);
|
||||||
|
$this->assertLessThan($mappingPanelStart, $noticePosition);
|
||||||
|
$this->assertGreaterThan($totalPanelStart, $filterPosition);
|
||||||
|
$this->assertLessThan($mappingPanelStart, $filterPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInventoryReportShowsWarehouseShortNameOnlyForExactProviderAndWarehouse(): void
|
||||||
|
{
|
||||||
|
InventoryWarehouseNameMapping::query()->create([
|
||||||
|
'company_id' => 1,
|
||||||
|
'warehouse_provider' => '测试仓储方',
|
||||||
|
'warehouse' => '测试主仓',
|
||||||
|
'warehouse_short_name' => '崔桥主仓',
|
||||||
|
]);
|
||||||
|
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
||||||
|
$repository->shouldReceive('available')->once()->andReturnTrue();
|
||||||
|
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
|
||||||
|
$repository->shouldReceive('paginateMonthlyInventoryReport')->once()->andReturn([
|
||||||
|
'data' => [
|
||||||
|
(object) ['warehouse_provider' => '测试仓储方', 'warehouse' => '测试主仓', 'product_code' => 'MATCHED'],
|
||||||
|
(object) ['warehouse_provider' => '其他仓储方', 'warehouse' => '测试主仓', 'product_code' => 'UNMATCHED'],
|
||||||
|
],
|
||||||
|
'total' => 2,
|
||||||
|
'state' => ['status' => 'succeeded'],
|
||||||
|
'sync' => ['code' => 'healthy'],
|
||||||
|
]);
|
||||||
|
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
|
||||||
|
|
||||||
|
$response = $this->loginAs()->get(route('business.pages.show', [
|
||||||
|
'section' => 'product-inventory', 'page' => 'inventory-report', 'month' => '2026-08',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertSee('data-column-key="warehouse_short_name"', false)
|
||||||
|
->assertSee('data-inventory-short-name-sort', false)
|
||||||
|
->assertSee('value="warehouse_short_name"', false)
|
||||||
|
->assertSee('崔桥主仓');
|
||||||
|
$html = $response->getContent();
|
||||||
|
$this->assertLessThan(
|
||||||
|
strpos($html, 'data-column-key="warehouse_short_name"'),
|
||||||
|
strpos($html, 'data-column-key="warehouse"'),
|
||||||
|
);
|
||||||
|
$this->assertMatchesRegularExpression('/data-column-key="warehouse_short_name"[^>]*>\s*崔桥主仓\s*<\/td>/', $html);
|
||||||
|
$this->assertMatchesRegularExpression('/data-column-key="warehouse_short_name"[^>]*>\s*<\/td>/', $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testInventoryReportKeepsJulyMonthFilter(): void
|
public function testInventoryReportKeepsJulyMonthFilter(): void
|
||||||
@ -127,7 +209,7 @@ class PagesTest extends FeatureTestCase
|
|||||||
$content = $response->streamedContent();
|
$content = $response->streamedContent();
|
||||||
$this->assertSame((string) strlen($content), $response->headers->get('Content-Length'));
|
$this->assertSame((string) strlen($content), $response->headers->get('Content-Length'));
|
||||||
$this->assertStringStartsWith(chr(0xEF) . chr(0xBB) . chr(0xBF), $content);
|
$this->assertStringStartsWith(chr(0xEF) . chr(0xBB) . chr(0xBF), $content);
|
||||||
$this->assertStringContainsString('货主,月份,仓储方,仓库,商品编码,分类,期初数量,期末数量', $content);
|
$this->assertStringContainsString('货主,月份,仓储方,仓库,仓库简称,商品编码,分类,期初数量,期末数量', $content);
|
||||||
$this->assertStringContainsString("'121630-49", $content);
|
$this->assertStringContainsString("'121630-49", $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +240,37 @@ class PagesTest extends FeatureTestCase
|
|||||||
$this->assertSame("\xEF\xBB\xBF商品编码,仓库,期初数量\n'121630-49,主仓,10\n", $response->streamedContent());
|
$this->assertSame("\xEF\xBB\xBF商品编码,仓库,期初数量\n'121630-49,主仓,10\n", $response->streamedContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testInventoryReportExportIncludesMappedShortNameAndLeavesUnmatchedBlank(): void
|
||||||
|
{
|
||||||
|
InventoryWarehouseNameMapping::query()->create([
|
||||||
|
'company_id' => 1,
|
||||||
|
'warehouse_provider' => '测试仓储方',
|
||||||
|
'warehouse' => '主仓',
|
||||||
|
'warehouse_short_name' => '测试简称',
|
||||||
|
]);
|
||||||
|
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
||||||
|
$repository->shouldReceive('available')->once()->andReturnTrue();
|
||||||
|
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
|
||||||
|
$repository->shouldReceive('paginateMonthlyInventoryReport')->once()->andReturn([
|
||||||
|
'data' => [
|
||||||
|
(object) ['warehouse_provider' => '测试仓储方', 'warehouse' => '主仓'],
|
||||||
|
(object) ['warehouse_provider' => '其他仓储方', 'warehouse' => '主仓'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
|
||||||
|
|
||||||
|
$response = $this->loginAs()->get(route('business.product-inventory.inventory-report.export', [
|
||||||
|
'month' => '2026-08', 'scope' => 'page',
|
||||||
|
'columns' => 'warehouse,warehouse_short_name,warehouse_provider',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response->assertOk()->assertDownload();
|
||||||
|
$this->assertSame(
|
||||||
|
"\xEF\xBB\xBF仓库,仓库简称,仓储方\n主仓,测试简称,测试仓储方\n主仓,,其他仓储方\n",
|
||||||
|
$response->streamedContent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testInventoryReportCurrentPageExportDoesNotReadAllFilteredRows(): void
|
public function testInventoryReportCurrentPageExportDoesNotReadAllFilteredRows(): void
|
||||||
{
|
{
|
||||||
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
||||||
@ -218,6 +331,39 @@ class PagesTest extends FeatureTestCase
|
|||||||
Queue::assertPushed(InventoryReportExport::class);
|
Queue::assertPushed(InventoryReportExport::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testInventoryReportBackgroundExportWritesWarehouseShortName(): void
|
||||||
|
{
|
||||||
|
Queue::fake();
|
||||||
|
InventoryWarehouseNameMapping::query()->create([
|
||||||
|
'company_id' => 1,
|
||||||
|
'warehouse_provider' => '后台仓储方',
|
||||||
|
'warehouse' => '后台仓库',
|
||||||
|
'warehouse_short_name' => '后台简称',
|
||||||
|
]);
|
||||||
|
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
||||||
|
$repository->shouldReceive('exportMonthlyInventoryReportRows')->once()->andReturn(
|
||||||
|
(static function (): \Generator {
|
||||||
|
yield (object) ['warehouse_provider' => '后台仓储方', 'warehouse' => '后台仓库'];
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
$fileName = 'inventory-short-name-test-' . uniqid() . '.csv';
|
||||||
|
$path = storage_path('app/temp/' . $fileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
(new InventoryReportExport(
|
||||||
|
$this->user,
|
||||||
|
['month' => '2026-08'],
|
||||||
|
[['name' => 'warehouse_short_name', 'label' => '仓库简称']],
|
||||||
|
$fileName,
|
||||||
|
1,
|
||||||
|
))->handle($repository, app(InventoryWarehouseShortNames::class));
|
||||||
|
|
||||||
|
$this->assertSame("\xEF\xBB\xBF仓库简称\n后台简称\n", file_get_contents($path));
|
||||||
|
} finally {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function testInventoryReportExportRejectsFieldsOutsideThePageSchema(): void
|
public function testInventoryReportExportRejectsFieldsOutsideThePageSchema(): void
|
||||||
{
|
{
|
||||||
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
||||||
@ -1194,4 +1340,68 @@ class PagesTest extends FeatureTestCase
|
|||||||
->assertDontSee('class="business-data-page"', false)
|
->assertDontSee('class="business-data-page"', false)
|
||||||
->assertDontSee('class="business-blank-page"', false);
|
->assertDontSee('class="business-blank-page"', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testInventoryWarehouseMappingCanBeAddedEditedAndExportedAsExcel(): void
|
||||||
|
{
|
||||||
|
$provider = '测试仓储方-' . uniqid();
|
||||||
|
$warehouse = '测试仓库-' . uniqid();
|
||||||
|
|
||||||
|
$this->loginAs()->post(route('business.product-inventory.inventory-report.warehouse-mappings.store'), [
|
||||||
|
'warehouse_provider' => $provider,
|
||||||
|
'warehouse' => $warehouse,
|
||||||
|
])->assertRedirect();
|
||||||
|
|
||||||
|
$mapping = InventoryWarehouseNameMapping::query()
|
||||||
|
->where('warehouse_provider', $provider)
|
||||||
|
->where('warehouse', $warehouse)
|
||||||
|
->firstOrFail();
|
||||||
|
$this->assertStringStartsWith('测试仓库', $mapping->warehouse_short_name);
|
||||||
|
|
||||||
|
$this->loginAs()->patch(route('business.product-inventory.inventory-report.warehouse-mappings.update', $mapping->id), [
|
||||||
|
'warehouse_provider' => $provider,
|
||||||
|
'warehouse' => $warehouse,
|
||||||
|
'warehouse_short_name' => '测试简称',
|
||||||
|
])->assertRedirect();
|
||||||
|
|
||||||
|
$response = $this->loginAs()->get(route('business.product-inventory.inventory-report.warehouse-mappings.export'));
|
||||||
|
$response->assertOk()
|
||||||
|
->assertDownload()
|
||||||
|
->assertHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
|
$path = tempnam(sys_get_temp_dir(), 'inventory-mapping-export-');
|
||||||
|
file_put_contents($path, $response->streamedContent());
|
||||||
|
try {
|
||||||
|
$sheet = IOFactory::load($path)->getActiveSheet();
|
||||||
|
$this->assertSame(['仓储方', '仓库', '仓库简称'], $sheet->rangeToArray('A1:C1')[0]);
|
||||||
|
$this->assertSame([$provider, $warehouse, '测试简称'], $sheet->rangeToArray('A2:C2')[0]);
|
||||||
|
} finally {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInventoryWarehouseMappingPageUsesTheIndependentLedger(): void
|
||||||
|
{
|
||||||
|
$this->loginAs()->get(route('business.pages.show', [
|
||||||
|
'section' => 'product-inventory',
|
||||||
|
'page' => 'inventory-report',
|
||||||
|
'mapping' => 1,
|
||||||
|
]))->assertOk()
|
||||||
|
->assertSeeText('仓储方')
|
||||||
|
->assertSeeText('仓库简称')
|
||||||
|
->assertSeeText('导入并预览')
|
||||||
|
->assertSeeText('导出完整表格(Excel)')
|
||||||
|
->assertSeeText('选择文件')
|
||||||
|
->assertSee('class="inventory-warehouse-mapping__export"', false)
|
||||||
|
->assertSee('class="inventory-warehouse-mapping__file-label"', false)
|
||||||
|
->assertSee('class="inventory-warehouse-mapping__import-button"', false)
|
||||||
|
->assertSee('data-inventory-mapping-create', false)
|
||||||
|
->assertSee('data-inventory-mapping-edit', false)
|
||||||
|
->assertSee('data-directory-table-key="warehouse-mapping"', false)
|
||||||
|
->assertSee('data-inventory-mapping-template', false)
|
||||||
|
->assertSeeText('当前页·显示列')
|
||||||
|
->assertSeeText('全部筛选结果·全部字段')
|
||||||
|
->assertSee('.inventory-warehouse-mapping__table thead,', false)
|
||||||
|
->assertSee('position: sticky;', false)
|
||||||
|
->assertSee('data-directory-fixed="start"', false)
|
||||||
|
->assertSee('data-directory-fixed="end"', false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ class NavigationTest extends FeatureTestCase
|
|||||||
->assertSee('/workspace/order-data/raw-data', false)
|
->assertSee('/workspace/order-data/raw-data', false)
|
||||||
->assertSee('/workspace/refunds/non-jingxi-refund-data', false)
|
->assertSee('/workspace/refunds/non-jingxi-refund-data', false)
|
||||||
->assertSee('/workspace/product-inventory/normal-products', false)
|
->assertSee('/workspace/product-inventory/normal-products', false)
|
||||||
|
->assertSee('/workspace/product-inventory/monthly-weighted-average-price', false)
|
||||||
->assertSee('/workspace/product-inventory/inventory-report', false);
|
->assertSee('/workspace/product-inventory/inventory-report', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,12 +38,33 @@ class NavigationTest extends FeatureTestCase
|
|||||||
'采购退货', '其它进仓', '其它退货', '销售出仓', '销售退货',
|
'采购退货', '其它进仓', '其它退货', '销售出仓', '销售退货',
|
||||||
'盘点', '调拨出', '调拨入', '期初', '其它出库',
|
'盘点', '调拨出', '调拨入', '期初', '其它出库',
|
||||||
])
|
])
|
||||||
->assertSeeText('复用“应付款项 → 快递应付 → 三方仓进销存”')
|
->assertSeeText('进销存总表')
|
||||||
|
->assertSeeText('仓库名称映射表')
|
||||||
|
->assertSee('data-inventory-report-tabs', false)
|
||||||
|
->assertSee('data-inventory-report-tab="mapping"', false)
|
||||||
|
->assertSee('id="inventory-report-mapping-panel"', false)
|
||||||
|
->assertDontSeeText('复用“应付款项 → 快递应付 → 三方仓进销存”')
|
||||||
->assertSeeText('进销存共享镜像尚未建立')
|
->assertSeeText('进销存共享镜像尚未建立')
|
||||||
->assertDontSeeText('已按附件模板导入 20 条演示数据')
|
->assertDontSeeText('已按附件模板导入 20 条演示数据')
|
||||||
->assertDontSeeText('进销存报表字段和数据来源尚未定义。');
|
->assertDontSeeText('进销存报表字段和数据来源尚未定义。');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMonthlyWeightedAveragePricePageIsAvailable(): void
|
||||||
|
{
|
||||||
|
$this->loginAs()
|
||||||
|
->get(route('business.pages.show', [
|
||||||
|
'section' => 'product-inventory',
|
||||||
|
'page' => 'monthly-weighted-average-price',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSeeText('商品月加权平均单价')
|
||||||
|
->assertSeeText('取数规则待确认')
|
||||||
|
->assertSeeTextInOrder([
|
||||||
|
'月份', '商品编码', '商品名称', '采购数量', '采购金额', '月加权平均单价',
|
||||||
|
])
|
||||||
|
->assertSeeText('确认取数规则后再接入经营分析副本库数据');
|
||||||
|
}
|
||||||
|
|
||||||
public function testFinancialRiskControlPageIsAvailableBetweenFinancialReports(): void
|
public function testFinancialRiskControlPageIsAvailableBetweenFinancialReports(): void
|
||||||
{
|
{
|
||||||
$navigation = BusinessNavigation::all();
|
$navigation = BusinessNavigation::all();
|
||||||
@ -210,11 +232,11 @@ class NavigationTest extends FeatureTestCase
|
|||||||
array_column($orderData['pages'][0]['tabs'], 'title')
|
array_column($orderData['pages'][0]['tabs'], 'title')
|
||||||
);
|
);
|
||||||
$this->assertSame(
|
$this->assertSame(
|
||||||
['normal-products', 'online-daifa-price', 'inventory-report'],
|
['normal-products', 'online-daifa-price', 'monthly-weighted-average-price', 'inventory-report'],
|
||||||
array_column($productInventory['pages'], 'slug')
|
array_column($productInventory['pages'], 'slug')
|
||||||
);
|
);
|
||||||
$this->assertSame(
|
$this->assertSame(
|
||||||
['普通商品资料', '线上代发价格', '进销存报表'],
|
['普通商品资料', '线上代发价格', '商品月加权平均单价', '进销存报表'],
|
||||||
array_column($productInventory['pages'], 'title')
|
array_column($productInventory['pages'], 'title')
|
||||||
);
|
);
|
||||||
$this->assertArrayNotHasKey('children', $orderData['pages'][0]);
|
$this->assertArrayNotHasKey('children', $orderData['pages'][0]);
|
||||||
|
|||||||
@ -50,4 +50,27 @@ class WebProcessOwnershipTest extends TestCase
|
|||||||
$initializer
|
$initializer
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_local_entrypoint_does_not_build_persistent_laravel_caches(): void
|
||||||
|
{
|
||||||
|
$entrypoint = file_get_contents(base_path('deploy/entrypoint.sh'));
|
||||||
|
|
||||||
|
$this->assertIsString($entrypoint);
|
||||||
|
$this->assertStringContainsString('if [ "$app_environment" = "local" ]; then', $entrypoint);
|
||||||
|
$this->assertStringContainsString('run_artisan_as_web optimize:clear', $entrypoint);
|
||||||
|
$this->assertStringContainsString('run_artisan_as_web config:cache', $entrypoint);
|
||||||
|
$this->assertStringContainsString('run_artisan_as_web view:cache', $entrypoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_local_php_and_view_caches_are_disabled(): void
|
||||||
|
{
|
||||||
|
$php = file_get_contents(base_path('deploy/php-local.ini'));
|
||||||
|
$view = file_get_contents(base_path('config/view.php'));
|
||||||
|
|
||||||
|
$this->assertIsString($php);
|
||||||
|
$this->assertStringContainsString('opcache.enable=0', $php);
|
||||||
|
$this->assertStringContainsString('opcache.enable_cli=0', $php);
|
||||||
|
$this->assertStringContainsString("'cache' => env('APP_ENV', 'production') === 'local'", $view);
|
||||||
|
$this->assertStringContainsString("? false", $view);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user