236 lines
9.2 KiB
PHP
236 lines
9.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Recon;
|
|
|
|
use App\Exceptions\RepositoryException;
|
|
use App\Http\Requests\Recon\ReconciliationFormRequest;
|
|
use App\Models\ReconciliationModel;
|
|
use App\Models\SettlementModel;
|
|
use App\Services\BillNumberService;
|
|
use App\Services\ReconciliationBuildService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
|
use Modules\AnnoRoute\Attribute\GetRoute;
|
|
use Modules\AnnoRoute\Attribute\PostRoute;
|
|
use Modules\AnnoRoute\Attribute\PutRoute;
|
|
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
|
use Modules\Common\Http\Controllers\BaseController;
|
|
|
|
/**
|
|
* 财务对账管理(D1 品类 / D2 供应商筛选、D5 差额对比、D9 结算表生成)
|
|
* 对账明细的 D4/D6/D8 操作见 ReconItemController
|
|
*/
|
|
#[RequestAttribute('/recon/list', 'recon.list')]
|
|
class ReconciliationController extends BaseController
|
|
{
|
|
protected array $searchField = [
|
|
'status' => '=',
|
|
'category_id' => '=',
|
|
'supplier_id' => '=',
|
|
'title' => 'like',
|
|
'period_start' => 'date',
|
|
];
|
|
|
|
/** 对账单列表 */
|
|
#[GetRoute(authorize: 'query')]
|
|
public function query(Request $request): JsonResponse
|
|
{
|
|
$params = $request->all();
|
|
$pageSize = $params['pageSize'] ?? 10;
|
|
$data = $this->buildSearch($params, ReconciliationModel::query()->with('operator:id,nickname'))
|
|
->orderBy('id', 'desc')
|
|
->paginate($pageSize)
|
|
->toArray();
|
|
return $this->success($data);
|
|
}
|
|
|
|
/** 创建对账单(草稿,recon_no = RC…) */
|
|
#[PostRoute(authorize: 'create')]
|
|
public function create(ReconciliationFormRequest $request): JsonResponse
|
|
{
|
|
$validated = $request->validated();
|
|
$recon = ReconciliationModel::create([
|
|
'recon_no' => app(BillNumberService::class)->make('RC'),
|
|
'title' => $validated['title'],
|
|
'period_start' => $validated['period_start'],
|
|
'period_end' => $validated['period_end'],
|
|
'category_id' => $validated['category_id'],
|
|
'supplier_id' => $validated['supplier_id'],
|
|
'publish_amount' => 0,
|
|
'actual_amount' => 0,
|
|
'diff_amount' => 0,
|
|
'status' => ReconciliationModel::STATUS_DRAFT,
|
|
'operator_id' => (int) $request->user()->id,
|
|
'remark' => $validated['remark'] ?? '',
|
|
]);
|
|
return $this->success(['id' => $recon->id]);
|
|
}
|
|
|
|
/** 编辑对账单(仅草稿/对账中) */
|
|
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
|
|
public function update(int $id, ReconciliationFormRequest $request): JsonResponse
|
|
{
|
|
$recon = ReconciliationModel::find($id);
|
|
if (empty($recon)) {
|
|
throw new RepositoryException('对账单不存在');
|
|
}
|
|
if ($recon->status === ReconciliationModel::STATUS_SETTLED) {
|
|
throw new RepositoryException('对账单已结算,不能编辑');
|
|
}
|
|
$validated = $request->validated();
|
|
$recon->update([
|
|
'title' => $validated['title'],
|
|
'period_start' => $validated['period_start'],
|
|
'period_end' => $validated['period_end'],
|
|
'category_id' => $validated['category_id'],
|
|
'supplier_id' => $validated['supplier_id'],
|
|
'remark' => $validated['remark'] ?? '',
|
|
]);
|
|
return $this->success();
|
|
}
|
|
|
|
/** 删除对账单(仅草稿可删,连带明细) */
|
|
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
|
|
public function delete(int $id): JsonResponse
|
|
{
|
|
$recon = ReconciliationModel::find($id);
|
|
if (empty($recon)) {
|
|
throw new RepositoryException('对账单不存在');
|
|
}
|
|
if ($recon->status !== ReconciliationModel::STATUS_DRAFT) {
|
|
throw new RepositoryException('仅草稿状态的对账单可以删除');
|
|
}
|
|
DB::transaction(function () use ($recon) {
|
|
$recon->items()->delete();
|
|
$recon->delete();
|
|
});
|
|
return $this->success();
|
|
}
|
|
|
|
/** 生成对账明细(按周期 + 品类 + 供应商拉取采购分摊数据;可重复生成) */
|
|
#[PostRoute(route: '/{id}/build', authorize: 'build', where: ['id' => '[0-9]+'])]
|
|
public function build(int $id): JsonResponse
|
|
{
|
|
$recon = ReconciliationModel::find($id);
|
|
if (empty($recon)) {
|
|
throw new RepositoryException('对账单不存在');
|
|
}
|
|
if ($recon->status === ReconciliationModel::STATUS_SETTLED) {
|
|
throw new RepositoryException('对账单已结算,不能重新生成明细');
|
|
}
|
|
$count = app(ReconciliationBuildService::class)->build($recon);
|
|
return $this->success(['count' => $count], '对账明细已生成');
|
|
}
|
|
|
|
/**
|
|
* D5 差额对比视图:按门店 / 按商品两个维度 + 合计行
|
|
*/
|
|
#[GetRoute(route: '/{id}/diff', authorize: 'query', where: ['id' => '[0-9]+'])]
|
|
public function diff(int $id): JsonResponse
|
|
{
|
|
$recon = ReconciliationModel::find($id);
|
|
if (empty($recon)) {
|
|
throw new RepositoryException('对账单不存在');
|
|
}
|
|
|
|
$items = $recon->items()->with('store:id,name')->get();
|
|
|
|
$byStore = $items->groupBy('store_id')->map(function ($group) {
|
|
$first = $group->first();
|
|
$publish = $group->sum('publish_amount');
|
|
$actual = $group->sum('actual_amount');
|
|
return [
|
|
'store_id' => $first->store_id,
|
|
'store_name' => $first->store?->name ?? '',
|
|
'publish' => (float) $publish,
|
|
'actual' => (float) $actual,
|
|
'diff' => (float) bcsub((string) $publish, (string) $actual, 2),
|
|
];
|
|
})->values();
|
|
|
|
$byProduct = $items->groupBy('product_id')->map(function ($group) {
|
|
$first = $group->first();
|
|
$publish = $group->sum('publish_amount');
|
|
$actual = $group->sum('actual_amount');
|
|
return [
|
|
'product_id' => $first->product_id,
|
|
'product_name' => $first->product_name,
|
|
'publish' => (float) $publish,
|
|
'actual' => (float) $actual,
|
|
'diff' => (float) bcsub((string) $publish, (string) $actual, 2),
|
|
];
|
|
})->values();
|
|
|
|
$publishTotal = (string) $items->sum('publish_amount');
|
|
$actualTotal = (string) $items->sum('actual_amount');
|
|
|
|
return $this->success([
|
|
'by_store' => $byStore->toArray(),
|
|
'by_product' => $byProduct->toArray(),
|
|
'total' => [
|
|
'publish' => (float) $publishTotal,
|
|
'actual' => (float) $actualTotal,
|
|
'diff' => (float) bcsub($publishTotal, $actualTotal, 2),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* D9 生成结算表:按门店聚合明细生成 settlement 记录,对账单 status → 已结算
|
|
* (回框统计表规则待业务确认,本次仅预留结构)
|
|
*/
|
|
#[PostRoute(route: '/{id}/settle', authorize: 'settle', where: ['id' => '[0-9]+'])]
|
|
public function settle(int $id, Request $request): JsonResponse
|
|
{
|
|
$recon = ReconciliationModel::find($id);
|
|
if (empty($recon)) {
|
|
throw new RepositoryException('对账单不存在');
|
|
}
|
|
if ($recon->status !== ReconciliationModel::STATUS_WORKING) {
|
|
throw new RepositoryException('仅「对账中」的对账单可以生成结算表');
|
|
}
|
|
|
|
$count = DB::transaction(function () use ($recon, $request) {
|
|
$groups = $recon->items()->get()->groupBy('store_id');
|
|
if ($groups->isEmpty()) {
|
|
throw new RepositoryException('对账单无明细,请先生成对账明细');
|
|
}
|
|
|
|
$billNumber = app(BillNumberService::class);
|
|
foreach ($groups as $storeId => $items) {
|
|
$publish = $items->reduce(
|
|
static fn (string $carry, $item): string => bcadd($carry, (string) $item->publish_amount, 2),
|
|
'0'
|
|
);
|
|
$actual = $items->reduce(
|
|
static fn (string $carry, $item): string => bcadd($carry, (string) $item->actual_amount, 2),
|
|
'0'
|
|
);
|
|
|
|
SettlementModel::create([
|
|
'settlement_no' => $billNumber->make('JS'),
|
|
'recon_id' => $recon->id,
|
|
'store_id' => (int) $storeId,
|
|
'period_start' => $recon->period_start,
|
|
'period_end' => $recon->period_end,
|
|
'total_amount' => $publish,
|
|
'actual_amount' => $actual,
|
|
'diff_amount' => bcsub($publish, $actual, 2),
|
|
'status' => SettlementModel::STATUS_SETTLED,
|
|
'operator_id' => (int) $request->user()->id,
|
|
'settled_at' => now(),
|
|
]);
|
|
}
|
|
|
|
$recon->status = ReconciliationModel::STATUS_SETTLED;
|
|
$recon->save();
|
|
|
|
return $groups->count();
|
|
});
|
|
|
|
return $this->success(['count' => $count], '结算表已生成');
|
|
}
|
|
}
|