CaiWuXiTong/modules/EcommerceFinance/View/PlantingLossPageData.php
limeng 4c617fd063 fix(种菜损耗): 扣款金额改为 成本价×数量 + 该单发货运费
财务更正口径:种菜损耗扣款 = Σ 发货条码在普通商品资料的成本价 × 数量 + 该订单在发货数据中的运费合计,
不再按成本价×1.2;运费为空的订单标"运费缺失",缺失部分不计入扣款金额。

涉及:金额计算展示(只读);不涉及迁移、权限和上游写入。

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

135 lines
6.0 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, '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']++;
$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, '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);
}
}
}