CaiWuXiTong/modules/EcommerceFinance/View/SupplierBookkeepingPageData.php

208 lines
11 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\SupplierBookkeepingWorkOrderRepository as Repository;
use Modules\EcommerceFinance\Services\SupplierBookkeepingConfigStore;
use Modules\EcommerceFinance\Services\SupplierBookkeepingDeductionRule as Rule;
use Modules\EcommerceFinance\Services\SupplierBookkeepingValuation;
use Modules\EcommerceFinance\Services\SupplierBookkeepingProductResolver;
/** 供应商应付 → 厂家记账(聚工单“记账”工单镜像)。 */
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' => '记账金额',
'original_amount' => '原记账金额',
'matched_cost' => '匹配商品成本', 'matched_freight' => '核算运费', 'matched_quantity' => '匹配发货数量',
'matched_tracking_no' => '匹配快递单号', 'valuation_window' => '发货匹配期间',
'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' => '商品编码', 'resolved_product_code' => '实际商品编码',
'supplier_match_basis' => '供应商匹配说明', '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', 'original_amount', 'matched_cost', 'matched_freight', 'actual_paid_amount'];
public function __construct(
private readonly Repository $repository,
private readonly SupplierBookkeepingConfigStore $configs,
private readonly SupplierBookkeepingValuation $valuation,
private readonly SupplierBookkeepingProductResolver $products,
) {}
/** @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();
$matched = [];
foreach ($rows ?? $this->repository->rows() as $row) {
$row['original_amount'] = $row['amount'];
$row += ['matched_cost' => null, 'matched_freight' => null, 'matched_quantity' => '', 'matched_tracking_no' => '', 'valuation_window' => '', 'resolved_product_code' => '', 'supplier_match_basis' => ''];
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;
$matched[] = $row;
}
$resolved = [];
foreach ($this->products->apply($matched) as $row) {
// 先补全供应商,再按当前配置及既定金额顺序判断;不缓存判定。
$row = Rule::decide($row, $configs) + $row;
if ($row['supplier_match_basis'] !== '') $row['deduction_basis'] = $row['supplier_match_basis'] . ';' . $row['deduction_basis'];
foreach (self::TEXT_FILTERS as $key => $label) {
$value = (string) ($row[$key] ?? '');
if ($key === 'product_code') $value .= "\n" . $row['resolved_product_code'];
if ($filters[$key] !== '' && mb_stripos($value, $filters[$key]) === false) continue 2;
}
$resolved[] = $row;
}
foreach ($this->valuation->apply($resolved) as $row) {
if ($deduction !== '' && $row['deduction_status'] !== $deduction) continue;
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'),
]),
];
}
}