一次点击按采购拆分任务,并用一条钉钉群消息发送全部采购独立处理链接。 Co-authored-by: 苏木 <594326199@qq.com> Co-committed-by: 苏木 <594326199@qq.com>
348 lines
13 KiB
PHP
348 lines
13 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\View;
|
||
|
||
use App\Http\Controllers\Business\PayablesFinalActions;
|
||
use Illuminate\Pagination\LengthAwarePaginator;
|
||
use Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository;
|
||
use Modules\EcommerceFinance\Services\PayablesFinalRegistry;
|
||
use Modules\EcommerceFinance\Services\SupplierNegativeAlertDingTalkWorkflow;
|
||
use Modules\EcommerceFinance\Services\SupplierPayableOverrideStore;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 供应商负数预警:直接读取「供应商应付(终版)→ 供应商应付表」,
|
||
* 仅保留「供应商余额 - 应付余额 + 预估金额 < 0」的记录。
|
||
*/
|
||
class SupplierNegativeAlertPageData
|
||
{
|
||
private const DATASET = 'supplier-payables-report';
|
||
|
||
private const PER_PAGE = 25;
|
||
|
||
public function __construct(
|
||
private readonly SupplierPayableOverrideStore $store,
|
||
private readonly PayablesFinalRegistry $registry,
|
||
private readonly SupplierManagementMirrorRepository $supplierManagement,
|
||
private readonly SupplierNegativeAlertDingTalkWorkflow $dingTalkWorkflow,
|
||
) {
|
||
}
|
||
|
||
/** @return array<string,mixed> */
|
||
public function page(): array
|
||
{
|
||
$allRows = $this->decoratedRows();
|
||
$months = $this->monthOptions($allRows);
|
||
$supplierNames = $this->supplierOptions($allRows);
|
||
$purchaserNames = $this->purchaserOptions($allRows);
|
||
$filters = $this->filters(request()->query(), $months, $purchaserNames);
|
||
$negativeRows = $this->filterRows($allRows, $filters);
|
||
$filteredSupplierNames = array_values(array_unique(array_filter(array_map(
|
||
static fn (array $row): string => trim((string) ($row['supplier'] ?? '')),
|
||
$negativeRows,
|
||
))));
|
||
$filteredPurchaserNames = $this->purchaserOptions($negativeRows);
|
||
$unassignedSupplierNames = array_values(array_unique(array_filter(array_map(
|
||
static fn (array $row): string => trim((string) ($row['purchaser'] ?? '')) === ''
|
||
? trim((string) ($row['supplier'] ?? ''))
|
||
: '',
|
||
$negativeRows,
|
||
))));
|
||
$latestResults = $this->dingTalkWorkflow->latestResults(
|
||
$filteredSupplierNames,
|
||
(int) (company_id() ?: 1),
|
||
);
|
||
|
||
$total = count($negativeRows);
|
||
$pages = max((int) ceil($total / self::PER_PAGE), 1);
|
||
$page = min(max((int) request()->query('page', 1), 1), $pages);
|
||
$pageRows = array_slice($negativeRows, ($page - 1) * self::PER_PAGE, self::PER_PAGE);
|
||
$items = array_map(
|
||
static function (array $row) use ($latestResults): object {
|
||
$supplier = trim((string) ($row['supplier'] ?? ''));
|
||
$result = $latestResults[$supplier] ?? null;
|
||
$row['handling_result'] = $result ? trim((string) $result->result_text) : '';
|
||
$row['handling_result_meta'] = $result
|
||
? '采购 ' . trim((string) $result->purchaser_name)
|
||
. ' · ' . mb_substr((string) $result->submitted_at, 0, 16)
|
||
: '';
|
||
|
||
return (object) $row;
|
||
},
|
||
$pageRows,
|
||
);
|
||
|
||
$negativeAlertFields = [];
|
||
$fields = collect($this->registry->fields(
|
||
PayablesFinalRegistry::SECTION_SUPPLIER,
|
||
self::DATASET,
|
||
))->map(
|
||
static fn (array $definition, string $name): array => ['name' => $name] + $definition,
|
||
)->values()->all();
|
||
foreach ($fields as $field) {
|
||
$negativeAlertFields[] = $field;
|
||
if (($field['name'] ?? '') === 'negative_reason') {
|
||
$negativeAlertFields[] = ['name' => 'purchaser', 'label' => '采购', 'type' => 'text'];
|
||
$negativeAlertFields[] = ['name' => 'handling_result', 'label' => '处理结果', 'type' => 'text'];
|
||
}
|
||
}
|
||
|
||
return [
|
||
'view' => 'business.pages.negative-alerts.supplier-negative-alert',
|
||
'negativeAlertFields' => $negativeAlertFields,
|
||
'negativeAlertRows' => new LengthAwarePaginator(
|
||
$items,
|
||
$total,
|
||
self::PER_PAGE,
|
||
$page,
|
||
[
|
||
'path' => request()->url(),
|
||
'query' => request()->except('page'),
|
||
],
|
||
),
|
||
'negativeAlertMonths' => $months,
|
||
'negativeAlertMonth' => $filters['month'],
|
||
'negativeAlertSupplier' => $filters['supplier'],
|
||
'negativeAlertSupplierNames' => $supplierNames,
|
||
'negativeAlertChecker' => $filters['checker'],
|
||
'negativeAlertCheckerNames' => $this->textOptions($allRows, 'checker'),
|
||
'negativeAlertSettlementMethod' => $filters['settlement_method'],
|
||
'negativeAlertSettlementMethods' => $this->textOptions($allRows, 'settlement_method'),
|
||
'negativeAlertPurchaser' => $filters['purchaser'],
|
||
'negativeAlertPurchaserNames' => $purchaserNames,
|
||
'negativeAlertSupplierCount' => count($filteredSupplierNames),
|
||
'negativeAlertFilteredPurchaserCount' => count($filteredPurchaserNames),
|
||
'negativeAlertUnassignedSupplierCount' => count($unassignedSupplierNames),
|
||
'negativeAlertTotal' => $total,
|
||
'negativeAlertRobot' => $this->dingTalkWorkflow->configuration(),
|
||
'negativeAlertSourceUrl' => route('business.pages.show', [
|
||
'section' => 'supplier-payables-final',
|
||
'page' => 'supplier-payables-report',
|
||
]),
|
||
];
|
||
}
|
||
|
||
/** @param array<string,mixed> $filters
|
||
* @return list<array<string,mixed>>
|
||
*/
|
||
public function rowsForFilters(array $filters): array
|
||
{
|
||
$allRows = $this->decoratedRows();
|
||
|
||
return $this->filterRows($allRows, $this->filters(
|
||
$filters,
|
||
$this->monthOptions($allRows),
|
||
$this->purchaserOptions($allRows),
|
||
));
|
||
}
|
||
|
||
/** @return list<array<string,mixed>> */
|
||
private function decoratedRows(): array
|
||
{
|
||
$rows = $this->negativeRows();
|
||
$supplierNames = array_values(array_unique(array_filter(array_map(
|
||
static fn (array $row): string => trim((string) ($row['supplier'] ?? '')),
|
||
$rows,
|
||
))));
|
||
$purchasersBySupplier = [];
|
||
if ($supplierNames !== []) {
|
||
try {
|
||
foreach ($this->supplierManagement->suppliersByNames($supplierNames) as $supplier) {
|
||
$name = trim((string) ($supplier->scmname ?? ''));
|
||
$buyer = trim((string) ($supplier->buyer ?? ''));
|
||
if ($name !== '' && $buyer !== '') {
|
||
$purchasersBySupplier[$name][$buyer] = true;
|
||
}
|
||
}
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
}
|
||
}
|
||
$purchasersBySupplier = array_map(
|
||
static fn (array $buyers): string => implode('、', array_keys($buyers)),
|
||
$purchasersBySupplier,
|
||
);
|
||
|
||
foreach ($rows as &$row) {
|
||
$supplier = trim((string) ($row['supplier'] ?? ''));
|
||
$row['purchaser'] = $purchasersBySupplier[$supplier] ?? '';
|
||
}
|
||
unset($row);
|
||
|
||
return $rows;
|
||
}
|
||
|
||
/** @return list<array<string,mixed>> */
|
||
private function negativeRows(): array
|
||
{
|
||
$rows = $this->store->listRows(
|
||
PayablesFinalActions::DATASET_PREFIX . 'supplier:' . self::DATASET,
|
||
(int) (company_id() ?: 1),
|
||
);
|
||
|
||
$negativeRows = [];
|
||
foreach ($rows as $row) {
|
||
$balance = $this->number($row['supplier_balance'] ?? null) ?? 0.0;
|
||
$shouldPay = $this->number($row['should_pay'] ?? null) ?? 0.0;
|
||
$estimatedPrice = $this->number($row['estimated_price'] ?? null) ?? 0.0;
|
||
$alertAmount = $balance - $shouldPay + $estimatedPrice;
|
||
if ($alertAmount >= 0) {
|
||
continue;
|
||
}
|
||
|
||
$row['supplier_balance'] = $balance;
|
||
$row['_negative_alert_amount'] = round($alertAmount, 2);
|
||
$negativeRows[] = $row;
|
||
}
|
||
|
||
usort($negativeRows, static function (array $left, array $right): int {
|
||
return strcmp(
|
||
(string) ($right['summary_month'] ?? ''),
|
||
(string) ($left['summary_month'] ?? ''),
|
||
) ?: strnatcasecmp(
|
||
(string) ($left['supplier'] ?? ''),
|
||
(string) ($right['supplier'] ?? ''),
|
||
);
|
||
});
|
||
|
||
return $negativeRows;
|
||
}
|
||
|
||
/** @param list<array<string,mixed>> $rows
|
||
* @return list<string>
|
||
*/
|
||
private function monthOptions(array $rows): array
|
||
{
|
||
$months = [];
|
||
foreach ($rows as $row) {
|
||
$month = trim((string) ($row['summary_month'] ?? ''));
|
||
if (preg_match('/^\d{4}-\d{2}$/', $month) === 1) {
|
||
$months[$month] = true;
|
||
}
|
||
}
|
||
$months = array_keys($months);
|
||
rsort($months);
|
||
|
||
return $months;
|
||
}
|
||
|
||
/** @param list<array<string,mixed>> $rows
|
||
* @return list<string>
|
||
*/
|
||
private function supplierOptions(array $rows): array
|
||
{
|
||
$names = array_values(array_unique(array_filter(array_map(
|
||
static fn (array $row): string => trim((string) ($row['supplier'] ?? '')),
|
||
$rows,
|
||
))));
|
||
sort($names, SORT_NATURAL | SORT_FLAG_CASE);
|
||
|
||
return $names;
|
||
}
|
||
|
||
/** @param list<array<string,mixed>> $rows
|
||
* @return list<string>
|
||
*/
|
||
private function purchaserOptions(array $rows): array
|
||
{
|
||
$names = array_values(array_unique(array_filter(array_map(
|
||
static fn (array $row): string => trim((string) ($row['purchaser'] ?? '')),
|
||
$rows,
|
||
))));
|
||
sort($names, SORT_NATURAL | SORT_FLAG_CASE);
|
||
|
||
return $names;
|
||
}
|
||
|
||
/** @param list<array<string,mixed>> $rows
|
||
* @return list<string>
|
||
*/
|
||
private function textOptions(array $rows, string $field): array
|
||
{
|
||
$values = array_values(array_unique(array_filter(array_map(
|
||
static fn (array $row): string => trim((string) ($row[$field] ?? '')),
|
||
$rows,
|
||
))));
|
||
sort($values, SORT_NATURAL | SORT_FLAG_CASE);
|
||
|
||
return $values;
|
||
}
|
||
|
||
/** @param array<string,mixed> $input
|
||
* @param list<string> $months
|
||
* @param list<string> $purchasers
|
||
* @return array{month:string,supplier:string,checker:string,settlement_method:string,purchaser:string}
|
||
*/
|
||
private function filters(array $input, array $months, array $purchasers): array
|
||
{
|
||
$text = static fn (string $key): string => mb_substr(
|
||
trim(is_scalar($input[$key] ?? '') ? (string) ($input[$key] ?? '') : ''),
|
||
0,
|
||
191,
|
||
);
|
||
$month = $text('month');
|
||
$purchaser = $text('purchaser');
|
||
|
||
return [
|
||
'month' => in_array($month, $months, true) ? $month : '',
|
||
'supplier' => $text('supplier'),
|
||
'checker' => $text('checker'),
|
||
'settlement_method' => $text('settlement_method'),
|
||
'purchaser' => in_array($purchaser, $purchasers, true) ? $purchaser : '',
|
||
];
|
||
}
|
||
|
||
/** @param list<array<string,mixed>> $rows
|
||
* @param array{month:string,supplier:string,checker:string,settlement_method:string,purchaser:string} $filters
|
||
* @return list<array<string,mixed>>
|
||
*/
|
||
private function filterRows(array $rows, array $filters): array
|
||
{
|
||
return array_values(array_filter(
|
||
$rows,
|
||
static function (array $row) use ($filters): bool {
|
||
if ($filters['month'] !== '' && (string) ($row['summary_month'] ?? '') !== $filters['month']) {
|
||
return false;
|
||
}
|
||
|
||
if ($filters['supplier'] !== '') {
|
||
$supplierNeedle = mb_strtolower($filters['supplier']);
|
||
$supplierHaystack = mb_strtolower(implode("\n", [
|
||
(string) ($row['supplier'] ?? ''),
|
||
(string) ($row['company'] ?? ''),
|
||
]));
|
||
if (! str_contains($supplierHaystack, $supplierNeedle)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
foreach (['checker', 'settlement_method'] as $field) {
|
||
if ($filters[$field] === '') {
|
||
continue;
|
||
}
|
||
if (! str_contains(
|
||
mb_strtolower((string) ($row[$field] ?? '')),
|
||
mb_strtolower($filters[$field]),
|
||
)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return $filters['purchaser'] === ''
|
||
|| trim((string) ($row['purchaser'] ?? '')) === $filters['purchaser'];
|
||
},
|
||
));
|
||
}
|
||
|
||
private function number(mixed $value): ?float
|
||
{
|
||
if ($value === null || trim((string) $value) === '') {
|
||
return null;
|
||
}
|
||
|
||
$normalized = str_replace([',', ',', ' '], '', trim((string) $value));
|
||
|
||
return is_numeric($normalized) ? (float) $normalized : null;
|
||
}
|
||
}
|