155 lines
4.5 KiB
PHP
155 lines
4.5 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Services;
|
||
|
||
use Illuminate\Support\Facades\File;
|
||
|
||
/**
|
||
* 账套科目索引(resources/data/coa.json)。
|
||
*
|
||
* 账套是「小企业会计准则 + 5 位一级科目」,层级用点分:5601.05.04。
|
||
* 映射表里写的是「一级科目名 + 二级科目名」两列,二级名往往是第 3、4 层,
|
||
* 所以按「一级名 → 一级编码 → 编码前缀」这条路去子树里找,才找得准。
|
||
*/
|
||
class AccountMappingCoa
|
||
{
|
||
/** @var array<int,array{code:string,name:string,level:int,category:string,balance:string}>|null */
|
||
private ?array $accounts = null;
|
||
|
||
/** @return array<int,array{code:string,name:string,level:int,category:string,balance:string}> */
|
||
public function all(): array
|
||
{
|
||
if ($this->accounts !== null) {
|
||
return $this->accounts;
|
||
}
|
||
|
||
$path = resource_path('data/coa.json');
|
||
$payload = File::exists($path) ? json_decode((string) File::get($path), true) : null;
|
||
$rows = is_array($payload['accounts'] ?? null) ? $payload['accounts'] : [];
|
||
|
||
$this->accounts = array_map(static fn (array $row): array => [
|
||
'code' => (string) ($row[0] ?? ''),
|
||
'name' => (string) ($row[1] ?? ''),
|
||
'level' => (int) ($row[2] ?? 1),
|
||
'category' => (string) ($row[3] ?? ''),
|
||
'balance' => (string) ($row[4] ?? ''),
|
||
], $rows);
|
||
|
||
return $this->accounts;
|
||
}
|
||
|
||
/** @return array<int,array{code:string,name:string}> 一级科目(用于下拉第一级) */
|
||
public function level1(): array
|
||
{
|
||
return array_values(array_map(
|
||
static fn (array $a): array => ['code' => $a['code'], 'name' => $a['name']],
|
||
array_filter($this->all(), static fn (array $a): bool => $a['level'] === 1 && $a['name'] !== '')
|
||
));
|
||
}
|
||
|
||
/** @return array<int,array{code:string,name:string,level:int}> 某编码子树下的非一级科目 */
|
||
public function descendants(string $code): array
|
||
{
|
||
if ($code === '') {
|
||
return [];
|
||
}
|
||
|
||
$prefix = $code . '.';
|
||
$hits = [];
|
||
foreach ($this->all() as $a) {
|
||
if ($a['code'] !== $code && str_starts_with($a['code'], $prefix)) {
|
||
$hits[] = ['code' => $a['code'], 'name' => $a['name'], 'level' => $a['level']];
|
||
}
|
||
}
|
||
|
||
return $hits;
|
||
}
|
||
|
||
/** 一级科目名 → 编码(账套里一级科目名唯一) */
|
||
public function level1Code(string $name): ?string
|
||
{
|
||
$name = trim($name);
|
||
if ($name === '') {
|
||
return null;
|
||
}
|
||
|
||
foreach ($this->all() as $a) {
|
||
if ($a['level'] === 1 && $a['name'] === $name) {
|
||
return $a['code'];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 在「一级科目」子树下按名称找科目编码。
|
||
*
|
||
* @return array<int,string> 命中编码;空数组 = 不存在;多于 1 个 = 同名歧义
|
||
*/
|
||
public function resolve(string $level1Name, string $subName): array
|
||
{
|
||
$subName = trim($subName);
|
||
if ($subName === '') {
|
||
return [];
|
||
}
|
||
|
||
$base = $this->level1Code($level1Name);
|
||
if ($base === null) {
|
||
return [];
|
||
}
|
||
|
||
$hits = [];
|
||
foreach ($this->all() as $a) {
|
||
if ($a['name'] === $subName && ($a['code'] === $base || str_starts_with($a['code'], $base . '.'))) {
|
||
$hits[] = $a['code'];
|
||
}
|
||
}
|
||
|
||
sort($hits);
|
||
|
||
return $hits;
|
||
}
|
||
|
||
/** 全账套里同名的编码(用于解释「为什么找不到」) @return array<int,string> */
|
||
public function anywhereByName(string $name): array
|
||
{
|
||
$name = trim($name);
|
||
if ($name === '') {
|
||
return [];
|
||
}
|
||
|
||
$hits = [];
|
||
foreach ($this->all() as $a) {
|
||
if ($a['name'] === $name) {
|
||
$hits[] = $a['code'];
|
||
}
|
||
}
|
||
|
||
sort($hits);
|
||
|
||
return $hits;
|
||
}
|
||
|
||
/** 名称或编码 → 编码(映射表里偶尔会直接写编码) */
|
||
public function toCode(string $level1Token): ?string
|
||
{
|
||
$token = trim($level1Token);
|
||
if ($token === '') {
|
||
return null;
|
||
}
|
||
|
||
if (preg_match('/^\d+(\.\d+)*$/', $token) === 1) {
|
||
foreach ($this->all() as $a) {
|
||
if ($a['code'] === $token) {
|
||
return $token;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
return $this->level1Code($token);
|
||
}
|
||
}
|