新增店铺收款唯品会JX账户页签
店铺收款账户账单新增 vipshop-jx 页签:导航配置、列定义、控制器与前端路由;读取共享账单表 ec_account_bills 中的唯品会JX(JITX)原始账单(店铺/类型/二次备注/销售订单号/含税金额),按金额正负拆出收入与支出,订单日期由销售订单号前 6 位还原(原始账单无日期列)。DB 页签日期统一按 Y-m-d H:i:s 返回,JX 页签汇总口径为收支净额,并补齐单元与功能测试。
This commit is contained in:
parent
e73183f52e
commit
a08b28199d
@ -409,6 +409,7 @@ $navigation = [
|
||||
['slug' => 'douyin', 'title' => '抖音账户'],
|
||||
['slug' => 'dewu', 'title' => '得物账户'],
|
||||
['slug' => 'vipshop', 'title' => '唯品会MP账户'],
|
||||
['slug' => 'vipshop-jx', 'title' => '唯品会JX账户'],
|
||||
],
|
||||
],
|
||||
[
|
||||
|
||||
@ -58,6 +58,23 @@ class AccountBillColumns
|
||||
['商家应收(G=E-F-G)', 'balance', 'money'],
|
||||
],
|
||||
],
|
||||
|
||||
// 唯品会JX(JITX 入仓/经销)账单只有 5 列原始字段(店铺/类型/二次备注/销售订单号/含税金额),
|
||||
// 没有日期列;订单日期由销售订单号前 6 位(YYMMDD)还原,其余列由金额正负拆出收/支。
|
||||
'vipshop-jx' => [
|
||||
'title' => '唯品会JX账户',
|
||||
'columns' => [
|
||||
['记录编码', 'record_code', 'num'],
|
||||
['店铺', 'store_name', 'text'],
|
||||
['类型', 'bill_item', 'tag'],
|
||||
['二次备注', 'payload.second_remark', 'text'],
|
||||
['销售订单号', 'order_no', 'num'],
|
||||
['订单日期(按订单号)', 'occurred_at', 'date'],
|
||||
['含税金额', 'amount', 'money'],
|
||||
['收入金额', 'income', 'money_in'],
|
||||
['支出金额', 'expense', 'money_out'],
|
||||
],
|
||||
],
|
||||
'taobao-wechat' => [
|
||||
'title' => '淘系微信账户',
|
||||
'groups' => [
|
||||
|
||||
@ -103,11 +103,19 @@ class AccountBills extends Controller
|
||||
->value('balance');
|
||||
|
||||
$data = $rows->toArray();
|
||||
$data['data'] = collect($rows->items())
|
||||
->map(fn (AccountBill $bill): array => $this->billRow($bill))
|
||||
->all();
|
||||
$income = (float) ($summaryRow->income ?? 0);
|
||||
$expense = (float) ($summaryRow->expense ?? 0);
|
||||
$data['summary'] = [
|
||||
'income' => (float) ($summaryRow->income ?? 0),
|
||||
'expense' => (float) ($summaryRow->expense ?? 0),
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'count' => (int) ($summaryRow->count ?? 0),
|
||||
'balance' => $latestBalance !== null ? (float) $latestBalance : 0,
|
||||
// 唯品会JX账单没有账户余额列,该页签以收支净额作为汇总口径。
|
||||
'balance' => $billType === 'vipshop-jx'
|
||||
? round($income - $expense, 2)
|
||||
: ($latestBalance !== null ? (float) $latestBalance : 0),
|
||||
];
|
||||
$data['columns'] = AccountBillColumns::for($billType);
|
||||
|
||||
@ -144,7 +152,7 @@ class AccountBills extends Controller
|
||||
{
|
||||
$this->authorizeStoreReceivingBills();
|
||||
|
||||
if ($bill_type === 'vipshop') {
|
||||
if (in_array($bill_type, ['vipshop', 'vipshop-jx'], true)) {
|
||||
$query = AccountBill::query()
|
||||
->where('company_id', company_id())
|
||||
->where('bill_type', $bill_type);
|
||||
@ -185,6 +193,20 @@ class AccountBills extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/** 账单表格按 Y-m-d H:i:s 展示日期,避免 Eloquent 默认序列化成 ISO8601(UTC)。 */
|
||||
private function billRow(AccountBill $bill): array
|
||||
{
|
||||
return collect($bill->toArray())
|
||||
->map(function (mixed $value, string $field) use ($bill): mixed {
|
||||
$attribute = $bill->getAttribute($field);
|
||||
|
||||
return $attribute instanceof \DateTimeInterface
|
||||
? $attribute->format('Y-m-d H:i:s')
|
||||
: $value;
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
private function exportQuery(Request $request, $query, string $billType)
|
||||
{
|
||||
$definition = AccountBillColumns::for($billType);
|
||||
|
||||
@ -84,7 +84,7 @@ export default {
|
||||
// Every account-bill tab uses the same explicit picker path. Keeping one
|
||||
// control avoids browser-native date inputs appearing on older tabs.
|
||||
usesStableDateControl(){return true;},
|
||||
usesMirrorOptions(){return ['douyin','pinduoduo','dewu','taobao-wechat','jd-transactions','jd-wallet','vipshop'].indexOf(this.billType)!==-1;},
|
||||
usesMirrorOptions(){return ['douyin','pinduoduo','dewu','taobao-wechat','jd-transactions','jd-wallet','vipshop','vipshop-jx'].indexOf(this.billType)!==-1;},
|
||||
invalidDateRange(){return Boolean(this.filters.start&&this.filters.end&&this.filters.start>this.filters.end);},
|
||||
startDateConfig(){return {...dateConfig,maxDate:this.filters.end||null};},
|
||||
endDateConfig(){return {...dateConfig,minDate:this.filters.start||null};},
|
||||
@ -92,7 +92,7 @@ export default {
|
||||
canExport(){return !this.loading&&!this.loadError&&!this.invalidDateRange&&!this.filtersDirty&&this.total>0&&(this.exportMode==='filtered-all'||this.visibleColumns.length>0);},
|
||||
visibleColumns(){return this.configuredColumns.filter(column=>column.visible);},
|
||||
visibleGroups(){return this.visibleColumns.reduce((groups,column)=>{if(!column.group)return groups;const last=groups[groups.length-1];if(last&&last.label===column.group)last.span+=1;else groups.push({label:column.group,span:1});return groups;},[]);},
|
||||
stats(){return[{label:'收入合计',value:this.money(this.summary.income),note:`${this.summary.count} 笔交易`,className:'is-income'},{label:'支出合计',value:this.money(this.summary.expense),note:'按当前筛选',className:'is-expense'},{label:this.billType==='jd-transactions'?'收支净额':(this.billType==='jd-wallet'?'账户余额合计(最新)':'期末余额(最新)'),value:this.money(this.summary.balance),note:this.billType==='jd-transactions'?'按当前筛选':(this.billType==='jd-wallet'?'筛选结果中每个店铺账户最新一笔':'最新一条记录'),className:''},{label:'交易笔数',value:this.summary.count,note:'当前筛选结果',className:''}];},
|
||||
stats(){const netLabel=['jd-transactions','vipshop-jx'].indexOf(this.billType)!==-1;return[{label:'收入合计',value:this.money(this.summary.income),note:`${this.summary.count} 笔交易`,className:'is-income'},{label:'支出合计',value:this.money(this.summary.expense),note:'按当前筛选',className:'is-expense'},{label:netLabel?'收支净额':(this.billType==='jd-wallet'?'账户余额合计(最新)':'期末余额(最新)'),value:this.money(this.summary.balance),note:netLabel?'按当前筛选':(this.billType==='jd-wallet'?'筛选结果中每个店铺账户最新一笔':'最新一条记录'),className:''},{label:'交易笔数',value:this.summary.count,note:'当前筛选结果',className:''}];},
|
||||
rangeStart(){return this.total===0?0:(this.currentPage-1)*this.perPage+1;},rangeEnd(){return Math.min(this.currentPage*this.perPage,this.total);}
|
||||
},
|
||||
mounted(){if(this.usesMirrorOptions)this.fetchFilterOptions();this.fetch();},
|
||||
|
||||
6
resources/assets/js/views/business/pages.js
vendored
6
resources/assets/js/views/business/pages.js
vendored
@ -5,7 +5,7 @@
|
||||
*
|
||||
* 店铺收款账户账单(store-receiving-bills)下各 tab:
|
||||
* - taobao-alipay → 独立实现 TaobaoAlipayBills(经营分析副本共享镜像)
|
||||
* - taobao-wechat / pinduoduo / jd-wallet / jd-transactions / dewu
|
||||
* - taobao-wechat / pinduoduo / jd-wallet / jd-transactions / dewu / vipshop / vipshop-jx
|
||||
* → 通用实现 AccountBills(表 ec_account_bills,按 bill_type 区分)
|
||||
* - douyin → AccountBills 展示共享原始账单镜像中的头条放心购渠道
|
||||
*/
|
||||
@ -16,8 +16,8 @@ import Vue from 'vue';
|
||||
import TaobaoAlipayBills from './../../components/business/TaobaoAlipayBills.vue';
|
||||
import AccountBills from './../../components/business/AccountBills.vue';
|
||||
|
||||
const GENERIC_TABS = ['taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop'];
|
||||
const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop'];
|
||||
const GENERIC_TABS = ['taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop', 'vipshop-jx'];
|
||||
const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop', 'vipshop-jx'];
|
||||
|
||||
function pageContext(pathname = window.location.pathname) {
|
||||
const parts = (pathname || '').split('/').filter(Boolean);
|
||||
|
||||
94
tests/Feature/Business/VipshopJxReceivingBillsTest.php
Normal file
94
tests/Feature/Business/VipshopJxReceivingBillsTest.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Modules\EcommerceFinance\Models\AccountBill;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
/**
|
||||
* 店铺收款账户账单 › 唯品会JX账户(bill_type = vipshop-jx,读取 ec_account_bills)。
|
||||
*/
|
||||
class VipshopJxReceivingBillsTest extends FeatureTestCase
|
||||
{
|
||||
public function test_vipshop_jx_tab_reads_only_its_own_bill_type(): void
|
||||
{
|
||||
$this->allowPage();
|
||||
$this->bill('vipshop-jx', '26060921475309', '销售', '2026-06-09 00:00:00', 82.48, 0);
|
||||
$this->bill('vipshop-jx', '26070393670736', '客退', '2026-07-03 00:00:00', 0, 68.60);
|
||||
$this->bill('vipshop', '26060100000001', '订单退款', '2026-06-01 00:00:00', 0, 999.00);
|
||||
|
||||
$response = $this->loginAs()
|
||||
->getJson(route('ecommerce-finance.account-bills.index', [
|
||||
'bill_type' => 'vipshop-jx',
|
||||
'limit' => 20,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertJsonPath('columns.title', '唯品会JX账户')
|
||||
->assertJsonPath('total', 2)
|
||||
->assertJsonPath('summary.count', 2)
|
||||
->assertJsonPath('summary.income', 82.48)
|
||||
->assertJsonPath('summary.expense', 68.60)
|
||||
// 该页签没有账户余额列,汇总口径为收支净额。
|
||||
->assertJsonPath('summary.balance', 13.88)
|
||||
// 日期按 Y-m-d H:i:s 返回,前端直接展示,不再出现 ISO8601(UTC)。
|
||||
->assertJsonPath('data.0.occurred_at', '2026-07-03 00:00:00')
|
||||
->assertJsonPath('data.1.occurred_at', '2026-06-09 00:00:00');
|
||||
|
||||
$this->assertSame(
|
||||
['记录编码', '店铺', '类型', '二次备注', '销售订单号', '订单日期(按订单号)', '含税金额', '收入金额', '支出金额'],
|
||||
array_column($response->json('columns.columns'), 'label'),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_vipshop_jx_filter_options_and_export_stay_inside_the_tab(): void
|
||||
{
|
||||
$this->allowPage();
|
||||
$this->bill('vipshop-jx', '26060921475309', '销售', '2026-06-09 00:00:00', 82.48, 0);
|
||||
$this->bill('vipshop-jx', '26070393670736', '客退', '2026-07-03 00:00:00', 0, 68.60);
|
||||
$this->bill('vipshop', '26060100000001', '订单退款', '2026-06-01 00:00:00', 0, 999.00);
|
||||
|
||||
$this->loginAs()
|
||||
->getJson(route('ecommerce-finance.account-bills.filter-options', ['bill_type' => 'vipshop-jx']))
|
||||
->assertOk()
|
||||
->assertExactJson([
|
||||
'shops' => ['唯品会JITX_测试店'],
|
||||
'bill_items' => ['客退', '销售'],
|
||||
]);
|
||||
|
||||
$export = $this->loginAs()->get(route('ecommerce-finance.account-bills.export', [
|
||||
'bill_type' => 'vipshop-jx',
|
||||
'bill_item' => '销售',
|
||||
]));
|
||||
|
||||
$export->assertOk()->assertDownload('vipshop-jx_account_bills.csv');
|
||||
$csv = $export->streamedContent();
|
||||
$this->assertStringContainsString('26060921475309', $csv);
|
||||
$this->assertStringNotContainsString('26070393670736', $csv);
|
||||
$this->assertStringNotContainsString('999.00', $csv);
|
||||
}
|
||||
|
||||
private function bill(string $billType, string $orderNo, string $item, string $occurredAt, float $income, float $expense): void
|
||||
{
|
||||
AccountBill::create([
|
||||
'company_id' => $this->company->id,
|
||||
'bill_type' => $billType,
|
||||
'record_code' => $orderNo . '-' . $item,
|
||||
'store_name' => $billType === 'vipshop-jx' ? '唯品会JITX_测试店' : '唯品会MP-测试店',
|
||||
'order_no' => $orderNo,
|
||||
'amount' => $income - $expense,
|
||||
'bill_item' => $item,
|
||||
'occurred_at' => $occurredAt,
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'payload' => ['second_remark' => '订单'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function allowPage(): void
|
||||
{
|
||||
$this->user->attachPermission(
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ class AccountBillsTableTest extends TestCase
|
||||
$this->assertStringContainsString("usesStableDateControl(){return true;}", $source);
|
||||
$this->assertStringContainsString("{ 'is-stable-date': usesStableDateControl }", $source);
|
||||
$this->assertStringContainsString('.tw-date.is-stable-date .tw-date-control', $source);
|
||||
$this->assertStringContainsString("['douyin','pinduoduo','dewu','jd-transactions','jd-wallet','vipshop'].indexOf(this.billType)!==-1", $source);
|
||||
$this->assertStringContainsString("['douyin','pinduoduo','dewu','taobao-wechat','jd-transactions','jd-wallet','vipshop','vipshop-jx'].indexOf(this.billType)!==-1", $source);
|
||||
$this->assertStringContainsString('v-if="usesMirrorOptions && !filterOptionsError"', $source);
|
||||
$this->assertStringContainsString('page-key="store-receiving-bills"', $source);
|
||||
$this->assertStringContainsString(':tab-key="billType"', $source);
|
||||
@ -24,4 +24,18 @@ class AccountBillsTableTest extends TestCase
|
||||
$this->assertStringContainsString('position:sticky', $source);
|
||||
$this->assertStringContainsString('overflow:auto', $source);
|
||||
}
|
||||
|
||||
public function test_store_receiving_tabs_route_the_vipshop_jx_account(): void
|
||||
{
|
||||
$source = file_get_contents(__DIR__ . '/../../../resources/assets/js/views/business/pages.js');
|
||||
|
||||
$this->assertStringContainsString(
|
||||
"const GENERIC_TABS = ['taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop', 'vipshop-jx'];",
|
||||
$source,
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
"const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop', 'vipshop-jx'];",
|
||||
$source,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user