财务更正口径:厂家记账配置表中没有的供应商(含配置不在生效期内)视同正常记账,作为厂家扣款项; 取消“待配置”判定。工单未带供应商、或多供应商判定不一致的仍为“待确认”,不计入扣款。 涉及:扣款金额汇总口径;无数据库变更。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JouFR6TJ7vGscRJanYScEo
187 lines
9.6 KiB
PHP
187 lines
9.6 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\View;
|
|
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Modules\EcommerceFinance\Repositories\SupplierBookkeepingWorkOrderRepository as Repository;
|
|
use Modules\EcommerceFinance\Services\SupplierBookkeepingConfigStore;
|
|
use Modules\EcommerceFinance\Services\SupplierBookkeepingDeductionRule as Rule;
|
|
|
|
/** 供应商应付 → 厂家记账(聚工单“记账”工单镜像)。 */
|
|
class SupplierBookkeepingPageData
|
|
{
|
|
public const TITLE = '厂家记账';
|
|
|
|
public const PER_PAGE = 50;
|
|
|
|
public const TEXT_FILTERS = [
|
|
'work_order_no' => '工单编号',
|
|
'online_order_no' => '线上订单号',
|
|
'tracking_no' => '物流单号',
|
|
'shop_name' => '店铺名称',
|
|
'supplier' => '供应商',
|
|
'product_code' => '商品编码',
|
|
];
|
|
|
|
public const COLUMNS = [
|
|
'started_at' => '发起时间', 'work_order_no' => '工单编号', 'initiator' => '发起人',
|
|
'bookkeeping_reason' => '记账原因', 'reason_level_1' => '记账原因-一级选项', 'reason_level_2' => '记账原因-二级选项',
|
|
'bookkeeping_type' => '记账类型', 'executor' => '执行人', 'source' => '记账来源', 'amount' => '记账金额',
|
|
'deduction_status' => '厂家扣款判定', 'supplier_config' => '厂家记账配置', 'deduction_basis' => '判定说明',
|
|
'remark' => '记账备注说明', 'attachment' => '上传附件', 'online_order_no' => '线上订单号',
|
|
'internal_order_no' => '内部单号', 'actual_paid_amount' => '实付金额', 'carrier' => '物流公司',
|
|
'tracking_no' => '物流单号', 'shop_name' => '店铺名称', 'platform' => '平台', 'shipping_warehouse' => '订单发货仓库',
|
|
'status' => '工单状态', 'processing_status' => '记账处理状态', 'counterparty_agreed' => '被记账方是否同意记账',
|
|
'counterparty_note' => '被记账方处理说明', 'specialist_result' => '专员记账判定结果', 'specialist_note' => '专员处理说明',
|
|
'updated_at' => '修改时间', 'work_order_type' => '工单类型', 'product_code' => '商品编码', 'style_code' => '款式编码',
|
|
'other_attribute_6' => '其它属性6', 'product_name' => '商品名称', 'platform_sku_id' => '平台商品skuId',
|
|
'platform_product_id' => '平台商品id', 'product_quantity' => '商品数量', 'product_price' => '商品价格',
|
|
'product_amount' => '商品金额', 'buyer_paid_amount' => '买家实付', 'supplier' => '供应商',
|
|
'accepting_department' => '受理部门', 'order_shipped_at' => '订单发货时间',
|
|
];
|
|
|
|
public const MONEY_COLUMNS = ['amount', 'actual_paid_amount'];
|
|
|
|
public function __construct(
|
|
private readonly Repository $repository,
|
|
private readonly SupplierBookkeepingConfigStore $configs,
|
|
) {}
|
|
|
|
/** @return array<string,string> */
|
|
public function filters(): array
|
|
{
|
|
$filters = [];
|
|
foreach (['started_from', 'started_to'] as $key) {
|
|
$value = request()->query($key, '');
|
|
abort_unless(is_string($value) && ($value === '' || preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)), 422, '发起日期格式无效。');
|
|
$filters[$key] = $value;
|
|
}
|
|
$reason = request()->query('reason', '');
|
|
abort_unless(is_string($reason) && ($reason === '' || in_array($reason, Repository::reasonPaths(), true) || array_key_exists($reason, Repository::REASONS)), 422, '记账原因无效。');
|
|
$filters['reason'] = $reason;
|
|
$status = request()->query('status', '');
|
|
abort_unless(is_string($status) && ($status === '' || in_array($status, Repository::STATUS_LABELS, true)), 422, '工单状态无效。');
|
|
$filters['status'] = $status;
|
|
$deduction = request()->query('deduction', '');
|
|
abort_unless(is_string($deduction) && ($deduction === '' || in_array($deduction, Rule::STATUSES, true)), 422, '扣款判定无效。');
|
|
$filters['deduction'] = $deduction;
|
|
foreach (['bookkeeping_type' => 50, ...array_fill_keys(array_keys(self::TEXT_FILTERS), 200)] as $key => $max) {
|
|
$value = request()->query($key, '');
|
|
abort_unless(is_string($value) && mb_strlen($value) <= $max, 422, '筛选条件格式无效。');
|
|
$filters[$key] = trim($value);
|
|
}
|
|
|
|
return $filters;
|
|
}
|
|
|
|
/** @return array<string,string> 可导出列(按请求顺序) */
|
|
public function exportColumns(): array
|
|
{
|
|
$requested = request()->query('columns');
|
|
if ($requested === null || $requested === '') {
|
|
return self::COLUMNS;
|
|
}
|
|
abort_unless(is_string($requested) && strlen($requested) <= 1000, 422, '导出列无效。');
|
|
$keys = explode(',', $requested);
|
|
abort_unless(count($keys) === count(array_unique($keys)) && array_diff($keys, array_keys(self::COLUMNS)) === [], 422, '导出列无效。');
|
|
|
|
return array_replace(array_flip($keys), array_intersect_key(self::COLUMNS, array_flip($keys)));
|
|
}
|
|
|
|
/**
|
|
* @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
|
|
{
|
|
$from = $filters['started_from'] !== '' ? $filters['started_from'] . ' 00:00:00' : '';
|
|
$to = $filters['started_to'] !== '' ? $filters['started_to'] . ' 23:59:59' : '';
|
|
$deduction = $filters['deduction'] ?? '';
|
|
$configs = $this->configs->activeConfigs();
|
|
foreach ($rows ?? $this->repository->rows() as $row) {
|
|
// 配置随时可改,判定不进镜像结果缓存,每次按当前配置计算。
|
|
$row = Rule::decide($row, $configs) + $row;
|
|
if ($from !== '' && $row['started_at'] < $from) continue;
|
|
if ($to !== '' && $row['started_at'] > $to) continue;
|
|
if ($filters['reason'] !== '' && $row['bookkeeping_reason'] !== $filters['reason'] && $row['reason_level_1'] !== $filters['reason']) continue;
|
|
if ($filters['status'] !== '' && $row['status'] !== $filters['status']) continue;
|
|
if ($filters['bookkeeping_type'] !== '' && $row['bookkeeping_type'] !== $filters['bookkeeping_type']) continue;
|
|
if ($deduction !== '' && $row['deduction_status'] !== $deduction) continue;
|
|
foreach (self::TEXT_FILTERS as $key => $label) {
|
|
if ($filters[$key] !== '' && mb_stripos((string) ($row[$key] ?? ''), $filters[$key]) === false) continue 2;
|
|
}
|
|
yield $row;
|
|
}
|
|
}
|
|
|
|
public static function cents(mixed $amount): int
|
|
{
|
|
if ($amount === null || ! preg_match('/^(-?)(\d+)\.(\d{2})$/', (string) $amount, $m)) {
|
|
return 0;
|
|
}
|
|
$value = ((int) $m[2]) * 100 + (int) $m[3];
|
|
|
|
return $m[1] === '-' ? -$value : $value;
|
|
}
|
|
|
|
public function page(): array
|
|
{
|
|
if (request()->query('bk_view') === 'config') {
|
|
return app(SupplierBookkeepingConfigPageData::class)->page();
|
|
}
|
|
$filters = $this->filters();
|
|
$page = max(1, (int) request('page', 1));
|
|
$sync = $this->repository->syncStatus();
|
|
$summary = ['count' => 0, 'amount_cents' => 0, 'supplier_reason' => 0, 'warehouse_reason' => 0, 'deduction_count' => 0, 'deduction_cents' => 0, 'review' => 0];
|
|
$items = [];
|
|
$types = [];
|
|
$error = null;
|
|
|
|
if (! in_array($sync['code'], ['ready', 'failed'], true)) {
|
|
$error = $sync['code'] === 'initial_failed'
|
|
? '聚工单工单镜像首次同步失败,暂不能展示厂家记账,请联系管理员重新同步。'
|
|
: '聚工单工单镜像尚未完成首次同步,完成后自动展示厂家记账;当前为空不代表没有记账工单。';
|
|
} else {
|
|
try {
|
|
$all = $this->repository->rows();
|
|
foreach ($all as $row) {
|
|
if ($row['bookkeeping_type'] !== '') $types[$row['bookkeeping_type']] = true;
|
|
}
|
|
foreach ($this->rows($filters, $all) as $row) {
|
|
if ($summary['count'] >= ($page - 1) * self::PER_PAGE && count($items) < self::PER_PAGE) $items[] = $row;
|
|
$summary['count']++;
|
|
$summary['amount_cents'] += self::cents($row['amount']);
|
|
$summary[$row['reason_level_1'] === '厂家记账' ? 'supplier_reason' : 'warehouse_reason']++;
|
|
if ($row['deduction_status'] === Rule::STATUS_DEDUCT) {
|
|
$summary['deduction_count']++;
|
|
$summary['deduction_cents'] += self::cents($row['amount']);
|
|
} elseif ($row['deduction_status'] === Rule::STATUS_REVIEW) {
|
|
$summary['review']++;
|
|
}
|
|
}
|
|
} catch (\Throwable $exception) {
|
|
report($exception);
|
|
$error = '厂家记账数据暂时无法读取,请稍后重试。';
|
|
$summary = array_fill_keys(array_keys($summary), 0);
|
|
$items = [];
|
|
}
|
|
}
|
|
ksort($types);
|
|
|
|
return [
|
|
'view' => 'business.pages.supplier-payables.manufacturer-bookkeeping',
|
|
'bookkeepingView' => 'details',
|
|
'bookkeepingFilters' => $filters,
|
|
'bookkeepingSummary' => $summary,
|
|
'bookkeepingSync' => $sync,
|
|
'bookkeepingError' => $error,
|
|
'bookkeepingTypes' => array_keys($types),
|
|
'bookkeepingRows' => new LengthAwarePaginator($items, $summary['count'], self::PER_PAGE, $page, [
|
|
'path' => request()->url(),
|
|
'query' => request()->except('page'),
|
|
]),
|
|
];
|
|
}
|
|
}
|