9 月有 19 家新店铺(1245 行)在历史映射里查不到,导致利润中心为空。现在按「ops_fys_shop_info.erpShopName → responsibleDepartment」补(就是店铺维度报表里 ERP系统店铺名称 → 店铺所属利润中心 那一列),两边都查不到才留空并在命令输出里列出。 历史值仍然优先,避免把人工维护过的取值改掉:实测 371 家可在 ERP 命中的店铺里 365 家两边一致,6 家不一致(朴美家居、朴美书房、拼多多-HX103、索菲亚办公、顾升、乐之益)已单独报给用户确认。
360 lines
18 KiB
PHP
360 lines
18 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Business;
|
||
|
||
use App\Business\PlantingDetailImporter;
|
||
use App\Business\PlantingWorkOrderSynchronizer;
|
||
use App\Models\Auth\Permission;
|
||
use App\Models\Business\PlantingDetail;
|
||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Tests\Feature\FeatureTestCase;
|
||
|
||
/**
|
||
* 账单流水数据 › 种菜明细表(导入 + 页面 + 筛选 + 导出)。
|
||
*/
|
||
class PlantingDetailsPageTest extends FeatureTestCase
|
||
{
|
||
private array $files = [];
|
||
|
||
protected function tearDown(): void
|
||
{
|
||
foreach ($this->files as $file) {
|
||
if (is_file($file)) {
|
||
unlink($file);
|
||
}
|
||
}
|
||
|
||
parent::tearDown();
|
||
}
|
||
|
||
public function test_import_then_page_filter_and_export(): void
|
||
{
|
||
$this->allowPage();
|
||
$result = app(PlantingDetailImporter::class)->import($this->csvFixture(), $this->company->id, true, null, 'test-import');
|
||
|
||
$this->assertSame(2, $result['inserted']);
|
||
$this->assertSame('2665.3600', $result['selling_price']);
|
||
$this->assertSame('2665.8600', $result['payment_amount']);
|
||
$this->assertSame('2026-06-01', $result['date_min']);
|
||
$this->assertSame('2026-07-15', $result['date_max']);
|
||
|
||
$page = $this->loginAs()->get(route('business.pages.show', [
|
||
'section' => 'planting-data',
|
||
'page' => 'planting-details',
|
||
]));
|
||
|
||
$page->assertOk()
|
||
->assertSee('种菜明细表')
|
||
->assertSee('3306215028042824171')
|
||
->assertSee('260529-659019537933390')
|
||
->assertSee('2,665.35')
|
||
->assertSee('阿里巴巴-常州乐畅家居有限公司');
|
||
|
||
// 时间筛选是可用的日期区间控件:点整块开日历、单个清除、快捷区间按钮
|
||
$page->assertSee('id="planting-date-from"', false)
|
||
->assertSee('id="planting-date-to"', false)
|
||
->assertSee('data-planting-clear="planting-date-from"', false)
|
||
->assertSee('data-planting-range="this-month"', false)
|
||
->assertSee('showPicker', false)
|
||
->assertSee('addEventListener(\'change\'', false);
|
||
|
||
// 按店铺 + 日期筛选
|
||
$filtered = $this->loginAs()->get(route('business.pages.show', [
|
||
'section' => 'planting-data',
|
||
'page' => 'planting-details',
|
||
'store' => '拼多多-亿家达实木家具官方旗舰店',
|
||
'date_from' => '2026-07-01',
|
||
]));
|
||
|
||
$filtered->assertOk()
|
||
->assertSee('260529-659019537933390')
|
||
->assertDontSee('3306215028042824171');
|
||
|
||
// 导出:表头 + 行数与页面筛选一致
|
||
$export = $this->loginAs()->get(route('business.planting-data.planting-details.export', [
|
||
'store' => '拼多多-亿家达实木家具官方旗舰店',
|
||
]));
|
||
|
||
$export->assertOk()->assertDownload();
|
||
$csv = $export->streamedContent();
|
||
$this->assertStringContainsString('日期,订单编号,财务售价', explode("\n", $csv)[0]);
|
||
$this->assertStringContainsString('260529-659019537933390', $csv);
|
||
$this->assertStringNotContainsString('3306215028042824171', $csv);
|
||
$this->assertStringContainsString('2026-07-15', $csv);
|
||
}
|
||
|
||
public function test_importer_handles_excel_serial_dates_and_grouped_amounts(): void
|
||
{
|
||
$path = $this->writeFile('planting.xlsx');
|
||
$book = new Spreadsheet();
|
||
$sheet = $book->getActiveSheet();
|
||
$sheet->fromArray([
|
||
['日期', '订单编号', '财务售价', '财务运费', '财务佣金', '京喜差价', '扣款', '财务空包费', '财务打款金额', '笔数', '备注', '正确店铺', '负责人', '账户', '类型', '创建人', '标签', '所属利润中心', '负责平台', 'ID'],
|
||
[46174, 'ORDER-1', '1,234.56', 0, '0.5', 0, 0, 0, '1,235.06', 2, '', '天猫-亿家达旗舰店', '仝孟欣', '', 'CCDD', '齐静春', 'CCDD', '运营中心A', '', 'ID-1'],
|
||
// 无效日期(空)+ 无效订单号 → 跳过
|
||
['', '', 10, 0, 0, 0, 0, 0, 10, 1, '', '天猫-亿家达旗舰店', '', '', '', '', '', '', '', ''],
|
||
]);
|
||
(new Xlsx($book))->save($path);
|
||
$book->disconnectWorksheets();
|
||
|
||
$result = app(PlantingDetailImporter::class)->import($path, $this->company->id, true, null, 'test-xlsx');
|
||
|
||
$this->assertSame(1, $result['inserted']);
|
||
$this->assertSame(1, $result['skipped']);
|
||
$this->assertSame('2026-06-01', $result['date_min']);
|
||
|
||
$record = PlantingDetail::query()->where('company_id', $this->company->id)->firstOrFail();
|
||
$this->assertSame('2026-06-01', $record->bill_date->toDateString());
|
||
$this->assertSame('1234.5600', $record->finance_selling_price);
|
||
$this->assertSame('1235.0600', $record->finance_payment_amount);
|
||
$this->assertSame(2, $record->pieces);
|
||
$this->assertSame('CCDD', $record->type);
|
||
$this->assertSame('运营中心A', $record->profit_center);
|
||
}
|
||
|
||
/** 源表 ID 列存在「一个格子多个 ID、换行分隔」的行,导入要保留而不是报错截断。 */
|
||
public function test_importer_keeps_multi_line_external_ids(): void
|
||
{
|
||
$longId = implode("\n", ['10205993844217', '10205981846604', '10205993844217', '10210832117878']);
|
||
$path = $this->writeFile('planting-multi-id.csv');
|
||
$handle = fopen($path, 'wb');
|
||
fputcsv($handle, ['日期', '订单编号', '财务售价', '财务运费', '财务佣金', '京喜差价', '扣款', '财务空包费', '财务打款金额', '笔数', '备注', '正确店铺', '负责人', '账户', '类型', '创建人', '标签', '所属利润中心', '负责平台', 'ID']);
|
||
fputcsv($handle, ['2026-03-05', 'ORDER-9', '1356', '0', '0', '0', '0', '0', '1356', '1', '', '京喜-招财邸喜店工厂店', '李金林', '6228760405003956861', '', '骆冰', '', '运营中心A', '', $longId]);
|
||
fclose($handle);
|
||
|
||
$result = app(PlantingDetailImporter::class)->import($path, $this->company->id, true, null, 'test-multi-id');
|
||
$this->assertSame(1, $result['inserted']);
|
||
|
||
$record = PlantingDetail::query()->where('company_id', $this->company->id)->firstOrFail();
|
||
$this->assertSame($longId, $record->external_id);
|
||
}
|
||
|
||
/**
|
||
* 种菜报销工单同步:字段映射、日期=完成日-1、利润中心回填、重复执行不翻倍。
|
||
*/
|
||
public function test_sync_from_work_orders_maps_fields_and_dedupes(): void
|
||
{
|
||
$this->allowPage();
|
||
$this->fakeWorkOrderMirror();
|
||
|
||
// 历史行提供 店铺 → 利润中心 映射
|
||
PlantingDetail::query()->create([
|
||
'company_id' => $this->company->id,
|
||
'bill_date' => '2026-06-01',
|
||
'order_no' => 'HISTORY-1',
|
||
'store' => '天猫-亿家达旗舰店',
|
||
'profit_center' => '运营中心A',
|
||
'finance_payment_amount' => '1.0000',
|
||
]);
|
||
// 本月里没有对应工单的人工行:默认不删
|
||
$manual = PlantingDetail::query()->create([
|
||
'company_id' => $this->company->id,
|
||
'bill_date' => '2026-09-10',
|
||
'order_no' => 'MANUAL-ONLY',
|
||
'store' => '天猫-亿家达旗舰店',
|
||
'finance_payment_amount' => '9.0000',
|
||
]);
|
||
|
||
$result = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id);
|
||
|
||
// 只认「种菜报销 + 已完成 + 完成时间在 9/2~10/1」的两张工单
|
||
$this->assertSame(2, $result['orders']);
|
||
$this->assertSame(2, $result['inserted']);
|
||
$this->assertSame(0, $result['deleted']);
|
||
|
||
$first = PlantingDetail::query()->where('company_id', $this->company->id)
|
||
->where('order_no', '3316392626048007769')->firstOrFail();
|
||
$this->assertSame('2026-09-01', $first->bill_date->toDateString()); // 完成日 9-02 → 日期 9-01
|
||
$this->assertSame('37.7900', $first->finance_selling_price);
|
||
$this->assertSame('2.0000', $first->finance_commission);
|
||
$this->assertSame('37.7900', $first->finance_payment_amount);
|
||
$this->assertSame('天猫-亿家达旗舰店', $first->store);
|
||
$this->assertSame('黄文梳', $first->owner);
|
||
$this->assertSame('6214830551373771', $first->account);
|
||
$this->assertSame('CCDD', $first->type);
|
||
$this->assertSame('CCDD', $first->tag);
|
||
$this->assertSame('黄文梳', $first->creator);
|
||
$this->assertSame('1074872715022', $first->external_id);
|
||
$this->assertSame('运营中心A', $first->profit_center); // 从历史行回填
|
||
$this->assertSame(1, $first->pieces);
|
||
$this->assertSame(PlantingWorkOrderSynchronizer::SOURCE, $first->created_from);
|
||
|
||
$second = PlantingDetail::query()->where('company_id', $this->company->id)
|
||
->where('order_no', '260901-000000000000001')->firstOrFail();
|
||
$this->assertSame('2026-09-29', $second->bill_date->toDateString()); // 完成日 9-30
|
||
// 店铺在历史里没有利润中心,用店铺维度报表的 ERP 店铺资料补齐
|
||
$this->assertSame('运营中心G', $second->profit_center);
|
||
$this->assertSame(1, $result['profit_center_from_erp']);
|
||
|
||
// 再跑一遍:同月同订单号先删后写,不会翻倍
|
||
$again = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id);
|
||
$this->assertSame(2, $again['deleted']);
|
||
$this->assertSame(2, $again['inserted']);
|
||
$this->assertSame(1, PlantingDetail::query()->where('company_id', $this->company->id)
|
||
->where('order_no', '3316392626048007769')->count());
|
||
$this->assertSame(1, PlantingDetail::query()->where('company_id', $this->company->id)
|
||
->whereKey($manual->id)->count());
|
||
|
||
// dry-run 不写库
|
||
$dry = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id, true);
|
||
$this->assertTrue($dry['dry_run']);
|
||
$this->assertSame(2, $dry['orders']);
|
||
$this->assertSame(0, $dry['inserted']);
|
||
}
|
||
|
||
public function test_sync_force_clear_replaces_whole_month(): void
|
||
{
|
||
$this->allowPage();
|
||
$this->fakeWorkOrderMirror();
|
||
|
||
PlantingDetail::query()->create([
|
||
'company_id' => $this->company->id,
|
||
'bill_date' => '2026-09-10',
|
||
'order_no' => 'MANUAL-ONLY',
|
||
'finance_payment_amount' => '9.0000',
|
||
]);
|
||
|
||
$result = app(PlantingWorkOrderSynchronizer::class)
|
||
->sync('2026-09-01', '2026-09-30', $this->company->id, false, true);
|
||
|
||
$this->assertSame(1, $result['deleted']);
|
||
$this->assertSame(2, $result['inserted']);
|
||
$this->assertSame(0, PlantingDetail::query()->where('company_id', $this->company->id)
|
||
->where('order_no', 'MANUAL-ONLY')->count());
|
||
$this->assertSame(2, PlantingDetail::query()->where('company_id', $this->company->id)->count());
|
||
}
|
||
|
||
/** 造一个只有聚水潭协同工单镜像的 sqlite 业务镜像连接。 */
|
||
private function fakeWorkOrderMirror(): void
|
||
{
|
||
config(['upstream_mirrors.connection' => 'business_mirror']);
|
||
config(['database.connections.business_mirror' => [
|
||
'driver' => 'sqlite',
|
||
'database' => ':memory:',
|
||
'prefix' => '',
|
||
]]);
|
||
DB::purge('business_mirror');
|
||
|
||
Schema::connection('business_mirror')->create('js_jst_work_order_info', function ($table): void {
|
||
$table->increments('Id');
|
||
$table->string('woNo')->nullable();
|
||
$table->string('woTypeId')->nullable();
|
||
$table->string('status')->nullable();
|
||
$table->string('createby')->nullable();
|
||
$table->string('completeTime')->nullable();
|
||
$table->text('businessFieldList')->nullable();
|
||
});
|
||
|
||
// 店铺维度报表用的 ERP 店铺资料:只给第二张工单的店铺,用来验证「历史没有就用 ERP 补」
|
||
Schema::connection('business_mirror')->create('ops_fys_shop_info', function ($table): void {
|
||
$table->increments('Id');
|
||
$table->string('erpShopName')->nullable();
|
||
$table->string('responsibleDepartment')->nullable();
|
||
});
|
||
DB::connection('business_mirror')->table('ops_fys_shop_info')->insert([
|
||
['erpShopName' => '拼多多-归来舍下住宅家具旗舰店', 'responsibleDepartment' => '运营中心G'],
|
||
]);
|
||
|
||
foreach ($this->workOrderFixtures() as $row) {
|
||
DB::connection('business_mirror')->table('js_jst_work_order_info')->insert($row);
|
||
}
|
||
}
|
||
|
||
/** @return list<array<string,mixed>> */
|
||
private function workOrderFixtures(): array
|
||
{
|
||
$planting = PlantingWorkOrderSynchronizer::WORK_TYPE_ID;
|
||
|
||
$fields = [
|
||
['fieldId' => 'orderField', 'type' => 'TEXT', 'value' => '3316392626048007769'],
|
||
['fieldId' => 'shopField', 'type' => 'SELECT', 'value' => '18501511', 'valueLabel' => '天猫-亿家达旗舰店'],
|
||
['fieldId' => 'NUMBERField1413856620250924131615', 'type' => 'NUMBER', 'value' => '37.79'],
|
||
['fieldId' => 'NUMBERField1413856620251230154457', 'type' => 'NUMBER', 'value' => '2'],
|
||
['fieldId' => 'NUMBERField1413856620251117101250', 'type' => 'NUMBER', 'value' => '37.79'],
|
||
['fieldId' => 'TEXTField1413856620251117180931', 'type' => 'TEXT', 'value' => '黄文梳'],
|
||
['fieldId' => 'TEXTField1413856620251117113451', 'type' => 'TEXT', 'value' => '6214830551373771'],
|
||
['fieldId' => 'TEXTField1413856620251113150546', 'type' => 'TEXT', 'value' => 'CCDD'],
|
||
['fieldId' => 'businessRemarkField', 'type' => 'TEXTAREA', 'value' => 'CCDD'],
|
||
['fieldId' => 'itemInfo', 'type' => 'FORM', 'childFieldList' => [[
|
||
['fieldId' => 'erpItemId', 'type' => 'TEXT', 'value' => '11961631'],
|
||
['fieldId' => 'platformItemId', 'type' => 'TEXT', 'value' => '1074872715022'],
|
||
]]],
|
||
];
|
||
|
||
$plain = [
|
||
['fieldId' => 'orderField', 'type' => 'TEXT', 'value' => '260901-000000000000001'],
|
||
['fieldId' => 'shopField', 'type' => 'SELECT', 'value' => '21355501', 'valueLabel' => '拼多多-归来舍下住宅家具旗舰店'],
|
||
['fieldId' => 'NUMBERField1413856620250924131615', 'type' => 'NUMBER', 'value' => '0.06'],
|
||
['fieldId' => 'NUMBERField1413856620251230154457', 'type' => 'NUMBER', 'value' => '0.5'],
|
||
['fieldId' => 'NUMBERField1413856620251117101250', 'type' => 'NUMBER', 'value' => '0.56'],
|
||
];
|
||
|
||
return [
|
||
[
|
||
'woNo' => 'GD565967381', 'woTypeId' => $planting, 'status' => 'CONFIRMED',
|
||
'createby' => '黄文梳', 'completeTime' => '2026-09-02 11:47:53',
|
||
'businessFieldList' => json_encode($fields, JSON_UNESCAPED_UNICODE),
|
||
],
|
||
[
|
||
'woNo' => 'GD999999999', 'woTypeId' => $planting, 'status' => 'CONFIRMED',
|
||
'createby' => '陆小凤', 'completeTime' => '2026-09-30 09:27:29',
|
||
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
|
||
],
|
||
// 未完成 → 忽略
|
||
[
|
||
'woNo' => 'GD888888888', 'woTypeId' => $planting, 'status' => 'WAIT_CONFIRM',
|
||
'createby' => '陆小凤', 'completeTime' => '2026-09-15 09:27:29',
|
||
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
|
||
],
|
||
// 别的工单类型 → 忽略
|
||
[
|
||
'woNo' => 'GD777777777', 'woTypeId' => 'ORDER_AFSALES', 'status' => 'CONFIRMED',
|
||
'createby' => '关雎', 'completeTime' => '2026-09-15 09:27:29',
|
||
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
|
||
],
|
||
// 完成时间 10/2 → 日期 10/1,不在 9 月窗口 → 忽略
|
||
[
|
||
'woNo' => 'GD666666666', 'woTypeId' => $planting, 'status' => 'CONFIRMED',
|
||
'createby' => '陆小凤', 'completeTime' => '2026-10-02 09:27:29',
|
||
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
|
||
],
|
||
];
|
||
}
|
||
|
||
private function csvFixture(): string
|
||
{
|
||
$path = $this->writeFile('planting.csv');
|
||
$rows = [
|
||
['日期', '订单编号', '财务售价', '财务运费', '财务佣金', '京喜差价', '扣款', '财务空包费', '财务打款金额', '笔数', '备注', '正确店铺', '负责人', '账户', '类型', '创建人', '标签', '所属利润中心', '负责平台', 'ID'],
|
||
['2026-06-01', '3306215028042824171', '2665.35', '0', '0', '0', '0', '0', '2665.35', '1', '', '阿里巴巴-常州乐畅家居有限公司', '叶颖龙', '6222031202029761854', '', '勇武', 'AABB', '运营中心B', '', '1056008452997'],
|
||
['2026-07-15', '260529-659019537933390', '0.01', '0', '0.5', '0', '0', '0', '0.51', '1', '', '拼多多-亿家达实木家具官方旗舰店', '屠锦超', '6222031211003375839', 'SSDD', '玄鸟', 'SSDD', '运营中心G', '', '959767499187'],
|
||
];
|
||
|
||
$handle = fopen($path, 'wb');
|
||
foreach ($rows as $row) {
|
||
fputcsv($handle, $row);
|
||
}
|
||
fclose($handle);
|
||
|
||
return $path;
|
||
}
|
||
|
||
private function writeFile(string $name): string
|
||
{
|
||
$path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('planting-', true) . '-' . $name;
|
||
$this->files[] = $path;
|
||
|
||
return $path;
|
||
}
|
||
|
||
private function allowPage(): void
|
||
{
|
||
$this->user->attachPermission(
|
||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||
);
|
||
}
|
||
}
|