移除对账

This commit is contained in:
liu
2026-08-14 15:54:53 +08:00
parent 629d28a369
commit f0927e57e9
20 changed files with 1 additions and 2363 deletions
@@ -1,125 +0,0 @@
<?php
namespace App\Http\Controllers\Recon;
use App\Exceptions\RepositoryException;
use App\Http\Requests\Recon\ReconItemUpdateRequest;
use App\Models\ReconciliationItemModel;
use App\Models\ReconciliationModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
/**
* 对账明细操作(D4 修改 / D6 单品级门店备注 / D8 对账状态标记)
* 权限点前缀 recon.itemauthorize: item.update → recon.item.item.update
*/
#[RequestAttribute('/recon/item', 'recon.item')]
class ReconItemController extends BaseController
{
/**
* D4 修改订货量/称重/数量/金额/商品名,自动重算本行 diff + 头汇总
*/
#[PutRoute(route: '/{id}', authorize: 'item.update', where: ['id' => '[0-9]+'])]
public function update(int $id, ReconItemUpdateRequest $request): JsonResponse
{
$item = ReconciliationItemModel::find($id);
if (empty($item)) {
throw new RepositoryException('对账明细不存在');
}
$this->assertEditable($item);
$validated = $request->validated();
if (isset($validated['product_name'])) {
$item->product_name = $validated['product_name'];
}
if (isset($validated['quantity'])) {
$item->quantity = $validated['quantity'];
}
if (isset($validated['weight'])) {
$item->weight = $validated['weight'];
}
if (isset($validated['publish_amount'])) {
$item->publish_amount = $validated['publish_amount'];
}
if (isset($validated['actual_amount'])) {
$item->actual_amount = $validated['actual_amount'];
}
// 重算本行差额
$item->diff_amount = bcsub((string) $item->publish_amount, (string) $item->actual_amount, 2);
$item->save();
$this->refreshReconSummary((int) $item->recon_id);
return $this->success(['diff_amount' => $item->diff_amount]);
}
/** D8 对账状态标记翻转 */
#[PutRoute(route: '/{id}/toggle', authorize: 'item.update', where: ['id' => '[0-9]+'])]
public function toggle(int $id): JsonResponse
{
$item = ReconciliationItemModel::find($id);
if (empty($item)) {
throw new RepositoryException('对账明细不存在');
}
$this->assertEditable($item);
$item->is_reconciled = $item->is_reconciled === ReconciliationItemModel::RECONCILED
? ReconciliationItemModel::NOT_RECONCILED
: ReconciliationItemModel::RECONCILED;
$item->save();
return $this->success(['is_reconciled' => $item->is_reconciled]);
}
/** D6 单品级门店备注 */
#[PutRoute(route: '/{id}/remark', authorize: 'item.update', where: ['id' => '[0-9]+'])]
public function remark(int $id, Request $request): JsonResponse
{
$data = $request->validate([
'store_remark' => 'nullable|string|max:255',
], [
'store_remark.max' => '备注最长 255 个字符',
]);
$item = ReconciliationItemModel::find($id);
if (empty($item)) {
throw new RepositoryException('对账明细不存在');
}
$this->assertEditable($item);
$item->store_remark = (string) ($data['store_remark'] ?? '');
$item->save();
return $this->success();
}
/**
* 已结算的对账单明细不允许修改
*/
private function assertEditable(ReconciliationItemModel $item): void
{
$recon = ReconciliationModel::find($item->recon_id);
if ($recon !== null && $recon->status === ReconciliationModel::STATUS_SETTLED) {
throw new RepositoryException('对账单已结算,明细不能修改');
}
}
/**
* 明细变更后重算对账单头汇总(publish / actual / diff
*/
private function refreshReconSummary(int $reconId): void
{
$sums = ReconciliationItemModel::query()
->where('recon_id', $reconId)
->selectRaw('COALESCE(SUM(publish_amount), 0) as publish_total, COALESCE(SUM(actual_amount), 0) as actual_total')
->first();
ReconciliationModel::whereKey($reconId)->update([
'publish_amount' => $sums->publish_total,
'actual_amount' => $sums->actual_total,
'diff_amount' => bcsub((string) $sums->publish_total, (string) $sums->actual_total, 2),
]);
}
}
@@ -1,235 +0,0 @@
<?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], '结算表已生成');
}
}
@@ -1,53 +0,0 @@
<?php
namespace App\Http\Controllers\Recon;
use App\Exceptions\RepositoryException;
use App\Models\SettlementModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
/**
* 结算表管理(D9 生成于对账结算,D10 导出下载存档)
*/
#[RequestAttribute('/recon/settlement', 'recon.settlement')]
class SettlementController extends BaseController
{
protected array $searchField = [
'settlement_no' => 'like',
'recon_id' => '=',
'store_id' => '=',
'status' => '=',
];
/** 结算表列表 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
$params = $request->all();
$pageSize = $params['pageSize'] ?? 10;
$data = $this->buildSearch(
$params,
SettlementModel::query()->with(['store:id,name', 'recon:id,recon_no,title'])
)
->orderBy('id', 'desc')
->paginate($pageSize)
->toArray();
return $this->success($data);
}
/** 结算表详情 */
#[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
public function detail(int $id): JsonResponse
{
$settlement = SettlementModel::with(['store:id,name', 'recon:id,recon_no,title', 'operator:id,nickname'])
->find($id);
if (empty($settlement)) {
throw new RepositoryException('结算表不存在');
}
return $this->success($settlement->toArray());
}
}
@@ -1,38 +0,0 @@
<?php
namespace App\Http\Requests\Recon;
use Modules\Common\Http\Requests\BaseFormRequest;
/**
* 对账明细修改 验证(D4:订货量/称重/数量/金额/商品信息;diff 与头汇总由后端重算)
*/
class ReconItemUpdateRequest extends BaseFormRequest
{
protected $stopOnFirstFailure = true;
public function rules(): array
{
return [
'product_name' => 'nullable|string|max:100',
'quantity' => 'nullable|numeric|min:0',
'weight' => 'nullable|numeric|min:0',
'publish_amount' => 'nullable|numeric|min:0',
'actual_amount' => 'nullable|numeric|min:0',
];
}
public function messages(): array
{
return [
'quantity.numeric' => '订货量必须为数字',
'quantity.min' => '订货量不能小于 0',
'weight.numeric' => '称重必须为数字',
'weight.min' => '称重不能小于 0',
'publish_amount.numeric' => '公布金额必须为数字',
'publish_amount.min' => '公布金额不能小于 0',
'actual_amount.numeric' => '实际金额必须为数字',
'actual_amount.min' => '实际金额不能小于 0',
];
}
}
@@ -1,46 +0,0 @@
<?php
namespace App\Http\Requests\Recon;
use Modules\Common\Http\Requests\BaseFormRequest;
/**
* 财务对账单 创建/编辑 验证
*/
class ReconciliationFormRequest extends BaseFormRequest
{
protected $stopOnFirstFailure = true;
protected function prepareForValidation(): void
{
$this->merge([
'category_id' => (int) ($this->input('category_id') ?? 0),
'supplier_id' => (int) ($this->input('supplier_id') ?? 0),
]);
}
public function rules(): array
{
return [
'title' => 'required|string|max:100',
'period_start' => 'required|date_format:Y-m-d',
'period_end' => 'required|date_format:Y-m-d|after_or_equal:period_start',
'category_id' => 'required|integer|min:0',
'supplier_id' => 'required|integer|min:0',
'remark' => 'nullable|string|max:255',
];
}
public function messages(): array
{
return [
'title.required' => '对账标题不能为空',
'title.max' => '对账标题最长 100 个字符',
'period_start.required' => '请选择对账周期开始日期',
'period_start.date_format' => '开始日期格式为 Y-m-d',
'period_end.required' => '请选择对账周期结束日期',
'period_end.date_format' => '结束日期格式为 Y-m-d',
'period_end.after_or_equal' => '结束日期不能早于开始日期',
];
}
}