CaiWuXiTong/modules/EcommerceFinance/View/PlantingLossPageData.php
limeng 4e174963e8 fix(种菜损耗): 透明挂钩/虚拟条码整单不扣款;拍A发B 从种菜报销直接挑出发运营
财务确认(2026-10-20):种菜损耗是查种菜报销的单有没有真实发出有价值的货——
- 发货数据中交易单号下出现信誉订单虚拟条码或透明挂钩(含条形码 GG 开头)即整单不扣款;
- 任务备注/卖家备注含拍A发B/拍A发A 的不扣款,且无论是否匹配到发货都从种菜报销中挑出发运营;
- “导出拍A发B(发运营)”直接按「种菜报销」页字段输出;其余真实发货的单按成本价×数量+运费扣款。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VANWYsNUbu2piMtCKymoyd
2026-10-20 10:10:29 +08:00

136 lines
6.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\View;
use Illuminate\Pagination\LengthAwarePaginator;
use Modules\EcommerceFinance\Repositories\PlantingReimbursementRepository as Repository;
use Modules\EcommerceFinance\Services\PlantingLossCalculator as Calculator;
/** 种菜数据汇总 → 种菜损耗:种菜报销明细匹配近两个月发货数据,拍A发B/拍A发A、只发虚拟条码/透明挂钩的不扣款,其余按成本价×数量+运费扣款。 */
class PlantingLossPageData
{
use PlantingPageFilters;
public const TITLE = '种菜损耗';
public const PER_PAGE = 50;
public const TEXT_FILTERS = [
'work_order_no' => '工单编号',
'online_order_no' => '线上订单号',
'shop_name' => '店铺名称',
'task_remark' => '任务备注',
'seller_remark' => '卖家备注',
'express_no' => '快递单号',
];
public const COLUMNS = [
'loss_result' => '损耗判定', 'deduction_amount' => '扣款金额', 'loss_basis' => '判定说明', 'deduction_detail' => '扣款计算(条码×数量×成本价+运费)',
'completed_at' => '报销完成时间', 'work_order_no' => '工单编号', 'platform' => '平台', 'shop_name' => '店铺名称',
'online_order_no' => '线上订单号', 'task_remark' => '任务备注', 'seller_remark' => '卖家备注',
'actual_paid_amount' => '实付金额', 'task_payment' => '任务付款金额', 'reimbursement_amount' => '最终报销金额',
'shipped_at' => '发货时间', 'express_no' => '快递单号', 'warehouse_name' => '发货仓库',
'shipped_barcode' => '发货条码', 'shipped_product_name' => '发货商品名称', 'shipped_qty' => '发货数量',
'shipped_amount' => '发货金额', 'shipped_freight' => '发货运费',
'initiator' => '发起人', 'payee_name' => '信誉账号名称', 'product_code' => '报销商品编码', 'product_name' => '报销商品名称',
'internal_order_no' => '内部单号', 'erp_order_status' => 'ERP订单状态',
];
public const MONEY_COLUMNS = ['deduction_amount', 'actual_paid_amount', 'task_payment', 'reimbursement_amount', 'shipped_amount', 'shipped_freight'];
private const SUM_KEYS = ['deduction_amount', 'task_payment', 'reimbursement_amount', 'shipped_amount', 'shipped_freight'];
public function __construct(
private readonly Calculator $calculator,
private readonly Repository $repository,
) {}
/** @return array<string,string> */
public function filters(): array
{
$filters = $this->baseFilters();
$result = request()->query('result', Calculator::RESULT_DEDUCT);
abort_unless(is_string($result) && ($result === 'all' || in_array($result, Calculator::RESULTS, true)), 422, '损耗判定无效。');
$filters['result'] = $result;
return $filters;
}
/**
* @param array<string,string> $filters
* @param list<array<string,mixed>>|null $rows
* @return \Generator<int,array<string,mixed>>
*/
public function rows(array $filters, ?array $rows = null): \Generator
{
foreach ($rows ?? $this->calculator->rows($filters['month']) as $row) {
if ($filters['result'] !== 'all' && $row['loss_result'] !== $filters['result']) {
continue;
}
if ($this->matchesText($row, $filters)) {
yield $row;
}
}
}
public function page(): array
{
$filters = $this->filters();
$page = max(1, (int) request('page', 1));
$sync = $this->repository->syncStatus();
$shippingSync = $this->calculator->shippingStatus();
$empty = ['count' => 0, 'cost_missing' => 0] + array_fill_keys(self::SUM_KEYS, 0);
$summary = ['total' => 0, 'matched' => 0, 'filtered' => $empty] + array_fill_keys(Calculator::RESULTS, $empty);
$items = [];
$error = $this->syncError($sync);
if ($error === null) {
try {
$all = $this->calculator->rows($filters['month']);
foreach ($all as $row) {
$summary['total']++;
$summary['matched'] += ($row['shipped_at'] ?? '') !== '' ? 1 : 0;
$this->add($summary[$row['loss_result']], $row);
}
foreach ($this->rows($filters, $all) as $row) {
if ($summary['filtered']['count'] >= ($page - 1) * self::PER_PAGE && count($items) < self::PER_PAGE) {
$items[] = $row;
}
$this->add($summary['filtered'], $row);
}
} catch (\Throwable $exception) {
report($exception);
$error = '种菜损耗暂时无法计算(工单镜像或发货数据读取失败),请稍后重试。';
$summary = ['total' => 0, 'matched' => 0, 'filtered' => $empty] + array_fill_keys(Calculator::RESULTS, $empty);
$items = [];
}
}
return [
'view' => 'business.pages.billing-transactions.planting-loss',
'plantingFilters' => $filters,
'plantingWindow' => Repository::window($filters['month']),
'plantingShippingWindow' => Calculator::shippingWindow($filters['month']),
'plantingMonths' => Repository::months(),
'plantingSummary' => $summary,
'plantingSync' => $sync,
'plantingShippingSync' => $shippingSync,
'plantingError' => $error,
'plantingRows' => new LengthAwarePaginator($items, $summary['filtered']['count'], self::PER_PAGE, $page, [
'path' => request()->url(),
'query' => request()->except('page'),
]),
];
}
/** @param array<string,int> $bucket @param array<string,mixed> $row */
private function add(array &$bucket, array $row): void
{
$bucket['count']++;
$bucket['cost_missing'] += ! empty($row['cost_missing']) ? 1 : 0;
foreach (self::SUM_KEYS as $key) {
$bucket[$key] += self::cents($row[$key] ?? null);
}
}
}