- 种菜数据汇总新增「种菜报销工作台」:按聚工单锁单口径取单(待处理 WAIT_CONFIRM、付款 D-7~D-1、ERP订单状态非取消/删除/空、售后单号为空;可切换处理中 CONFIRMING 和付款起始日),逐单校验重复报销、金额公式、佣金资格、收款人在职、以旧换新买家实付(取买家实付/看板订单实付中最低,查不到标签的待复查)、看板超额(同日按付款先后报到日限额)与看板未匹配 - 导出前须勾选「已在聚工单核对条数和金额一致」;页面给出应核对的条数/金额、各口径合计自检,聚工单镜像停更超过 20 分钟时提示先别报销 - 页面给出可报销合计(三大天猫/其他)、钉钉「种菜报销流程」明细行、按发起人的发群通知与取消清单 - 导出种菜对账、种菜报销明细(打款卡号明细表)、聚工单种菜任务工单导入模版,格式对齐手工文件 - 财务可上传种菜监控看板、报销佣金人员、花名册在职、店铺信息;名单存 storage 持久卷 JSON,花名册只存姓名 - 聚工单镜像读取补充 ERP订单标签、待报销工单和已报销订单查询 涉及:只读聚工单镜像;不涉及迁移、上游写入、钉钉和聚工单写操作。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VANWYsNUbu2piMtCKymoyd
120 lines
5.7 KiB
PHP
120 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\View;
|
|
|
|
use InvalidArgumentException;
|
|
use Modules\EcommerceFinance\Repositories\PlantingReimbursementRepository as Repository;
|
|
use Modules\EcommerceFinance\Services\PlantingReimbursementAuditor as Auditor;
|
|
use Modules\EcommerceFinance\Services\PlantingReimbursementReferences as References;
|
|
use Modules\EcommerceFinance\Services\PlantingReimbursementWorkbench as Workbench;
|
|
|
|
/** 种菜数据汇总 → 种菜报销工作台:待报销种菜工单逐单校验,出打款明细、钉钉流程明细和要取消的工单。 */
|
|
class PlantingReimbursementWorkbenchPageData
|
|
{
|
|
public const TITLE = '种菜报销工作台';
|
|
|
|
/** 聚工单镜像超过这么久没同步成功就提示数据不是最新的。 */
|
|
public const STALE_MINUTES = 20;
|
|
|
|
public const DECISION_FILTERS = ['all' => '全部', Auditor::DECISION_PASS => '可报销', Auditor::DECISION_CANCEL => '取消工单', Auditor::DECISION_RECHECK => '待复查'];
|
|
|
|
public const COLUMNS = [
|
|
'decision_label' => '处理结果', 'issue_text' => '原因', 'work_order_no' => '工单编号', 'status' => '工单状态',
|
|
'paid_at' => '付款时间', 'initiator' => '发起人', 'shop_name' => '店铺', 'profit_center' => '所属利润中心',
|
|
'online_order_no' => '线上订单号', 'task_payment' => '任务付款', 'task_freight' => '快递费', 'task_commission' => '佣金',
|
|
'jingxi_price_diff' => '京喜差价', 'task_deduction' => '扣款', 'task_empty_package_fee' => '空包费',
|
|
'reimbursement_amount' => '最终报销金额', 'actual_paid_amount' => '买家实付', 'payee_name' => '收款人',
|
|
'payee_account' => '账号', 'payee_bank' => '开户行', 'erp_order_label' => 'ERP订单标签', 'task_remark' => '任务备注',
|
|
];
|
|
|
|
public const MONEY_COLUMNS = [
|
|
'task_payment', 'task_freight', 'task_commission', 'jingxi_price_diff', 'task_deduction',
|
|
'task_empty_package_fee', 'reimbursement_amount', 'actual_paid_amount',
|
|
];
|
|
|
|
public function __construct(
|
|
private readonly Workbench $workbench,
|
|
private readonly Repository $repository,
|
|
private readonly References $references,
|
|
) {}
|
|
|
|
/** @return array{date:string,paid_from:string,scope:string,decision:string} */
|
|
public function filters(): array
|
|
{
|
|
$date = trim((string) request('date', '')) ?: Workbench::defaultDate();
|
|
try {
|
|
$paidFrom = trim((string) request('paid_from', '')) ?: Workbench::defaultPaidFrom($date);
|
|
Workbench::paidWindow($date, $paidFrom);
|
|
} catch (InvalidArgumentException $exception) {
|
|
abort(422, $exception->getMessage());
|
|
}
|
|
$scope = (string) request('scope', '') ?: 'pending';
|
|
abort_unless(isset(Workbench::SCOPES[$scope]), 422, '取单范围无效。');
|
|
$decision = (string) request('decision', '') ?: 'all';
|
|
abort_unless(isset(self::DECISION_FILTERS[$decision]), 422, '处理结果筛选无效。');
|
|
|
|
return ['date' => $date, 'paid_from' => $paidFrom, 'scope' => $scope, 'decision' => $decision];
|
|
}
|
|
|
|
/** @param array{date:string,paid_from:string,scope:string} $filters */
|
|
public function build(array $filters): array
|
|
{
|
|
return $this->workbench->build((int) company_id(), $filters['date'], $filters['paid_from'], Workbench::scopeStatuses($filters['scope']));
|
|
}
|
|
|
|
public function page(): array
|
|
{
|
|
$filters = $this->filters();
|
|
$sync = $this->repository->syncStatus();
|
|
$result = null;
|
|
$error = null;
|
|
|
|
if (! in_array($sync['code'], ['ready', 'failed'], true)) {
|
|
$error = '聚工单镜像未就绪,暂时无法生成报销批次。';
|
|
} else {
|
|
try {
|
|
$result = $this->build($filters);
|
|
} catch (\Throwable $exception) {
|
|
report($exception);
|
|
$error = '种菜报销工作台数据暂时无法读取,请稍后重试。';
|
|
}
|
|
}
|
|
|
|
$rows = $result['rows'] ?? [];
|
|
if ($filters['decision'] !== 'all') {
|
|
$rows = array_values(array_filter($rows, static fn (array $row): bool => $row['decision'] === $filters['decision']));
|
|
}
|
|
// 先看要处理的:取消、待复查在前。
|
|
$order = [Auditor::DECISION_CANCEL => 0, Auditor::DECISION_RECHECK => 1, Auditor::DECISION_PASS => 2];
|
|
usort($rows, static fn (array $a, array $b): int => [$order[$a['decision']], $a['initiator'], $a['paid_at']] <=> [$order[$b['decision']], $b['initiator'], $b['paid_at']]);
|
|
|
|
// 镜像停更超过 20 分钟或最近一次同步失败:数据不是最新的,提醒先别报销。
|
|
$syncStale = $sync['code'] !== 'ready' || $sync['last_succeeded_at'] === null
|
|
|| now()->diffInMinutes(\Illuminate\Support\Carbon::parse($sync['last_succeeded_at']), true) > self::STALE_MINUTES;
|
|
|
|
return [
|
|
'view' => 'business.pages.billing-transactions.planting-reimbursement-workbench',
|
|
'workbenchSyncStale' => $syncStale,
|
|
'workbenchFilters' => $filters,
|
|
'workbenchWindow' => Workbench::paidWindow($filters['date'], $filters['paid_from']),
|
|
'workbenchSync' => $sync,
|
|
'workbenchError' => $error,
|
|
'workbenchSummary' => $result['summary'] ?? null,
|
|
'workbenchRows' => $rows,
|
|
'workbenchUploads' => $result['uploads'] ?? $this->safeUploads(),
|
|
];
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
private function safeUploads(): array
|
|
{
|
|
try {
|
|
return $this->references->uploads((int) company_id());
|
|
} catch (\Throwable $exception) {
|
|
report($exception);
|
|
|
|
return array_fill_keys(array_keys(References::KINDS), null);
|
|
}
|
|
}
|
|
}
|