移除对账
This commit is contained in:
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\ReconciliationItemModel;
|
||||
use App\Models\SettlementModel;
|
||||
use Illuminate\Support\Collection;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
/**
|
||||
* D10 结算表导出(结算头 + 来源对账单中该门店的明细)
|
||||
*/
|
||||
class SettlementExport implements FromCollection, WithHeadings, WithMapping, WithStyles
|
||||
{
|
||||
public function __construct(private readonly SettlementModel $settlement)
|
||||
{
|
||||
}
|
||||
|
||||
public function collection(): Collection
|
||||
{
|
||||
return ReconciliationItemModel::query()
|
||||
->where('recon_id', $this->settlement->recon_id)
|
||||
->where('store_id', $this->settlement->store_id)
|
||||
->orderBy('sort')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return ['品名', '数量', '称重', '公布金额', '实际金额', '差额', '对账状态', '门店备注'];
|
||||
}
|
||||
|
||||
public function map($item): array
|
||||
{
|
||||
return [
|
||||
$item->product_name,
|
||||
(float) $item->quantity,
|
||||
(float) $item->weight,
|
||||
(float) $item->publish_amount,
|
||||
(float) $item->actual_amount,
|
||||
(float) $item->diff_amount,
|
||||
$item->is_reconciled ? '已对账' : '未对账',
|
||||
$item->store_remark,
|
||||
];
|
||||
}
|
||||
|
||||
public function styles(Worksheet $sheet): array
|
||||
{
|
||||
$sheet->freezePane('A2');
|
||||
|
||||
return [
|
||||
1 => ['font' => ['bold' => true]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* PDF 模板视图数据
|
||||
*
|
||||
* @return array{settlement: SettlementModel, storeName: string, reconNo: string, items: Collection}
|
||||
*/
|
||||
public function viewData(): array
|
||||
{
|
||||
return [
|
||||
'settlement' => $this->settlement,
|
||||
'storeName' => $this->settlement->store?->name ?? '',
|
||||
'reconNo' => $this->settlement->recon?->recon_no ?? '',
|
||||
'items' => $this->collection(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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.item,authorize: 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' => '结束日期不能早于开始日期',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 对账明细模型(订货量/称重/数量/金额可修改,diff = publish − actual)
|
||||
*/
|
||||
class ReconciliationItemModel extends Model
|
||||
{
|
||||
/** 未对账 */
|
||||
public const NOT_RECONCILED = 0;
|
||||
/** 已对账 */
|
||||
public const RECONCILED = 1;
|
||||
|
||||
protected $table = 'reconciliation_item';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'recon_id',
|
||||
'store_id',
|
||||
'order_item_id',
|
||||
'product_id',
|
||||
'product_name',
|
||||
'quantity',
|
||||
'weight',
|
||||
'publish_amount',
|
||||
'actual_amount',
|
||||
'diff_amount',
|
||||
'is_reconciled',
|
||||
'store_remark',
|
||||
'sort',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'recon_id' => 'integer',
|
||||
'store_id' => 'integer',
|
||||
'order_item_id' => 'integer',
|
||||
'product_id' => 'integer',
|
||||
'quantity' => 'decimal:2',
|
||||
'weight' => 'decimal:3',
|
||||
'publish_amount' => 'decimal:2',
|
||||
'actual_amount' => 'decimal:2',
|
||||
'diff_amount' => 'decimal:2',
|
||||
'is_reconciled' => 'integer',
|
||||
'sort' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所属对账单
|
||||
*/
|
||||
public function recon(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ReconciliationModel::class, 'recon_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 所属门店
|
||||
*/
|
||||
public function store(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(StoreModel::class, 'store_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 对账商品
|
||||
*/
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ProductModel::class, 'product_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 溯源订货明细
|
||||
*/
|
||||
public function orderItem(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(StoreOrderItemModel::class, 'order_item_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Modules\SystemUser\Models\SysUserModel;
|
||||
|
||||
/**
|
||||
* 财务对账模型(公布金额 vs 实际金额 vs 差额,按品类/供应商筛选)
|
||||
*/
|
||||
class ReconciliationModel extends Model
|
||||
{
|
||||
/** 状态:草稿 */
|
||||
public const STATUS_DRAFT = 0;
|
||||
/** 状态:对账中 */
|
||||
public const STATUS_WORKING = 1;
|
||||
/** 状态:已结算 */
|
||||
public const STATUS_SETTLED = 2;
|
||||
|
||||
protected $table = 'reconciliation';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'recon_no',
|
||||
'title',
|
||||
'period_start',
|
||||
'period_end',
|
||||
'category_id',
|
||||
'supplier_id',
|
||||
'publish_amount',
|
||||
'actual_amount',
|
||||
'diff_amount',
|
||||
'status',
|
||||
'operator_id',
|
||||
'remark',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'period_start' => 'date:Y-m-d',
|
||||
'period_end' => 'date:Y-m-d',
|
||||
'category_id' => 'integer',
|
||||
'supplier_id' => 'integer',
|
||||
'publish_amount' => 'decimal:2',
|
||||
'actual_amount' => 'decimal:2',
|
||||
'diff_amount' => 'decimal:2',
|
||||
'status' => 'integer',
|
||||
'operator_id' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 制单人(后台系统用户)
|
||||
*/
|
||||
public function operator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SysUserModel::class, 'operator_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 对账明细
|
||||
*/
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReconciliationItemModel::class, 'recon_id', 'id')->orderBy('sort');
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算表
|
||||
*/
|
||||
public function settlements(): HasMany
|
||||
{
|
||||
return $this->hasMany(SettlementModel::class, 'recon_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\SystemUser\Models\SysUserModel;
|
||||
|
||||
/**
|
||||
* 结算表模型(对账结算按门店聚合生成,可导出存档)
|
||||
*/
|
||||
class SettlementModel extends Model
|
||||
{
|
||||
/** 状态:待结算 */
|
||||
public const STATUS_PENDING = 0;
|
||||
/** 状态:已结算 */
|
||||
public const STATUS_SETTLED = 1;
|
||||
|
||||
protected $table = 'settlement';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'settlement_no',
|
||||
'recon_id',
|
||||
'store_id',
|
||||
'period_start',
|
||||
'period_end',
|
||||
'total_amount',
|
||||
'actual_amount',
|
||||
'diff_amount',
|
||||
'status',
|
||||
'file_path',
|
||||
'operator_id',
|
||||
'settled_at',
|
||||
'remark',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'recon_id' => 'integer',
|
||||
'store_id' => 'integer',
|
||||
'period_start' => 'date:Y-m-d',
|
||||
'period_end' => 'date:Y-m-d',
|
||||
'total_amount' => 'decimal:2',
|
||||
'actual_amount' => 'decimal:2',
|
||||
'diff_amount' => 'decimal:2',
|
||||
'status' => 'integer',
|
||||
'operator_id' => 'integer',
|
||||
'settled_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 来源对账单
|
||||
*/
|
||||
public function recon(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ReconciliationModel::class, 'recon_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算门店
|
||||
*/
|
||||
public function store(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(StoreModel::class, 'store_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 制单人(后台系统用户)
|
||||
*/
|
||||
public function operator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SysUserModel::class, 'operator_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,6 @@ class BillNumberService
|
||||
private const NUMBER_SOURCES = [
|
||||
'PO' => ['purchase_order', 'purchase_no'],
|
||||
'SO' => ['store_order', 'order_no'],
|
||||
'RC' => ['reconciliation', 'recon_no'],
|
||||
'JS' => ['settlement', 'settlement_no'],
|
||||
'ZD' => ['bill', 'bill_no'],
|
||||
'ZF' => ['payment', 'payment_no'],
|
||||
];
|
||||
@@ -33,7 +31,7 @@ class BillNumberService
|
||||
/**
|
||||
* 生成业务单号
|
||||
*
|
||||
* @param string $prefix 业务前缀:PO 采购单 / SO 订货单 / RC 对账 / JS 结算 / ZD 账单 / ZF 支付
|
||||
* @param string $prefix 业务前缀:PO 采购单 / SO 订货单 / ZD 账单 / ZF 支付
|
||||
* @return string 如 PO202607230001
|
||||
*/
|
||||
public function make(string $prefix): string
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use App\Models\ProductCategoryModel;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\ReconciliationItemModel;
|
||||
use App\Models\ReconciliationModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 对账明细构建(D1 品类 / D2 供应商筛选)
|
||||
*
|
||||
* 流程(事务内,可重复 build:先清后建):
|
||||
* 1. 按周期 + 品类(含子孙分类)+ 供应商拉取「已完成」门店订单的订货明细
|
||||
* 2. 每条订货明细 → 一条对账明细:
|
||||
* publish_amount = 订货金额(order_item.amount = 每包等级价×数量)
|
||||
* actual_amount = 采购成本(数量 × 每包成本价;单价/包规不参与金额计算)
|
||||
* diff = publish − actual,冗余 product_name / store_id
|
||||
* 3. 汇总写回头的 publish/actual/diff_amount,status → 对账中
|
||||
*/
|
||||
class ReconciliationBuildService
|
||||
{
|
||||
/**
|
||||
* @return int 生成的对账明细数
|
||||
*/
|
||||
public function build(ReconciliationModel $recon): int
|
||||
{
|
||||
return DB::transaction(function () use ($recon) {
|
||||
// 1. 按周期 + 品类 + 供应商拉取已完成订单的订货明细
|
||||
$itemQuery = StoreOrderItemModel::query()
|
||||
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
|
||||
->where('store_order.status', StoreOrderModel::STATUS_COMPLETED)
|
||||
->whereNull('store_order.deleted_at')
|
||||
->whereDate('store_order.order_date', '>=', $recon->period_start)
|
||||
->whereDate('store_order.order_date', '<=', $recon->period_end)
|
||||
->select('store_order_item.*');
|
||||
|
||||
if ((int) $recon->supplier_id > 0) {
|
||||
$itemQuery->where('store_order_item.supplier_id', $recon->supplier_id);
|
||||
}
|
||||
if ((int) $recon->category_id > 0) {
|
||||
$itemQuery->whereIn(
|
||||
'store_order_item.category_id',
|
||||
$this->descendantCategoryIds((int) $recon->category_id)
|
||||
);
|
||||
}
|
||||
|
||||
$orderItems = $itemQuery->get()->makeVisible('cost_price');
|
||||
if ($orderItems->isEmpty()) {
|
||||
throw new RepositoryException('周期内无符合筛选条件的已完成订单数据,无法生成对账明细');
|
||||
}
|
||||
|
||||
// 2. 先清后建(幂等)
|
||||
ReconciliationItemModel::where('recon_id', $recon->id)->delete();
|
||||
|
||||
$publishTotal = '0';
|
||||
$actualTotal = '0';
|
||||
$rows = [];
|
||||
$sort = 1;
|
||||
$now = now();
|
||||
foreach ($orderItems as $orderItem) {
|
||||
$publish = (string) $orderItem->amount;
|
||||
$actual = bcmul((string) $orderItem->quantity, (string) ($orderItem->cost_price ?? '0'), 2);
|
||||
$publishTotal = bcadd($publishTotal, $publish, 2);
|
||||
$actualTotal = bcadd($actualTotal, $actual, 2);
|
||||
|
||||
$rows[] = [
|
||||
'recon_id' => $recon->id,
|
||||
'store_id' => $orderItem->store_id,
|
||||
'order_item_id' => $orderItem->id,
|
||||
'product_id' => $orderItem->product_id,
|
||||
'product_name' => $orderItem->product_name,
|
||||
'quantity' => $orderItem->quantity,
|
||||
'weight' => $orderItem->weight,
|
||||
'publish_amount' => $publish,
|
||||
'actual_amount' => $actual,
|
||||
'diff_amount' => bcsub($publish, $actual, 2),
|
||||
'is_reconciled' => ReconciliationItemModel::NOT_RECONCILED,
|
||||
'store_remark' => '',
|
||||
'sort' => $sort++,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
ReconciliationItemModel::insert($rows);
|
||||
|
||||
// 3. 汇总写回头 + 状态流转
|
||||
$recon->publish_amount = $publishTotal;
|
||||
$recon->actual_amount = $actualTotal;
|
||||
$recon->diff_amount = bcsub($publishTotal, $actualTotal, 2);
|
||||
$recon->status = ReconciliationModel::STATUS_WORKING;
|
||||
$recon->save();
|
||||
|
||||
return count($rows);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类自身 + 全部子孙分类ID(多级分类下按顶级分类筛选)
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private function descendantCategoryIds(int $categoryId): array
|
||||
{
|
||||
$parentMap = ProductCategoryModel::pluck('parent_id', 'id');
|
||||
$ids = [$categoryId];
|
||||
$queue = [$categoryId];
|
||||
while ($queue !== []) {
|
||||
$current = array_shift($queue);
|
||||
foreach ($parentMap as $id => $parentId) {
|
||||
if ((int) $parentId === $current && ! in_array((int) $id, $ids, true)) {
|
||||
$ids[] = (int) $id;
|
||||
$queue[] = (int) $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user