- 新增配置表 business_supplier_bookkeeping_configs(+ 操作日志表),迁移时导入财务提供的 209 家存量配置 - 记账方式:正常记账 / 仅代发仓发错货记账 / 不记账,支持生效期;配置页支持新增、编辑、删除恢复、Excel 导入导出 - 扣款判定:厂家记账按供应商配置,仅正常记账扣款;仓库记账仅订单发货仓库为代发仓且二级原因含“仓库发错货”时作为厂家扣款项,其余不扣 - 未配置/生效期外显示待配置,多供应商判定不一致显示待确认,均不计入扣款金额 - 明细页新增厂家扣款判定筛选、扣款工单数与扣款金额指标、导出列 涉及:数据库迁移(新建配置表并写入存量配置)、金额汇总展示;不涉及上游写入和权限模型。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JouFR6TJ7vGscRJanYScEo
157 lines
7.2 KiB
PHP
157 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\Services;
|
|
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Modules\EcommerceFinance\Services\SupplierBookkeepingDeductionRule as Rule;
|
|
|
|
/** 供应商应付「厂家记账」配置:每个供应商一条记账方式,删除可恢复,操作留痕。 */
|
|
class SupplierBookkeepingConfigStore
|
|
{
|
|
public const TABLE = 'business_supplier_bookkeeping_configs';
|
|
public const EVENTS = 'business_supplier_bookkeeping_config_events';
|
|
|
|
private const FIELDS = ['mode', 'effective_from', 'effective_to', 'source_text'];
|
|
|
|
public function query()
|
|
{
|
|
return DB::table(self::TABLE);
|
|
}
|
|
|
|
/** @return array<string,array<string,mixed>> 供应商名称 => 生效中的配置(未删除) */
|
|
public function activeConfigs(): array
|
|
{
|
|
$result = [];
|
|
foreach ($this->query()->whereNull('deleted_at')->get() as $row) {
|
|
$result[trim($row->supplier_name)] = (array) $row;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function filtered(array $filters)
|
|
{
|
|
$query = $this->query();
|
|
$status = $filters['deleted_status'] ?? 'active';
|
|
if ($status === 'deleted') {
|
|
$query->whereNotNull('deleted_at');
|
|
} elseif ($status !== 'all') {
|
|
$query->whereNull('deleted_at');
|
|
}
|
|
if (($filters['supplier_name'] ?? '') !== '') {
|
|
$query->where('supplier_name', 'like', '%' . addcslashes($filters['supplier_name'], '\\%_') . '%');
|
|
}
|
|
if (($filters['mode'] ?? '') !== '') {
|
|
$query->where('mode', $filters['mode']);
|
|
}
|
|
|
|
return $query->orderBy('supplier_name')->orderBy('id');
|
|
}
|
|
|
|
/** @param array{supplier_name:string,mode:string,effective_from:?string,effective_to:?string,source_text?:string} $data */
|
|
public function save(array $data, string $actor, ?int $id = null): int
|
|
{
|
|
return DB::transaction(function () use ($data, $actor, $id): int {
|
|
$before = $id === null ? null : $this->query()->where('id', $id)->whereNull('deleted_at')->lockForUpdate()->first();
|
|
abort_if($id !== null && $before === null, 404);
|
|
$name = trim($data['supplier_name']);
|
|
$duplicate = $this->query()->where('supplier_name', $name)->when($id !== null, fn ($q) => $q->where('id', '<>', $id))->exists();
|
|
if ($duplicate) {
|
|
throw ValidationException::withMessages(['supplier_name' => '该供应商已有配置(含已删除),请编辑或恢复原配置。']);
|
|
}
|
|
$time = $this->time();
|
|
$values = ['supplier_name' => $name, 'updated_at' => $time] + $this->values($data);
|
|
if ($id === null) {
|
|
$id = $this->query()->insertGetId($values + ['creator' => $actor, 'created_at' => $time]);
|
|
} else {
|
|
$this->query()->where('id', $id)->update($values);
|
|
}
|
|
$this->event($id, $actor, $before ? 'update' : 'create', ($before ? '修改' : '新增') . '厂家记账配置:' . $name . ' → ' . Rule::describe($values), ['before' => $before, 'after' => $values]);
|
|
|
|
return $id;
|
|
});
|
|
}
|
|
|
|
public function changeDeleted(int $id, bool $deleted, string $actor): void
|
|
{
|
|
DB::transaction(function () use ($id, $deleted, $actor): void {
|
|
$row = $this->query()->where('id', $id)->lockForUpdate()->first();
|
|
abort_if($row === null, 404);
|
|
if (($row->deleted_at !== null) === $deleted) {
|
|
return;
|
|
}
|
|
$this->query()->where('id', $id)->update(['deleted_at' => $deleted ? $this->time() : null, 'updated_at' => $this->time()]);
|
|
$this->event($id, $actor, $deleted ? 'delete' : 'restore', ($deleted ? '删除' : '恢复') . '厂家记账配置:' . $row->supplier_name, ['supplier_name' => $row->supplier_name, 'mode' => $row->mode]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 整批导入:新供应商新增;已有供应商记账方式或生效期不同则按文件更新(已删除的一并恢复);相同则跳过。
|
|
*
|
|
* @param list<array{supplier_name:string,mode:string,effective_from:?string,effective_to:?string,source_text:string}> $rows
|
|
* @return array{added:int,updated:int,skipped:int}
|
|
*/
|
|
public function import(array $rows, string $actor, string $sourceHash): array
|
|
{
|
|
return DB::transaction(function () use ($rows, $actor, $sourceHash): array {
|
|
$added = $updated = $skipped = 0;
|
|
$time = $this->time();
|
|
foreach ($rows as $row) {
|
|
$name = trim($row['supplier_name']);
|
|
$values = $this->values($row);
|
|
$existing = $this->query()->where('supplier_name', $name)->lockForUpdate()->first();
|
|
if ($existing === null) {
|
|
$this->query()->insert(['supplier_name' => $name, 'creator' => $actor, 'created_at' => $time, 'updated_at' => $time] + $values);
|
|
$added++;
|
|
continue;
|
|
}
|
|
$same = $existing->deleted_at === null;
|
|
foreach (['mode', 'effective_from', 'effective_to'] as $field) {
|
|
$same = $same && (string) $existing->{$field} === (string) $values[$field];
|
|
}
|
|
if ($same) {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
$this->query()->where('id', $existing->id)->update($values + ['deleted_at' => null, 'updated_at' => $time]);
|
|
$updated++;
|
|
}
|
|
$this->event(null, $actor, 'import', "导入厂家记账配置:新增{$added}条,更新{$updated}条,未变化{$skipped}条", compact('added', 'updated', 'skipped') + ['source_sha256' => $sourceHash]);
|
|
|
|
return compact('added', 'updated', 'skipped');
|
|
});
|
|
}
|
|
|
|
public function events()
|
|
{
|
|
return DB::table(self::EVENTS)->orderByDesc('id');
|
|
}
|
|
|
|
private function values(array $data): array
|
|
{
|
|
$values = [];
|
|
foreach (self::FIELDS as $field) {
|
|
$value = $data[$field] ?? null;
|
|
$values[$field] = $field === 'source_text' ? mb_substr(trim((string) $value), 0, 255) : (($value === '' || $value === null) ? null : (string) $value);
|
|
}
|
|
abort_unless(isset(Rule::MODES[$values['mode']]), 422, '记账方式无效。');
|
|
if ($values['effective_from'] !== null && $values['effective_to'] !== null && $values['effective_from'] > $values['effective_to']) {
|
|
throw ValidationException::withMessages(['effective_to' => '生效结束日期不能早于开始日期。']);
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
private function event(?int $id, string $actor, string $action, string $description, array $details): void
|
|
{
|
|
DB::table(self::EVENTS)->insert(['config_id' => $id, 'actor' => $actor, 'action' => $action, 'description' => mb_substr($description, 0, 1000), 'details' => json_encode($details, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), 'created_at' => $this->time()]);
|
|
}
|
|
|
|
private function time(): string
|
|
{
|
|
return Carbon::now(config('app.platform_timezone', 'Asia/Shanghai'))->format('Y-m-d H:i:s');
|
|
}
|
|
}
|